diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 54b5cda0f..6114417c9 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore RUN apt-get update && apt-get install --no-install-recommends -y \ cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \ libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \ - libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev && \ + libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir -U \ platformio==6.1.16 \ diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..65326bb6d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix \ No newline at end of file diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index c048b7ac2..e93796614 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -100,7 +100,7 @@ runs: id: version - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }} overwrite: true diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 80f5c6855..8e461998a 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -8,8 +8,6 @@ runs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install dependencies shell: bash diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 14601b058..24e11bd4d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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` 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`** - 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 +class MyModule : public ProtobufModule, 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 newStatus; +newStatus.notifyObservers(&status); + +// Observer receives events via callback +CallbackObserver statusObserver = + CallbackObserver(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`) +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///` -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 diff --git a/.github/prompts/new-module.prompt.md b/.github/prompts/new-module.prompt.md new file mode 100644 index 000000000..8569a622c --- /dev/null +++ b/.github/prompts/new-module.prompt.md @@ -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`** — 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, 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 statusObserver = + CallbackObserver(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/` diff --git a/.github/prompts/new-sensor.prompt.md b/.github/prompts/new-sensor.prompt.md new file mode 100644 index 000000000..e02fc2462 --- /dev/null +++ b/.github/prompts/new-sensor.prompt.md @@ -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 // 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 diff --git a/.github/prompts/new-variant.prompt.md b/.github/prompts/new-variant.prompt.md new file mode 100644 index 000000000..1a324cea9 --- /dev/null +++ b/.github/prompts/new-variant.prompt.md @@ -0,0 +1,178 @@ +# New Hardware Variant + +Guide for adding a new Meshtastic hardware variant to the firmware. + +## Directory Structure + +Create under `variants///`: + +``` +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///variant.h` with pin definitions +- [ ] Create `variants///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 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0142c57a2..8f7670fa5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,16 +4,16 @@ - Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback - is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc... + is appreciated." This will allow other devs to potentially save you time by not accidentally duplicating work etc... - Please do not check in files that don't have real changes - Please do not reformat lines that you didn't have to change the code on -- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the linux version), +- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the Linux version), because it automatically follows our indentation rules and its auto reformatting will not cause spurious changes to lines. - If your PR fixes a bug, mention "fixes #bugnum" somewhere in your pull request description. - If your other co-developers have comments on your PR please tweak as needed. - Please also enable "Allow edits by maintainers". - Please do not submit untested code. -- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes. +- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and community members can help test your changes. - If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord ## 🤝 Attestations diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index de114be1c..d1bcd8898 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -16,8 +16,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: @@ -28,8 +27,6 @@ jobs: with: submodules: recursive path: meshtasticd - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install deps shell: bash @@ -42,7 +39,8 @@ jobs: sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 + if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target' + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} id: gpg @@ -60,11 +58,11 @@ jobs: run: debian/ci_pack_sdeb.sh env: SERIES: ${{ inputs.series }} - GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }} + GPG_KEY_ID: ${{ steps.gpg.outputs.keyid || '' }} PKG_VERSION: ${{ steps.version.outputs.deb }} - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src overwrite: true diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 23690766a..470104688 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -26,8 +26,6 @@ jobs: - uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Build ${{ inputs.platform }} id: build @@ -111,7 +109,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-firmware with: name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} @@ -127,7 +125,7 @@ jobs: release/device-*.bat - name: Store manifests as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-manifest with: name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 9cc0bac78..806df4e7a 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -4,9 +4,14 @@ on: workflow_dispatch: inputs: # trunk-ignore(checkov/CKV_GHA_7) + target: + type: string + required: false + description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. arch: type: choice options: + - all - esp32 - esp32s3 - esp32c3 @@ -15,32 +20,18 @@ on: - rp2040 - rp2350 - stm32 - target: - type: string - required: false - description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. - # find-target: - # type: boolean - # default: true - # description: 'Find the available targets' + description: Choose an arch to limit the search, or 'all' to search all architectures. + default: all permissions: read-all jobs: find-targets: - if: ${{ inputs.target == '' }} strategy: fail-fast: false matrix: arch: - - esp32 - - esp32s3 - - esp32c3 - - esp32c6 - - nrf52840 - - rp2040 - - rp2350 - - stm32 + - all runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 @@ -51,14 +42,37 @@ jobs: - run: pip install -U platformio - name: Generate matrix id: jsonStep + env: + BUILDTARGET: ${{ inputs.target }} + MATRIXARCH: ${{ inputs.arch }} run: | TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra) - echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY - echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY - echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY - echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY - echo "Targets:" >> $GITHUB_STEP_SUMMARY - echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY + if [ "$BUILDTARGET" = "" ]; then + echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY + echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY + echo "Arch: $MATRIXARCH" >> $GITHUB_STEP_SUMMARY + echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY + echo "## 🎯 The following target boards are available to build:" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY + echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY + echo $TARGETS | jq -r 'sort_by(.board) | sort_by(.platform) |.[] | "| " + .platform + " | " + .board + " |" ' >> $GITHUB_STEP_SUMMARY + else + echo "We build this one:" >> $GITHUB_STEP_SUMMARY + ARCH=$(echo "$TARGETS" | jq --arg BUILDTARGET "$BUILDTARGET" -r '.[] | select(.board==$BUILDTARGET) | .platform') + echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY + echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY + echo "| $ARCH | "$BUILDTARGET" |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "$ARCH" == "" ]]; then + echo "## ❌ Error: Target "$BUILDTARGET" not found!" >> $GITHUB_STEP_SUMMARY + else + echo "## ✅ Target "$BUILDTARGET" found, proceeding to build." >> $GITHUB_STEP_SUMMARY + fi + echo "You may need to refresh this page to make the built firmware appear below." >> $GITHUB_STEP_SUMMARY + echo "arch=$ARCH" >> $GITHUB_OUTPUT + fi + outputs: + arch: ${{ steps.jsonStep.outputs.arch }} version: if: ${{ inputs.target != '' }} @@ -78,16 +92,16 @@ jobs: build: if: ${{ inputs.target != '' && inputs.arch != 'native' }} - needs: [version] + needs: [version, find-targets] uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} pio_env: ${{ inputs.target }} - platform: ${{ inputs.arch }} + platform: ${{ needs.find-targets.outputs.arch }} gather-artifacts: permissions: - contents: write + contents: read pull-requests: write runs-on: ubuntu-latest needs: [version, build] @@ -98,7 +112,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: path: ./ pattern: firmware-*-* @@ -111,7 +125,7 @@ jobs: run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - name: Repackage in single firmware zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} overwrite: true @@ -127,7 +141,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -146,7 +160,7 @@ jobs: run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip overwrite: true diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index 392faeb8a..16363f562 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: push: branches: - - master + - develop # Default branch, same as 'cron' above paths: - debian/** - "*.rpkg" @@ -16,7 +16,7 @@ on: - .github/workflows/hook_copr.yml permissions: - contents: write + contents: read packages: write jobs: @@ -35,8 +35,8 @@ jobs: series: - jammy # 22.04 LTS - noble # 24.04 LTS - - plucky # 25.04 - questing # 25.10 + - resolute # 26.04 LTS uses: ./.github/workflows/package_ppa.yml with: ppa_repo: ppa:meshtastic/daily diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 8d19af894..d9b23a7e8 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -37,7 +37,7 @@ on: value: ${{ jobs.docker-build.outputs.digest }} permissions: - contents: write + contents: read packages: write jobs: @@ -50,35 +50,41 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - - name: Docker login - if: ${{ inputs.push }} - uses: docker/login-action@v3 - with: - username: meshtastic - password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 + with: + cache-image: true - name: Docker setup - uses: docker/setup-buildx-action@v3 + 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 + with: + username: meshtastic + password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} + - name: Docker tag id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: meshtastic/meshtasticd tags: | @@ -86,7 +92,7 @@ jobs: flavor: latest=false - name: Docker build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 id: docker_variant with: context: . @@ -97,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 diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index 396ddb68e..b2fd12599 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -12,7 +12,7 @@ on: type: string permissions: - contents: write + contents: read packages: write jobs: @@ -86,8 +86,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: | @@ -139,14 +137,14 @@ jobs: id: tags - name: Docker login - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: meshtastic password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} - name: Docker meta (Debian) id: meta_debian - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: meshtastic/meshtasticd tags: | @@ -167,7 +165,7 @@ jobs: - name: Docker meta (Alpine) id: meta_alpine - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: meshtastic/meshtasticd tags: | diff --git a/.github/workflows/hook_copr.yml b/.github/workflows/hook_copr.yml index eb4ebc57b..c419848a8 100644 --- a/.github/workflows/hook_copr.yml +++ b/.github/workflows/hook_copr.yml @@ -11,8 +11,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-copr-hook: @@ -22,8 +21,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{ github.ref }} - repository: ${{ github.repository }} - name: Trigger COPR build uses: vidplace7/copr-build@main diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 6b48e8128..88395600a 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -15,8 +15,7 @@ on: - "**.md" - version.properties - # Note: This is different from "pull_request". Need to specify ref when doing checkouts. - pull_request_target: + pull_request: branches: - master - develop @@ -29,6 +28,8 @@ on: workflow_dispatch: +permissions: read-all + jobs: setup: strategy: @@ -88,8 +89,6 @@ jobs: - uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Check ${{ matrix.check.board }} uses: meshtastic/gh-action-firmware@main with: @@ -126,9 +125,16 @@ jobs: test-native: if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }} + permissions: # Needed for dorny/test-reporter. + contents: read + actions: read + checks: write uses: ./.github/workflows/test_native.yml docker: + permissions: # Needed for pushing to GHCR. + contents: read + packages: write strategy: fail-fast: false matrix: @@ -153,9 +159,6 @@ jobs: gather-artifacts: # trunk-ignore(checkov/CKV2_GHA_1) if: github.repository == 'meshtastic/firmware' - permissions: - contents: write - pull-requests: write strategy: fail-fast: false matrix: @@ -173,11 +176,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -187,7 +187,7 @@ jobs: run: ls -R - name: Repackage in single firmware zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -205,7 +205,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -224,20 +224,13 @@ jobs: run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true path: ./*.elf retention-days: 30 - - uses: scruplelesswizard/comment-artifact@main - if: ${{ github.event_name == 'pull_request' }} - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" - github-token: ${{ secrets.GITHUB_TOKEN }} - shame: if: github.repository == 'meshtastic/firmware' continue-on-error: true @@ -245,42 +238,44 @@ jobs: needs: [build] steps: - uses: actions/checkout@v6 - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' with: filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head) fetch-depth: 0 - name: Download the current manifests - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: path: ./manifests-new/ pattern: manifest-* merge-multiple: true - name: Upload combined manifests for later commit and global stats crunching. - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-manifest with: name: manifests-${{ github.sha }} overwrite: true path: manifests-new/*.mt.json - name: Find the merge base - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV env: base: ${{ github.base_ref }} head: ${{ github.sha }} # Currently broken (for-loop through EVERY artifact -- rate limiting) # - name: Download the old manifests - # if: github.event_name == 'pull_request_target' + # if: github.event_name == 'pull_request' # run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/ # env: # GH_TOKEN: ${{ github.token }} # merge_base: ${{ env.MERGE_BASE }} # repo: ${{ github.repository }} # - name: Do scan and post comment - # if: github.event_name == 'pull_request_target' + # if: github.event_name == 'pull_request' # run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/ release-artifacts: + permissions: # Needed for 'gh release upload'. + contents: write runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} outputs: @@ -306,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<> $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 @@ -324,14 +321,14 @@ jobs: body: ${{ steps.release_notes.outputs.notes }} - name: Download source deb - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -355,7 +352,7 @@ jobs: }' > firmware-${{ needs.version.outputs.long }}.json - name: Save Release manifest artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: manifest-${{ needs.version.outputs.long }} overwrite: true @@ -372,6 +369,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release-firmware: + permissions: # Needed for 'gh release upload'. + contents: write strategy: fail-fast: false matrix: @@ -396,7 +395,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -413,7 +412,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -454,14 +453,14 @@ jobs: python-version: 3.x - name: Get firmware artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./publish - name: Get manifest artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: manifest-${{ needs.version.outputs.long }} path: ./publish @@ -469,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 }} diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml deleted file mode 100644 index bd3f6d4eb..000000000 --- a/.github/workflows/merge_queue.yml +++ /dev/null @@ -1,371 +0,0 @@ -name: Merge Queue -# Not sure how concurrency works in merge_queue, removing for now. -# concurrency: -# group: merge-queue-${{ github.head_ref || github.run_id }} -# cancel-in-progress: true -on: - # Merge group is a special trigger that is used to trigger the workflow when a merge group is created. - merge_group: - -jobs: - setup: - strategy: - fail-fast: true - matrix: - arch: - - all - - check - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: 3.x - cache: pip - - run: pip install -U platformio - - name: Generate matrix - id: jsonStep - run: | - if [[ "$GITHUB_HEAD_REF" == "" ]]; then - TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}}) - else - TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr) - fi - echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF" - echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT - outputs: - all: ${{ steps.jsonStep.outputs.all }} - check: ${{ steps.jsonStep.outputs.check }} - - version: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Get release version string - run: | - echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT - echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT - id: version - env: - BUILD_LOCATION: local - outputs: - long: ${{ steps.version.outputs.long }} - deb: ${{ steps.version.outputs.deb }} - - check: - needs: setup - strategy: - fail-fast: true - matrix: - check: ${{ fromJson(needs.setup.outputs.check) }} - - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_dispatch' }} - steps: - - uses: actions/checkout@v6 - - name: Build base - id: base - uses: ./.github/actions/setup-base - - name: Check ${{ matrix.check.board }} - run: bin/check-all.sh ${{ matrix.check.board }} - - build: - needs: [setup, version] - strategy: - matrix: - build: ${{ fromJson(needs.setup.outputs.all) }} - uses: ./.github/workflows/build_firmware.yml - with: - version: ${{ needs.version.outputs.long }} - pio_env: ${{ matrix.build.board }} - platform: ${{ matrix.build.platform }} - - build-debian-src: - if: github.repository == 'meshtastic/firmware' - uses: ./.github/workflows/build_debian_src.yml - with: - series: UNRELEASED - build_location: local - secrets: inherit - - package-pio-deps-native-tft: - if: ${{ github.event_name == 'workflow_dispatch' }} - uses: ./.github/workflows/package_pio_deps.yml - with: - pio_env: native-tft - secrets: inherit - - test-native: - if: ${{ !contains(github.ref_name, 'event/') }} - uses: ./.github/workflows/test_native.yml - - docker: - strategy: - fail-fast: false - matrix: - distro: [debian, alpine] - platform: [linux/amd64, linux/arm64, linux/arm/v7] - pio_env: [native, native-tft] - exclude: - - distro: alpine - platform: linux/arm/v7 - - pio_env: native-tft - platform: linux/arm64 - - pio_env: native-tft - platform: linux/arm/v7 - uses: ./.github/workflows/docker_build.yml - with: - distro: ${{ matrix.distro }} - platform: ${{ matrix.platform }} - runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} - pio_env: ${{ matrix.pio_env }} - push: false - - gather-artifacts: - # trunk-ignore(checkov/CKV2_GHA_1) - permissions: - contents: write - pull-requests: write - strategy: - fail-fast: false - matrix: - arch: - - esp32 - - esp32s3 - - esp32c3 - - esp32c6 - - nrf52840 - - rp2040 - - rp2350 - - stm32 - runs-on: ubuntu-latest - needs: [version, build] - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - uses: actions/download-artifact@v7 - with: - path: ./ - pattern: firmware-${{matrix.arch}}-* - merge-multiple: true - - - name: Display structure of downloaded files - run: ls -R - - - name: Move files up - run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - - - name: Repackage in single firmware zip - uses: actions/upload-artifact@v6 - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - overwrite: true - path: | - ./firmware-*.bin - ./firmware-*.uf2 - ./firmware-*.hex - ./firmware-*.zip - ./device-*.sh - ./device-*.bat - ./littlefs-*.bin - ./bleota*bin - ./Meshtastic_nRF52_factory_erase*.uf2 - retention-days: 30 - - - uses: actions/download-artifact@v7 - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./output - - # For diagnostics - - name: Show artifacts - run: ls -lR - - - name: Device scripts permissions - run: | - chmod +x ./output/device-install.sh || true - chmod +x ./output/device-update.sh || true - - - name: Zip firmware - run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - - name: Repackage in single elfs zip - uses: actions/upload-artifact@v6 - with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} - overwrite: true - path: ./*.elf - retention-days: 30 - - - uses: scruplelesswizard/comment-artifact@main - if: ${{ github.event_name == 'pull_request' }} - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" - github-token: ${{ secrets.GITHUB_TOKEN }} - - release-artifacts: - runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} - needs: - - version - - gather-artifacts - - build-debian-src - - package-pio-deps-native-tft - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Create release - uses: softprops/action-gh-release@v2 - id: create_release - with: - draft: true - prerelease: true - name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha - tag_name: v${{ needs.version.outputs.long }} - body: | - Autogenerated by github action, developer should edit as required before publishing... - - - name: Download source deb - uses: actions/download-artifact@v7 - with: - pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src - merge-multiple: true - path: ./output/debian-src - - - name: Download `native-tft` pio deps - uses: actions/download-artifact@v7 - with: - pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./output/pio-deps-native-tft - - - name: Zip Linux sources - working-directory: output - run: | - zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src - zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft - - # For diagnostics - - name: Display structure of downloaded files - run: ls -lR - - - name: Add Linux sources to GtiHub Release - # Only run when targeting master branch with workflow_dispatch - if: ${{ github.ref_name == 'master' }} - run: | - gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip - gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - release-firmware: - strategy: - fail-fast: false - matrix: - arch: - - esp32 - - esp32s3 - - esp32c3 - - esp32c6 - - nrf52840 - - rp2040 - - rp2350 - - stm32 - runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} - needs: [release-artifacts, version] - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - - uses: actions/download-artifact@v7 - with: - pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./output - - - name: Display structure of downloaded files - run: ls -lR - - - name: Device scripts permissions - run: | - chmod +x ./output/device-install.sh || true - chmod +x ./output/device-update.sh || true - - - name: Zip firmware - run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - - uses: actions/download-artifact@v7 - with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./elfs - - - name: Zip debug elfs - run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs - - # For diagnostics - - name: Display structure of downloaded files - run: ls -lR - - - name: Add bins and debug elfs to GitHub Release - # Only run when targeting master branch with workflow_dispatch - if: ${{ github.ref_name == 'master' }} - run: | - gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip - gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-firmware: - runs-on: ubuntu-24.04 - if: ${{ github.event_name == 'workflow_dispatch' }} - needs: [release-firmware, version] - env: - targets: |- - esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - - uses: actions/download-artifact@v7 - with: - pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} - merge-multiple: true - path: ./publish - - - name: Publish firmware to meshtastic.github.io - uses: peaceiris/actions-gh-pages@v4 - env: - # On event/* branches, use the event name as the destination prefix - DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} - with: - deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} - external_repository: meshtastic/meshtastic.github.io - publish_branch: master - publish_dir: ./publish - destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} - keep_files: true - user_name: github-actions[bot] - user_email: github-actions[bot]@users.noreply.github.com - commit_message: ${{ needs.version.outputs.long }} - enable_jekyll: true diff --git a/.github/workflows/models_issue_triage.yml b/.github/workflows/models_issue_triage.yml new file mode 100644 index 000000000..a02646ea0 --- /dev/null +++ b/.github/workflows/models_issue_triage.yml @@ -0,0 +1,213 @@ +name: Issue Triage (Models) + +on: + issues: + types: [opened] + +permissions: + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + triage: + if: ${{ github.repository == 'meshtastic/firmware' && github.event.issue.user.type != 'Bot' }} + runs-on: ubuntu-latest + steps: + # ───────────────────────────────────────────────────────────────────────── + # Step 1: Quality check (spam/AI-slop detection) - runs first, exits early if spam + # ───────────────────────────────────────────────────────────────────────── + - name: Detect spam or low-quality content + uses: actions/ai-inference@v2 + id: quality + continue-on-error: true + with: + max-tokens: 20 + prompt: | + Is this GitHub issue spam, AI-generated slop, or low quality? + + Title: ${{ github.event.issue.title }} + Body: ${{ github.event.issue.body }} + + Respond with exactly one of: spam, ai-generated, needs-review, ok + system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. + model: openai/gpt-4o-mini + + - name: Apply quality label if needed + if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' + uses: actions/github-script@v9 + env: + QUALITY_LABEL: ${{ steps.quality.outputs.response }} + with: + script: | + const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'spam': { color: 'd73a4a', description: 'Possible spam' }, + 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, + 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); + + // Set output to skip remaining steps + core.setOutput('is_spam', 'true'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 2: Duplicate detection - only if not spam + # ───────────────────────────────────────────────────────────────────────── + - name: Detect duplicate issues + if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' + uses: pelikhan/action-genai-issue-dedup@bdb3b5d9451c1090ffcdf123d7447a5e7c7a2528 # v0.0.19 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + # ───────────────────────────────────────────────────────────────────────── + # Step 3: Completeness check + auto-labeling (combined into one AI call) + # ───────────────────────────────────────────────────────────────────────── + - name: Determine if completeness check should be skipped + if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' + uses: actions/github-script@v9 + id: check-skip + with: + script: | + const title = (context.payload.issue.title || '').toLowerCase(); + const labels = (context.payload.issue.labels || []).map(label => label.name); + const hasFeatureRequest = title.includes('feature request'); + const hasEnhancement = labels.includes('enhancement'); + const shouldSkip = hasFeatureRequest && hasEnhancement; + core.setOutput('should_skip', shouldSkip ? 'true' : 'false'); + + - name: Analyze issue completeness and determine labels + if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' + uses: actions/ai-inference@v2 + id: analysis + continue-on-error: true + with: + prompt: | + Analyze this GitHub issue for completeness and determine if it needs labels. + + IMPORTANT: Distinguish between: + - Device/firmware bugs (crashes, reboots, lockups, radio/GPS/display/power issues) - these need device logs + - Build/release/packaging issues (missing files, CI failures, download problems) - these do NOT need device logs + - Documentation or website issues - these do NOT need device logs + + If this is a device/firmware bug, request device logs and explain how to get them: + + Web Flasher logs: + - Go to https://flasher.meshtastic.org + - Connect the device via USB and click Connect + - Open the device console/log output, reproduce the problem, then copy/download and attach/paste the logs + + Meshtastic CLI logs: + - Run: meshtastic --port --noproto + - Reproduce the problem, then copy/paste the terminal output + + Also request key context if missing: device model/variant, firmware version, region, steps to reproduce, expected vs actual. + + Respond ONLY with valid JSON (no markdown, no code fences): + {"complete": true, "comment": "", "label": "none"} + OR + {"complete": false, "comment": "Your helpful comment", "label": "needs-logs"} + + Use "needs-logs" ONLY if this is a device/firmware bug AND no logs are attached. + Use "needs-info" if basic info like firmware version or steps to reproduce are missing. + Use "none" if the issue is complete, is a feature request, or is a build/CI/packaging issue. + + Title: ${{ github.event.issue.title }} + Body: ${{ github.event.issue.body }} + system-prompt: You are a helpful assistant that triages GitHub issues. Be conservative with labels. Only request device logs for actual device/firmware bugs, not for build/release/CI issues. + model: openai/gpt-4o-mini + + - 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@v9 + id: process + env: + AI_RESPONSE: ${{ steps.analysis.outputs.response }} + with: + script: | + let raw = (process.env.AI_RESPONSE || '').trim(); + + // Strip markdown code fences if present + raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim(); + + let complete = true; + let comment = ''; + let label = 'none'; + + try { + const parsed = JSON.parse(raw); + complete = !!parsed.complete; + comment = (parsed.comment ?? '').toString().trim(); + label = (parsed.label ?? 'none').toString().trim().toLowerCase(); + } catch { + // If JSON parse fails, log warning and don't comment (avoid posting raw JSON) + console.log('Failed to parse AI response as JSON:', raw); + complete = true; + comment = ''; + label = 'none'; + } + + // Validate label + const allowedLabels = new Set(['needs-logs', 'needs-info', 'none']); + if (!allowedLabels.has(label)) label = 'none'; + + // Only comment if we have a valid parsed comment (not raw JSON) + const shouldComment = !complete && comment.length > 0 && !comment.startsWith('{'); + core.setOutput('should_comment', shouldComment ? 'true' : 'false'); + core.setOutput('comment_body', comment); + core.setOutput('label', label); + + - name: Apply triage label + if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none' + uses: actions/github-script@v9 + env: + LABEL_NAME: ${{ steps.process.outputs.label }} + with: + script: | + const label = process.env.LABEL_NAME; + const labelMeta = { + 'needs-logs': { color: 'cfd3d7', description: 'Device logs requested for triage' }, + 'needs-info': { color: 'f9d0c4', description: 'More information requested for triage' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.issue.number, labels: [label] }); + + - name: Comment on issue + if: steps.process.outputs.should_comment == 'true' + uses: actions/github-script@v9 + env: + COMMENT_BODY: ${{ steps.process.outputs.comment_body }} + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: process.env.COMMENT_BODY + }); diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml new file mode 100644 index 000000000..f39ee4845 --- /dev/null +++ b/.github/workflows/models_pr_triage.yml @@ -0,0 +1,139 @@ +name: PR Triage (Models) + +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + issues: write + models: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + triage: + if: ${{ github.repository == 'meshtastic/firmware' && github.event.pull_request.user.type != 'Bot' }} + runs-on: ubuntu-latest + steps: + # ───────────────────────────────────────────────────────────────────────── + # Step 1: Check if PR already has automation/type labels (skip if so) + # ───────────────────────────────────────────────────────────────────────── + - name: Check existing labels + uses: actions/github-script@v9 + id: check-labels + with: + script: | + const skipLabels = new Set(['automation']); + const typeLabels = new Set(['bugfix', 'hardware-support', 'enhancement', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']); + const prLabels = context.payload.pull_request.labels.map(l => l.name); + + const shouldSkipAll = prLabels.some(l => skipLabels.has(l)); + const hasTypeLabel = prLabels.some(l => typeLabels.has(l)); + + core.setOutput('skip_all', shouldSkipAll ? 'true' : 'false'); + core.setOutput('has_type_label', hasTypeLabel ? 'true' : 'false'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 2: Quality check (spam/AI-slop detection) + # ───────────────────────────────────────────────────────────────────────── + - name: Detect spam or low-quality content + if: steps.check-labels.outputs.skip_all != 'true' + uses: actions/ai-inference@v2 + id: quality + continue-on-error: true + with: + max-tokens: 20 + prompt: | + Is this GitHub pull request spam, AI-generated slop, or low quality? + + Title: ${{ github.event.pull_request.title }} + Body: ${{ github.event.pull_request.body }} + + Respond with exactly one of: spam, ai-generated, needs-review, ok + system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop. + model: openai/gpt-4o-mini + + - 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@v9 + id: quality-label + env: + QUALITY_LABEL: ${{ steps.quality.outputs.response }} + with: + script: | + const label = (process.env.QUALITY_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'spam': { color: 'd73a4a', description: 'Possible spam' }, + 'ai-generated': { color: 'fbca04', description: 'Possible AI-generated low-quality content' }, + 'needs-review': { color: 'f9d0c4', description: 'Needs human review' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); + + core.setOutput('is_spam', 'true'); + + # ───────────────────────────────────────────────────────────────────────── + # Step 3: Auto-label PR type (bugfix/hardware-support/enhancement) + # Only skip for spam/ai-generated; still classify needs-review PRs + # ───────────────────────────────────────────────────────────────────────── + - name: Classify PR for labeling + if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.quality.outputs.response != 'spam' && steps.quality.outputs.response != 'ai-generated' + uses: actions/ai-inference@v2 + id: classify + continue-on-error: true + with: + max-tokens: 30 + prompt: | + Classify this pull request into exactly one category. + + Return exactly one of: bugfix, hardware-support, enhancement + + Use bugfix if it fixes a bug, crash, or incorrect behavior. + Use hardware-support if it adds or improves support for a specific hardware device/variant. + Use enhancement if it adds a new feature, improves performance, or refactors code. + + Title: ${{ github.event.pull_request.title }} + Body: ${{ github.event.pull_request.body }} + system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label. + model: openai/gpt-4o-mini + + - 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@v9 + env: + TYPE_LABEL: ${{ steps.classify.outputs.response }} + with: + script: | + const label = (process.env.TYPE_LABEL || '').trim().toLowerCase(); + const labelMeta = { + 'bugfix': { color: 'd73a4a', description: 'Bug fix' }, + 'hardware-support': { color: '0e8a16', description: 'Hardware support addition or improvement' }, + 'enhancement': { color: 'a2eeef', description: 'New feature or enhancement' }, + }; + const meta = labelMeta[label]; + if (!meta) return; + + // Ensure label exists + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color: meta.color, description: meta.description }); + } + + // Apply label + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, labels: [label] }); diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml index 63f1fe8a0..b491f0062 100644 --- a/.github/workflows/package_obs.yml +++ b/.github/workflows/package_obs.yml @@ -18,8 +18,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: @@ -58,7 +57,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 82ffe66e9..6bd256f52 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -16,8 +16,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: pkg-pio-libdeps: @@ -27,8 +26,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Python uses: actions/setup-python@v6 @@ -54,9 +51,19 @@ jobs: PLATFORMIO_LIBDEPS_DIR: pio/libdeps PLATFORMIO_PACKAGES_DIR: pio/packages PLATFORMIO_CORE_DIR: pio/core + PLATFORMIO_SETTING_ENABLE_TELEMETRY: 0 + PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL: 3650 + PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD: 10240 + + - name: Mangle platformio cache + # Add "1" to epoch-timestamps of all downloads in the cache. + # This is a hack to prevent internet access at build-time. + run: | + cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak + jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak > pio/core/.cache/downloads/usage.db - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} overwrite: true diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml index 9a463dbea..c51e64e78 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -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 @@ -16,8 +18,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: @@ -28,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: @@ -36,17 +38,15 @@ jobs: with: submodules: recursive path: meshtasticd - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install deps 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@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} id: gpg @@ -60,7 +60,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true @@ -68,7 +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' }} + - 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 < + dput -c /etc/meshtastic-dput.cf + ssh-${up_ppa_repo} + meshtasticd_${up_version}~${up_series}_source.changes diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index d60c9c8ca..bf2239f63 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -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); diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 6306d777f..e3321712e 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -50,7 +50,7 @@ jobs: - name: Download test artifacts if: needs.native-tests.result != 'skipped' - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true @@ -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'); diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index 7f925b67c..a85979c72 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -23,8 +23,8 @@ jobs: series: - jammy # 22.04 LTS - noble # 24.04 LTS - - plucky # 25.04 - questing # 25.10 + - resolute # 26.04 LTS uses: ./.github/workflows/package_ppa.yml with: ppa_repo: |- diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index d93449d6d..95e5c2c3d 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -33,7 +33,7 @@ jobs: # step 3 - name: save report as pipeline artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: report.sarif overwrite: true diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index fc0702bd8..9255975a8 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Stale PR+Issues - uses: actions/stale@v10.1.1 + uses: actions/stale@v10.2.0 with: days-before-stale: 45 stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days. diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index b527c2fd9..2fabf0591 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -16,8 +16,6 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} submodules: recursive - name: Setup native build @@ -59,7 +57,7 @@ jobs: id: version - name: Save coverage information - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }} @@ -72,8 +70,6 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} submodules: recursive - name: Setup native build @@ -94,7 +90,7 @@ jobs: - name: Save test results if: always() # run this step even if previous step failed - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: platformio-test-report-${{ steps.version.outputs.long }} overwrite: true @@ -108,7 +104,7 @@ jobs: sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative. - name: Save coverage information - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }} @@ -128,29 +124,26 @@ jobs: if: always() steps: - uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - name: Download test artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.5.0 + uses: dorny/test-reporter@v3.0.0 with: name: PlatformIO Tests path: testreport.xml reporter: java-junit - name: Download coverage artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }} path: code-coverage-report @@ -163,7 +156,7 @@ jobs: genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report - name: Save Code Coverage Report - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: code-coverage-report-${{ steps.version.outputs.long }} path: code-coverage-report diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 241f2cd10..be5142843 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,7 @@ jobs: node-version: 24 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: version: latest diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index 35565d1e4..e9380467e 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -6,7 +6,7 @@ permissions: read-all jobs: update-protobufs: runs-on: ubuntu-latest - permissions: + permissions: # Needed for peter-evans/create-pull-request. contents: write pull-requests: write steps: diff --git a/.gitignore b/.gitignore index 769603202..43cee78db 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ __pycache__ *~ venv/ +.venv/ release/ .vscode/extensions.json /compile_commands.json @@ -50,3 +51,6 @@ idf_component.yml CMakeLists.txt /sdkconfig.* .dummy/* + +# PYTHONPATH used by the Nix shell +.python3 diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 9d563a39a..d0cbaa8bc 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,31 +4,31 @@ cli: plugins: sources: - id: trunk - ref: v1.7.4 + ref: v1.7.6 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.497 - - renovate@42.84.2 - - prettier@3.8.0 - - trufflehog@3.92.5 + - checkov@3.2.517 + - renovate@43.110.9 + - prettier@3.8.1 + - trufflehog@3.94.3 - yamllint@1.38.0 - - bandit@1.9.3 - - trivy@0.68.2 + - bandit@1.9.4 + - trivy@0.69.3 - taplo@0.10.0 - - ruff@0.14.13 - - isort@7.0.0 - - markdownlint@0.47.0 - - oxipng@10.0.0 - - svgo@4.0.0 - - actionlint@1.7.10 + - ruff@0.15.9 + - isort@8.0.1 + - markdownlint@0.48.0 + - oxipng@10.1.0 + - svgo@4.0.1 + - actionlint@1.7.12 - flake8@7.3.0 - hadolint@2.14.0 - shfmt@3.6.0 - shellcheck@0.11.0 - - black@26.1.0 + - black@26.3.1 - git-diff-check - - gitleaks@8.30.0 + - gitleaks@8.30.1 - clang-format@16.0.3 ignore: - linters: [ALL] diff --git a/Dockerfile b/Dockerfile index 91d3f7796..e00d81658 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ 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 \ libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ - libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev \ + libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware @@ -53,7 +53,7 @@ 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 \ liborcania2.3 libulfius2.7t64 libssl3t64 \ - libx11-6 libinput10 libxkbcommon-x11-0 \ + libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 000000000..12479b36d --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,26 @@ +# Lightweight container for running native PlatformIO tests on non-Linux hosts +FROM python:3.14-slim-trixie + +ENV DEBIAN_FRONTEND=noninteractive +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 \ + 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/* \ + && pip install --no-cache-dir platformio==6.1.19 \ + && useradd --create-home --shell /usr/sbin/nologin meshtastic + +WORKDIR /firmware +RUN chown -R meshtastic:meshtastic /firmware + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD platformio --version || exit 1 + +USER meshtastic + +# Run tests by default; override with docker run args for specific filters +CMD ["platformio", "test", "-e", "coverage", "-v"] diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 64c281788..75c9aa594 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -11,7 +11,7 @@ RUN apk --no-cache add \ bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \ libgpiod-dev yaml-cpp-dev bluez-dev \ libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \ - libx11-dev libinput-dev libxkbcommon-dev sqlite-dev \ + libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \ && rm -rf /var/cache/apk/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware @@ -42,7 +42,7 @@ USER root RUN apk --no-cache add \ shadow libstdc++ libbsd libgpiod yaml-cpp libusb \ - i2c-tools libuv libx11 libinput libxkbcommon \ + i2c-tools libuv libx11 libinput libxkbcommon sdl2 \ && rm -rf /var/cache/apk/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ diff --git a/bin/check-all.sh b/bin/check-all.sh index 29d6b5532..9c7fc694d 100755 --- a/bin/check-all.sh +++ b/bin/check-all.sh @@ -23,4 +23,4 @@ for BOARD in $BOARDS; do CHECK="${CHECK} -e ${BOARD}" done -pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high +pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=low --fail-on-defect=medium --fail-on-defect=high diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index 3c996051e..38fc057a0 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -187,6 +187,7 @@ Logging: LogLevel: info # debug, info, warn, error # TraceFile: /var/log/meshtasticd.json # JSONFile: /packets.json # File location for JSON output of decoded packets +# JSONFileRotate: 60 # Rotate JSON file every N minutes, or 0 for no rotation # JSONFilter: position # filter for packets to save to JSON file # AsciiLogs: true # default if not specified is !isatty() on stdout @@ -214,3 +215,4 @@ General: AvailableDirectory: /etc/meshtasticd/available.d/ # MACAddress: AA:BB:CC:DD:EE:FF # MACAddressSource: eth0 +# APIPort: 4403 diff --git a/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml index 825ab2699..a854dff5b 100644 --- a/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml +++ b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: BananaPi-BPI-R4-sx1262 + support: community + compatible: + - bananapi_bpi-r4 # OpenWrt target + Lora: Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header ## CS: 28 diff --git a/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml index ca5b27ebc..7687e0f58 100644 --- a/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml +++ b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml @@ -1,4 +1,10 @@ ## https://www.mikroe.com/lr-iot-click +Meta: + name: OpenWRT One mikroBUS LR-IOT-CLICK + support: community + compatible: + - openwrt_one # OpenWrt target + Lora: Module: lr1110 # OpenWRT ONE mikroBUS with LR-IOT-CLICK # CS: 25 diff --git a/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml index 6dc1e870d..ab0b62810 100644 --- a/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml +++ b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: OpenWRT One mikroBUS sx1262 + support: community + compatible: + - openwrt_one # OpenWrt target + Lora: Module: sx1262 IRQ: 10 diff --git a/bin/config.d/README.md b/bin/config.d/README.md new file mode 100644 index 000000000..b199fb439 --- /dev/null +++ b/bin/config.d/README.md @@ -0,0 +1,28 @@ +# meshtasticd configuration files + +This directory contains YAML configuration files for meshtasticd. Each file describes a specific hardware configuration, including the LoRa module and pin assignments. These configurations are used by meshtasticd to correctly interface with the hardware. + +## Metadata structure + +Each configuration file includes a `Meta` section that provides information about the configuration. +This configuration is consumed by configuration-selection tools. + +```yaml +Meta: + name: MeshAdv-Pi E22-900M30S # A unique identifier for this configuration. + support: community # community, official, or deprecated; determined by Meshtastic Leads. + compatible: # A list of compatible products or platforms. + - raspberry-pi +``` +`name`: A unique identifier for the configuration, typically reflecting the hardware it supports. + +`support`: Indicates the level of support for this configuration. It can be one of the following: + +- `community`: Supported by the Meshtastic community. Meshtastic Members may not possess, or have not tested this configuration. +- `official`: Fully supported by Meshtastic. Meshtastic Members have tested and verified this configuration. +- `deprecated`: No longer recommended for deployment by Meshtastic. + +`compatible`: A list of compatible products or platforms that can use this configuration. +This will vary depending on the intended use case / platform. +Multiple compatible entries can be included. E.g. Armbian `BOARD` value or OpenWrt `TARGET` value. +These tags can be consumed by different configuration-selection tools, filtering based upon their platform/etc. diff --git a/bin/config.d/display-waveshare-1-44.yaml b/bin/config.d/display-waveshare-1-44.yaml index d37f6cf6a..e6b4f8271 100644 --- a/bin/config.d/display-waveshare-1-44.yaml +++ b/bin/config.d/display-waveshare-1-44.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare 1.44inch LCD HAT + support: community + compatible: + - raspberry-pi + ### Waveshare 1.44inch LCD HAT Display: Panel: ST7735S diff --git a/bin/config.d/display-waveshare-2.8.yaml b/bin/config.d/display-waveshare-2.8.yaml index 2e28276d8..586d1107e 100644 --- a/bin/config.d/display-waveshare-2.8.yaml +++ b/bin/config.d/display-waveshare-2.8.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare 2.8inch LCD HAT + support: community + compatible: + - raspberry-pi + Display: ### Waveshare 2.8inch RPi LCD diff --git a/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml b/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml new file mode 100644 index 000000000..2f2b24603 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml @@ -0,0 +1,30 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: [DIO5, DIO6, DIO7] + MODE_STBY: [LOW, LOW, LOW] + MODE_RX: [LOW, HIGH, LOW] + MODE_TX: [HIGH, HIGH, LOW] + MODE_TX_HP: [HIGH, LOW, LOW] + MODE_TX_HF: [LOW, LOW, LOW] + MODE_GNSS: [LOW, LOW, HIGH] + MODE_WIFI: [LOW, LOW, LOW] + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/femtofox/femtofox_LR1121 generic.yaml b/bin/config.d/femtofox/femtofox_LR1121 generic.yaml new file mode 100644 index 000000000..c66eebed5 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_LR1121 generic.yaml @@ -0,0 +1,46 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: + - DIO5 + - DIO6 + MODE_STBY: + - LOW + - LOW + MODE_RX: + - HIGH + - LOW + MODE_TX: + - HIGH + - HIGH + MODE_TX_HP: + - LOW + - HIGH + MODE_TX_HF: + - LOW + - LOW + MODE_GNSS: + - LOW + - LOW + MODE_WIFI: + - LOW + - LOW + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml b/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml new file mode 100644 index 000000000..c2ab76d46 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml @@ -0,0 +1,30 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: [DIO5, DIO6, DIO7] + MODE_STBY: [LOW, LOW, LOW] + MODE_RX: [LOW, LOW, LOW] + MODE_TX: [LOW, HIGH, LOW] + MODE_TX_HP: [HIGH, LOW, LOW] + # MODE_TX_HF: [] + # MODE_GNSS: [] + MODE_WIFI: [LOW, LOW, LOW] + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/lora-Adafruit-RFM9x.yaml b/bin/config.d/lora-Adafruit-RFM9x.yaml index 20295dc72..1258af4f5 100644 --- a/bin/config.d/lora-Adafruit-RFM9x.yaml +++ b/bin/config.d/lora-Adafruit-RFM9x.yaml @@ -1,3 +1,9 @@ +Meta: + name: Adafruit RFM9x + support: deprecated + compatible: + - raspberry-pi + Lora: Module: RF95 # Adafruit RFM9x Reset: 25 diff --git a/bin/config.d/lora-MeshAdv-900M30S.yaml b/bin/config.d/lora-MeshAdv-900M30S.yaml index 5c148bf68..c90391cb0 100644 --- a/bin/config.d/lora-MeshAdv-900M30S.yaml +++ b/bin/config.d/lora-MeshAdv-900M30S.yaml @@ -1,5 +1,11 @@ # MeshAdv-Pi E22-900M30S # https://github.com/chrismyers2000/MeshAdv-Pi-Hat +Meta: + name: MeshAdv-Pi E22-900M30S + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 CS: 21 diff --git a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml index b47b5c996..d878bce1b 100644 --- a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml +++ b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml @@ -1,5 +1,11 @@ # MeshAdv Mini E22-900M22S # https://github.com/chrismyers2000/MeshAdv-Mini +Meta: + name: MeshAdv Mini E22-900M22S + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Ebyte E22-900M22S CS: 8 diff --git a/bin/config.d/lora-RAK6421-13300-slot1.yaml b/bin/config.d/lora-RAK6421-13300-slot1.yaml index 6f65f9ccd..a88544896 100644 --- a/bin/config.d/lora-RAK6421-13300-slot1.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot1.yaml @@ -1,11 +1,20 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13300 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 Busy: 24 # IO5 # Ant_sw: 13 # IO3 + Enable_Pins: + - 12 + - 13 DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 diff --git a/bin/config.d/lora-RAK6421-13300-slot2.yaml b/bin/config.d/lora-RAK6421-13300-slot2.yaml index cbc794d39..40b0cea09 100644 --- a/bin/config.d/lora-RAK6421-13300-slot2.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot2.yaml @@ -1,8 +1,17 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 2 pins + ### RAK13300 in Slot 2 pins IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 # Ant_sw: 23 # IO3 + Enable_Pins: + - 26 + - 23 spidev: spidev0.1 # CS: 7 \ No newline at end of file diff --git a/bin/config.d/lora-RAK6421-13302-slot1.yaml b/bin/config.d/lora-RAK6421-13302-slot1.yaml new file mode 100644 index 000000000..85b934ce6 --- /dev/null +++ b/bin/config.d/lora-RAK6421-13302-slot1.yaml @@ -0,0 +1,22 @@ +Meta: + name: RAK6421 + RAK13302 Slot 1 + support: official + compatible: + - raspberry-pi + +Lora: + + ### RAK13302 in Slot 1 + Module: sx1262 + IRQ: 22 #IO6 + Reset: 16 # IO4 + Busy: 24 # IO5 + # Ant_sw: 13 # IO3 + Enable_Pins: + - 12 + - 13 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: 8 + 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] \ No newline at end of file diff --git a/bin/config.d/lora-RAK6421-13302-slot2.yaml b/bin/config.d/lora-RAK6421-13302-slot2.yaml new file mode 100644 index 000000000..5aa23911f --- /dev/null +++ b/bin/config.d/lora-RAK6421-13302-slot2.yaml @@ -0,0 +1,18 @@ +Meta: + name: RAK6421 + RAK13302 Slot 2 + support: official + compatible: + - raspberry-pi + +Lora: + ### RAK13302 in Slot 2 pins + IRQ: 18 #IO6 + Reset: 24 # IO4 + Busy: 19 # IO5 + # Ant_sw: 23 # IO3 + Enable_Pins: + - 26 + - 23 + spidev: spidev0.1 + # CS: 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] \ No newline at end of file diff --git a/bin/config.d/lora-ecb41-pge-MeshAdv-900M30S.yaml b/bin/config.d/lora-ecb41-pge-MeshAdv-900M30S.yaml new file mode 100644 index 000000000..25fd5e359 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-MeshAdv-900M30S.yaml @@ -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 diff --git a/bin/config.d/lora-ecb41-pge-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-ecb41-pge-MeshAdv-Mini-900M22S.yaml new file mode 100644 index 000000000..e1f385312 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-MeshAdv-Mini-900M22S.yaml @@ -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 diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot1.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot1.yaml new file mode 100644 index 000000000..8782876ac --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot1.yaml @@ -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 diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml new file mode 100644 index 000000000..e0ef946d9 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml @@ -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 diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot1.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot1.yaml new file mode 100644 index 000000000..374be8e35 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot1.yaml @@ -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] diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml new file mode 100644 index 000000000..5548bc5c7 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml @@ -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] diff --git a/bin/config.d/lora-ecb41-pge-waveshare-sxxx.yaml b/bin/config.d/lora-ecb41-pge-waveshare-sxxx.yaml new file mode 100644 index 000000000..1253d3e31 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-waveshare-sxxx.yaml @@ -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 diff --git a/bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml b/bin/config.d/lora-femtofox_LR1121_TCXO.yaml similarity index 75% rename from bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml rename to bin/config.d/lora-femtofox_LR1121_TCXO.yaml index 7aa860f61..10166fa35 100644 --- a/bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml +++ b/bin/config.d/lora-femtofox_LR1121_TCXO.yaml @@ -1,20 +1,22 @@ ---- -Lora: -## Ebyte E80-900M22S -## This is a bit experimental -## -## - Module: lr1121 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO3_TCXO_VOLTAGE: 1.8 - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - - - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox Ebyte E80-900M22S with TCXO + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml b/bin/config.d/lora-femtofox_SX1262_TCXO.yaml similarity index 87% rename from bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml rename to bin/config.d/lora-femtofox_SX1262_TCXO.yaml index a4dec870a..31012c0f6 100644 --- a/bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml +++ b/bin/config.d/lora-femtofox_SX1262_TCXO.yaml @@ -1,21 +1,24 @@ ---- -Lora: -## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup -## HT-RA62 (Has internal switching, but whatever) -## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) -## Will work with any module with or without RF switching, and with TCXO - Module: sx1262 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO2_AS_RF_SWITCH: true - DIO3_TCXO_VOLTAGE: true - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? -# TXen: bridge to DIO2 on E22 module - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox SX1262 TCXO + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup +## HT-RA62 (Has internal switching, but whatever) +## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) +## Will work with any module with or without RF switching, and with TCXO + Module: sx1262 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? +# TXen: bridge to DIO2 on E22 module + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml b/bin/config.d/lora-femtofox_SX1262_XTAL.yaml similarity index 86% rename from bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml rename to bin/config.d/lora-femtofox_SX1262_XTAL.yaml index 6b956f3e3..7132f382e 100644 --- a/bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml +++ b/bin/config.d/lora-femtofox_SX1262_XTAL.yaml @@ -1,21 +1,24 @@ ---- -Lora: -## Ebyte E22-900MM22S with no external RF switching setup -## Waveshare SX126X XXXM, AI Thinker RA-01SH -## Will work with any module with or without RF switching and no TCXO - - Module: sx1262 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO2_AS_RF_SWITCH: true - DIO3_TCXO_VOLTAGE: false - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? -# TXen: bridge to DIO2 on E22 module - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox SX1262 XTAL + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E22-900MM22S with no external RF switching setup +## Waveshare SX126X XXXM, AI Thinker RA-01SH +## Will work with any module with or without RF switching and no TCXO + + Module: sx1262 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: false + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? +# TXen: bridge to DIO2 on E22 module + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml index 066e36a10..b0ac0306a 100644 --- a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml +++ b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml @@ -1,11 +1,21 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 (Autoconf default) + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13300 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 Busy: 24 # IO5 - # Ant_sw: 13 # IO3 + Enable_Pins: + - 12 + - 13 DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 + # GPIO_DETECT_PA: 13 + 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] \ No newline at end of file diff --git a/bin/config.d/lora-luckfox-pico-max-ws-raspberry-pi-pico-hat.yaml b/bin/config.d/lora-luckfox-pico-max-ws-raspberry-pi-pico-hat.yaml new file mode 100644 index 000000000..e0cc6197b --- /dev/null +++ b/bin/config.d/lora-luckfox-pico-max-ws-raspberry-pi-pico-hat.yaml @@ -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 diff --git a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml index 2fd128ce8..f944a7949 100644 --- a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml +++ b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: Luckfox Lyra PicoCalc Wio LoRa SX1262 + support: official + compatible: + - luckfox-lyra-plus # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ultra_1w.yaml b/bin/config.d/lora-lyra-ultra_1w.yaml new file mode 100644 index 000000000..71d05f84e --- /dev/null +++ b/bin/config.d/lora-lyra-ultra_1w.yaml @@ -0,0 +1,23 @@ +# For use with Armbian luckfox-lyra-ultra-w +# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat +# 1 Watt Lyra Ultra hat +Meta: + name: wehooper4 Luckfox Ultra 1W + support: community + compatible: + - luckfox-pico-ultra # Armbian + - luckfox-lyra-ultra # Armbian + +Lora: + Module: sx1262 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + CS: 10 + IRQ: 5 + Busy: 11 + Reset: 9 + RXen: 14 + + spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7) + spiSpeed: 2000000 diff --git a/bin/config.d/lora-lyra-ultra_2w.yaml b/bin/config.d/lora-lyra-ultra_2w.yaml new file mode 100644 index 000000000..e3bb18c7b --- /dev/null +++ b/bin/config.d/lora-lyra-ultra_2w.yaml @@ -0,0 +1,24 @@ +# For use with Armbian luckfox-lyra-ultra-w +# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat +# 2 Watt Lyra Ultra hat +Meta: + name: wehooper4 Luckfox Ultra 2W + support: community + compatible: + - luckfox-pico-ultra # Armbian + - luckfox-lyra-ultra # Armbian + +Lora: + Module: sx1262 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + SX126X_MAX_POWER: 8 + CS: 10 + IRQ: 5 + Busy: 11 + Reset: 9 + RXen: 14 + + spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7) + spiSpeed: 2000000 diff --git a/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml new file mode 100644 index 000000000..bdd98a41c --- /dev/null +++ b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml @@ -0,0 +1,31 @@ +# For use with Armbian luckfox-lyra // luckfox-lyra-plus +# Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config +# Waveshare LoRa HAT for Raspberry Pi Pico +# https://www.waveshare.com/wiki/Pico-LoRa-SX1262 +Meta: + name: Waveshare LoRa HAT for Raspberry Pi Pico + support: community + compatible: + - luckfox-lyra-plus # Armbian + +Lora: + Module: sx1262 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + spidev: spidev0.0 + CS: # GPIO0_B5 + pin: 13 + gpiochip: 0 + line: 13 + IRQ: # GPIO1_C2 + pin: 50 + gpiochip: 1 + line: 18 + Busy: # GPIO0_B4 + pin: 12 + gpiochip: 0 + line: 12 + Reset: # GPIO0_A2 + pin: 2 + gpiochip: 0 + line: 2 diff --git a/bin/config.d/lora-lyra-zero-MeshAdv-900M30S.yaml b/bin/config.d/lora-lyra-zero-MeshAdv-900M30S.yaml new file mode 100644 index 000000000..ea22c7953 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-MeshAdv-900M30S.yaml @@ -0,0 +1,39 @@ +# MeshAdv-Pi E22-900M30S +# https://github.com/chrismyers2000/MeshAdv-Pi-Hat +Meta: + name: MeshAdv-Pi E22-900M30S + support: community + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + CS: # GPIO0_C2 (physical 40) + pin: 18 + gpiochip: 0 + line: 18 + IRQ: # GPIO1_D1 (physical 36) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_C1 (physical 38) + pin: 17 + gpiochip: 0 + line: 17 + Reset: # GPIO0_B6 (physical 12) + pin: 14 + gpiochip: 0 + line: 14 + TXen: # GPIO1_C2 (physical 33) + pin: 50 + gpiochip: 1 + line: 18 + RXen: # GPIO1_D2 (physical 32) + pin: 58 + gpiochip: 1 + line: 26 + DIO3_TCXO_VOLTAGE: true + # Only for E22-900M33S: + # Limit the output power to 8 dBm + # SX126X_MAX_POWER: 8 + spidev: spidev0.0 diff --git a/bin/config.d/lora-lyra-zero-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-lyra-zero-MeshAdv-Mini-900M22S.yaml new file mode 100644 index 000000000..1fb150b15 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-MeshAdv-Mini-900M22S.yaml @@ -0,0 +1,33 @@ +# MeshAdv Mini E22-900M22S +# https://github.com/chrismyers2000/MeshAdv-Mini +Meta: + name: MeshAdv Mini E22-900M22S + support: community + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 # Ebyte E22-900M22S + CS: # GPIO0_B2_d (phys 24, RPi CE0) + pin: 10 + gpiochip: 0 + line: 10 + IRQ: # GPIO1_D1_d (phys 36, RPi GPIO16) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_C1_d (phys 38, RPi GPIO20) + pin: 17 + gpiochip: 0 + line: 17 + Reset: # GPIO0_B4_d (phys 18, RPi GPIO24) + pin: 12 + gpiochip: 0 + line: 12 + RXen: # GPIO1_D2_d (phys 32, RPi GPIO12) + pin: 58 + gpiochip: 1 + line: 26 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + spidev: spidev0.0 diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13300-slot1.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot1.yaml new file mode 100644 index 000000000..b65a30c20 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot1.yaml @@ -0,0 +1,38 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: community # Promote when tested + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_A5 (IO6) + pin: 5 + gpiochip: 0 + line: 5 + Reset: # GPIO1_D1 (IO4) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_B4 (IO5) + pin: 12 + gpiochip: 0 + line: 12 + # Ant_sw: # GPIO1_C2 (IO3) + # pin: 50 + # gpiochip: 1 + # line: 18 + Enable_Pins: + - pin: 58 # GPIO1_D2 + gpiochip: 1 + line: 26 + - pin: 50 # GPIO1_C2 + gpiochip: 1 + line: 18 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_B2 + # pin: 10 + # gpiochip: 0 + # line: 10 diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml new file mode 100644 index 000000000..255a3eca3 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml @@ -0,0 +1,36 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: community # Promote when tested + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_B6 (IO6) + pin: 14 + gpiochip: 0 + line: 14 + Reset: # GPIO0_B4 (IO4) + pin: 12 + gpiochip: 0 + line: 12 + Busy: # GPIO1_C0 (IO5) + pin: 48 + gpiochip: 1 + line: 16 + # Ant_sw: # GPIO0_B5 (IO3) + # pin: 13 + # gpiochip: 0 + # line: 13 + Enable_Pins: + - pin: 51 # GPIO1_C3 + gpiochip: 1 + line: 19 + - pin: 13 # GPIO0_B5 + gpiochip: 0 + line: 13 + spidev: spidev0.1 + # CS: # GPIO0_B1 + # pin: 9 + # gpiochip: 0 + # line: 9 diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13302-slot1.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot1.yaml new file mode 100644 index 000000000..c37c6fb3a --- /dev/null +++ b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot1.yaml @@ -0,0 +1,39 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: community # Promote when tested + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_A5 (IO6) + pin: 5 + gpiochip: 0 + line: 5 + Reset: # GPIO1_D1 (IO4) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_B4 (IO5) + pin: 12 + gpiochip: 0 + line: 12 + # Ant_sw: # GPIO1_C2 (IO3) + # pin: 50 + # gpiochip: 1 + # line: 18 + Enable_Pins: + - pin: 58 # GPIO1_D2 + gpiochip: 1 + line: 26 + - pin: 50 # GPIO1_C2 + gpiochip: 1 + line: 18 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_B2 + # pin: 10 + # gpiochip: 0 + # line: 10 + 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] diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml new file mode 100644 index 000000000..773a35ab0 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml @@ -0,0 +1,37 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: community # Promote when tested + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_B6 (IO6) + pin: 14 + gpiochip: 0 + line: 14 + Reset: # GPIO0_B4 (IO4) + pin: 12 + gpiochip: 0 + line: 12 + Busy: # GPIO1_C0 (IO5) + pin: 48 + gpiochip: 1 + line: 16 + # Ant_sw: # GPIO0_B5 (IO3) + # pin: 13 + # gpiochip: 0 + # line: 13 + Enable_Pins: + - pin: 51 # GPIO1_C3 + gpiochip: 1 + line: 19 + - pin: 13 # GPIO0_B5 + gpiochip: 0 + line: 13 + spidev: spidev0.1 + # CS: # GPIO0_B1 + # pin: 9 + # gpiochip: 0 + # line: 9 + 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] diff --git a/bin/config.d/lora-lyra-zero-waveshare-sxxx.yaml b/bin/config.d/lora-lyra-zero-waveshare-sxxx.yaml new file mode 100644 index 000000000..934945202 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-waveshare-sxxx.yaml @@ -0,0 +1,30 @@ +Meta: + name: Waveshare SX1262 + support: deprecated + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 # Waveshare SX126X XXXM + DIO2_AS_RF_SWITCH: true + CS: # GPIO0_C2 (physical 40) + pin: 18 + gpiochip: 0 + line: 18 + IRQ: # GPIO1_D1 (physical 36) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_C1 (physical 38) + pin: 17 + gpiochip: 0 + line: 17 + Reset: # GPIO0_B6 (physical 12) + pin: 14 + gpiochip: 0 + line: 14 + SX126X_ANT_SW: # GPIO1_B3 (physical 31) + pin: 43 + gpiochip: 1 + line: 11 + spidev: spidev0.0 diff --git a/bin/config.d/lora-meshstick-1262.yaml b/bin/config.d/lora-meshstick-1262.yaml index 3f8d6c617..355d0bc0f 100644 --- a/bin/config.d/lora-meshstick-1262.yaml +++ b/bin/config.d/lora-meshstick-1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: Lora Meshstick SX1262 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-ok3506-MeshAdv-900M30S.yaml b/bin/config.d/lora-ok3506-MeshAdv-900M30S.yaml new file mode 100644 index 000000000..c39a07ccd --- /dev/null +++ b/bin/config.d/lora-ok3506-MeshAdv-900M30S.yaml @@ -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 diff --git a/bin/config.d/lora-ok3506-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-ok3506-MeshAdv-Mini-900M22S.yaml new file mode 100644 index 000000000..8f2c4a417 --- /dev/null +++ b/bin/config.d/lora-ok3506-MeshAdv-Mini-900M22S.yaml @@ -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 diff --git a/bin/config.d/lora-ok3506-RAK6421-13300-slot1.yaml b/bin/config.d/lora-ok3506-RAK6421-13300-slot1.yaml new file mode 100644 index 000000000..d97bdd327 --- /dev/null +++ b/bin/config.d/lora-ok3506-RAK6421-13300-slot1.yaml @@ -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 diff --git a/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml b/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml new file mode 100644 index 000000000..969c20ad3 --- /dev/null +++ b/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml @@ -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 diff --git a/bin/config.d/lora-ok3506-RAK6421-13302-slot1.yaml b/bin/config.d/lora-ok3506-RAK6421-13302-slot1.yaml new file mode 100644 index 000000000..d73eaf7a0 --- /dev/null +++ b/bin/config.d/lora-ok3506-RAK6421-13302-slot1.yaml @@ -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] diff --git a/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml b/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml new file mode 100644 index 000000000..36b70658b --- /dev/null +++ b/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml @@ -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] diff --git a/bin/config.d/lora-ok3506-waveshare-sxxx.yaml b/bin/config.d/lora-ok3506-waveshare-sxxx.yaml new file mode 100644 index 000000000..1f5795f92 --- /dev/null +++ b/bin/config.d/lora-ok3506-waveshare-sxxx.yaml @@ -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 diff --git a/bin/config.d/lora-piggystick-lr1121.yaml b/bin/config.d/lora-piggystick-lr1121.yaml index 348db61b1..e11c78dd3 100644 --- a/bin/config.d/lora-piggystick-lr1121.yaml +++ b/bin/config.d/lora-piggystick-lr1121.yaml @@ -1,3 +1,9 @@ +Meta: + name: Lora Meshstick SX1262 + support: community + compatible: + - usb + Lora: Module: lr1121 CS: 0 diff --git a/bin/config.d/lora-pinedio-usb-sx1262.yaml b/bin/config.d/lora-pinedio-usb-sx1262.yaml index 6b8a9fc95..b2351c05a 100644 --- a/bin/config.d/lora-pinedio-usb-sx1262.yaml +++ b/bin/config.d/lora-pinedio-usb-sx1262.yaml @@ -1,5 +1,11 @@ +Meta: + name: Pinedio USB SX1262 + support: deprecated + compatible: + - usb + Lora: Module: sx1262 CS: 0 IRQ: 10 - spidev: ch341 \ No newline at end of file + spidev: ch341 diff --git a/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml b/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml index ea86a3728..7337f39ca 100644 --- a/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml +++ b/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml @@ -1,3 +1,9 @@ +Meta: + name: raxda-rock2f-starter-edition-hat + support: community + compatible: + - rock-2f # Armbian + Lora: ### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx diff --git a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml index d9b64c7da..185417cce 100644 --- a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml +++ b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml @@ -1,5 +1,11 @@ # https://www.waveshare.com/core1262-868m.htm # https://github.com/markbirss/lora-starter-edition-sx1262-i2c +Meta: + name: lora-starter-edition-sx1262-i2c + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-usb-meshstick-1262.yaml b/bin/config.d/lora-usb-meshstick-1262.yaml index a539d76a1..79ca132df 100644 --- a/bin/config.d/lora-usb-meshstick-1262.yaml +++ b/bin/config.d/lora-usb-meshstick-1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: meshstick-1262 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-meshtoad-e22.yaml b/bin/config.d/lora-usb-meshtoad-e22.yaml index b6cb61c6b..49182c83e 100644 --- a/bin/config.d/lora-usb-meshtoad-e22.yaml +++ b/bin/config.d/lora-usb-meshtoad-e22.yaml @@ -1,3 +1,9 @@ +Meta: + name: meshtoad-e22 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml index 7726eccd1..9f30217e0 100644 --- a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml @@ -1,3 +1,9 @@ +Meta: + name: umesh-1262-30dbm + support: community + compatible: + - clockwork-uconsole + Lora: Module: sx1262 CS: 0 @@ -13,8 +19,7 @@ Lora: # USB_Serialnum: 12345678 SX126X_MAX_POWER: 22 # Reduce output power to improve EMI - NUM_PA_POINTS: 22 - TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7 + TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7] # Note: This module integrates an additional PA to achieve higher output power. # The 'power' parameter here does not represent the actual RF output. # TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). diff --git a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml index c054a92f9..45c8e21d0 100644 --- a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml @@ -1,3 +1,9 @@ +Meta: + name: umesh-1268-30dbm + support: community + compatible: + - clockwork-uconsole + Lora: Module: sx1268 CS: 0 @@ -13,8 +19,7 @@ Lora: # USB_Serialnum: 12345678 SX126X_MAX_POWER: 22 # Reduce output power to improve EMI - NUM_PA_POINTS: 22 - TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7 + TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7] # Note: This module integrates an additional PA to achieve higher output power. # The 'power' parameter here does not represent the actual RF output. # TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). diff --git a/bin/config.d/lora-waveshare-sxxx.yaml b/bin/config.d/lora-waveshare-sxxx.yaml index a9ff13653..641cf1e49 100644 --- a/bin/config.d/lora-waveshare-sxxx.yaml +++ b/bin/config.d/lora-waveshare-sxxx.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare SX1262 + support: deprecated + compatible: + - raspberry-pi + Lora: Module: sx1262 # Waveshare SX126X XXXM DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml index 1e1c325e7..b84e18d5b 100644 --- a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml +++ b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml @@ -1,5 +1,12 @@ # https://www.waveshare.com/pico-lora-sx1262-868m.htm # https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter + +Meta: + name: ws-raspberry-pico-to-rpi-adapter + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml index 37d7e27d2..5743a9ed6 100644 --- a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml +++ b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml @@ -15,6 +15,12 @@ # 5 CS 24 # 26 DIO1/IRQ 26 +Meta: + name: ws-raspberry-pico-to-orangepi-03 + support: community + compatible: + - orange-pi-zero-3 # Armbian + Lora: Module: sx1262 # Waveshare Raspberry Pico Lora module DIO2_AS_RF_SWITCH: true diff --git a/bin/device-install.bat b/bin/device-install.bat index c200a3201..69469d581 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -156,16 +156,8 @@ IF %BPS_RESET% EQU 1 ( SET "PROGNAME=!FILENAME:.factory.bin=!" CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!" -IF "__!MCU!__" == "__esp32s3__" ( - @REM We are working with ESP32-S3 - SET "OTA_FILENAME=bleota-s3.bin" -) ELSE IF "__!MCU!__" == "__esp32c3__" ( - @REM We are working with ESP32-C3 - SET "OTA_FILENAME=bleota-c3.bin" -) ELSE ( - @REM Everything else - SET "OTA_FILENAME=bleota.bin" -) +@REM Determine OTA filename based on MCU type (unified OTA format) +SET "OTA_FILENAME=mt-!MCU!-ota.bin" CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!" @REM Set SPIFFS filename with "littlefs-" prefix. diff --git a/bin/device-install.sh b/bin/device-install.sh index 49427524e..52a848c38 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -131,14 +131,8 @@ if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then exit 1 fi - # Determine OTA filename based on MCU type - if [ "$MCU" == "esp32s3" ]; then - OTAFILE=bleota-s3.bin - elif [ "$MCU" == "esp32c3" ]; then - OTAFILE=bleota-c3.bin - else - OTAFILE=bleota.bin - fi + # Determine OTA filename based on MCU type (unified OTA format) + OTAFILE="mt-${MCU}-ota.bin" # Set SPIFFS filename with "littlefs-" prefix. SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin" diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py index b4c18c05b..1458e4390 100755 --- a/bin/generate_ci_matrix.py +++ b/bin/generate_ci_matrix.py @@ -43,7 +43,7 @@ for pio_env in pio_envs: env = { "ci": {"board": pio_env, "platform": env_platform}, "board_level": cfg.get(f"env:{pio_env}", "board_level", default=None), - "board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)), + "board_check": cfg.get(f"env:{pio_env}", "board_check", default="false").strip().lower() == "true", } all_envs.append(env) diff --git a/bin/generate_release_notes.py b/bin/generate_release_notes.py index d0f1147da..533ff6909 100755 --- a/bin/generate_release_notes.py +++ b/bin/generate_release_notes.py @@ -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 ", 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 = [] diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 6ad8962d1..a1690186b 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,18 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.23 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.22 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.21 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19 diff --git a/bin/test-native-docker.sh b/bin/test-native-docker.sh new file mode 100755 index 000000000..b42c940c5 --- /dev/null +++ b/bin/test-native-docker.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Run native PlatformIO tests inside Docker (for macOS / non-Linux hosts). +# +# Usage: +# ./bin/test-native-docker.sh # run all native tests +# ./bin/test-native-docker.sh -f test_transmit_history # run specific test filter +# ./bin/test-native-docker.sh --rebuild # force rebuild the image +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +IMAGE_NAME="meshtastic-native-test" + +REBUILD=false +EXTRA_ARGS=() + +for arg in "$@"; do + if [[ "$arg" == "--rebuild" ]]; then + REBUILD=true + else + EXTRA_ARGS+=("$arg") + fi +done + +if $REBUILD || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then + echo "Building test image (first run may take a few minutes)..." + docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/Dockerfile.test" "$ROOT_DIR" +fi + +# Disable BUILD_EPOCH to avoid full rebuilds between test runs (matches CI) +sed_cmd='s/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' + +# Default: run all tests. Pass extra args (e.g. -f test_transmit_history) through. +if [[ ${#EXTRA_ARGS[@]} -eq 0 ]]; then + CMD=("platformio" "test" "-e" "coverage" "-v") +else + CMD=("platformio" "test" "-e" "coverage" "-v" "${EXTRA_ARGS[@]}") +fi + +exec docker run --rm \ + -v "$ROOT_DIR:/src:ro" \ + "$IMAGE_NAME" \ + bash -c "rm -rf /tmp/fw-test && cp -a /src /tmp/fw-test && cd /tmp/fw-test && sed -i '${sed_cmd}' platformio.ini && ${CMD[*]}" diff --git a/boards/heltec_mesh_node_t096.json b/boards/heltec_mesh_node_t096.json new file mode 100644 index 000000000..1e417c5b4 --- /dev/null +++ b/boards/heltec_mesh_node_t096.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x2886", "0x1667"] + ], + "usb_product": "HT-n5262G", + "mcu": "nrf52840", + "variant": "heltec_mesh_node_t096", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Heltec nrf (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/boards/me25ls01-4y10td.json b/boards/me25ls01-4y10td.json index 9e1d63265..e0be46d67 100644 --- a/boards/me25ls01-4y10td.json +++ b/boards/me25ls01-4y10td.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Minesemi ME25LS01", diff --git a/boards/meshlink.json b/boards/meshlink.json index a608de88a..51190502e 100644 --- a/boards/meshlink.json +++ b/boards/meshlink.json @@ -33,7 +33,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "MeshLink", diff --git a/boards/mini-epaper-s3.json b/boards/mini-epaper-s3.json new file mode 100644 index 000000000..5140f88be --- /dev/null +++ b/boards/mini-epaper-s3.json @@ -0,0 +1,40 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_ESP32S3_DEV", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo Mini-Epaper-S3 (4 MB Flash, 2MB PSRAM)", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.lilygo.cc", + "vendor": "LilyGo" +} diff --git a/boards/minimesh_lite.json b/boards/minimesh_lite.json index 0b8f0b909..c94985531 100644 --- a/boards/minimesh_lite.json +++ b/boards/minimesh_lite.json @@ -31,7 +31,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Minimesh Lite", diff --git a/boards/ms24sf1.json b/boards/ms24sf1.json index 8356e3012..3f65a39c6 100644 --- a/boards/ms24sf1.json +++ b/boards/ms24sf1.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "MINEWSEMI_MS24SF1_SX1262", diff --git a/boards/nano-g2-ultra.json b/boards/nano-g2-ultra.json index 7afce178b..1de7c0186 100644 --- a/boards/nano-g2-ultra.json +++ b/boards/nano-g2-ultra.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "BQ nRF52840", diff --git a/boards/promicro-nrf52840.json b/boards/promicro-nrf52840.json index 99ae3f01e..e7539e0cf 100644 --- a/boards/promicro-nrf52840.json +++ b/boards/promicro-nrf52840.json @@ -33,7 +33,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "ProMicro compatible nRF52840", diff --git a/boards/station-g2.json b/boards/station-g2.json index 871f067aa..f7ce50779 100755 --- a/boards/station-g2.json +++ b/boards/station-g2.json @@ -8,7 +8,7 @@ "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=0" ], diff --git a/boards/t5-epaper-s3.json b/boards/t5-epaper-s3.json new file mode 100644 index 000000000..16106198e --- /dev/null +++ b/boards/t5-epaper-s3.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo T5-ePaper-S3", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://lilygo.cc/products/t5-e-paper-s3-pro", + "vendor": "LILYGO" +} diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json index 9e8870041..15bae896f 100644 --- a/boards/tracker-t1000-e.json +++ b/boards/tracker-t1000-e.json @@ -33,7 +33,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Seeed T1000-E", diff --git a/boards/ttgo-tbeam.json b/boards/ttgo-tbeam.json new file mode 100644 index 000000000..a4c43d525 --- /dev/null +++ b/boards/ttgo-tbeam.json @@ -0,0 +1,29 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_T_Beam", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "tbeam" + }, + "connectivity": ["wifi", "bluetooth", "can", "ethernet"], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "TTGO T-Beam (PSRAM Disabled)", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 1310720, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://github.com/LilyGO/TTGO-T-Beam", + "vendor": "TTGO" +} diff --git a/boards/wio-sdk-wm1110.json b/boards/wio-sdk-wm1110.json index f45b030d1..b0dc5326a 100644 --- a/boards/wio-sdk-wm1110.json +++ b/boards/wio-sdk-wm1110.json @@ -25,7 +25,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino", "freertos"], "name": "Seeed WIO WM1110", diff --git a/boards/wio-t1000-s.json b/boards/wio-t1000-s.json index 654a8f73d..3b61f3683 100644 --- a/boards/wio-t1000-s.json +++ b/boards/wio-t1000-s.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Seeed WIO WM1110", diff --git a/boards/wio-tracker-wm1110.json b/boards/wio-tracker-wm1110.json index 37a9186ab..3137f68e7 100644 --- a/boards/wio-tracker-wm1110.json +++ b/boards/wio-tracker-wm1110.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Seeed WIO WM1110", diff --git a/branding/README.md b/branding/README.md index 3a558bf20..b59936140 100644 --- a/branding/README.md +++ b/branding/README.md @@ -12,6 +12,6 @@ Ex: - `logo_320x480.png` - `logo_320x240.png` -This file is copied to `data/boot/logo.png` before filesytem image compilation. +This file is copied to `data/boot/logo.png` before filesystem image compilation. For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33). diff --git a/debian/changelog b/debian/changelog index 38489b074..c3f1424a5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,27 @@ +meshtasticd (2.7.23.0) unstable; urgency=medium + + * Version 2.7.23 + + -- GitHub Actions Tue, 14 Apr 2026 12:29:48 +0000 + +meshtasticd (2.7.22.0) unstable; urgency=medium + + * Version 2.7.22 + + -- GitHub Actions Mon, 06 Apr 2026 11:34:12 +0000 + +meshtasticd (2.7.21.0) unstable; urgency=medium + + * Version 2.7.21 + + -- GitHub Actions Wed, 11 Mar 2026 11:45:36 +0000 + +meshtasticd (2.7.20.0) unstable; urgency=medium + + * Version 2.7.20 + + -- GitHub Actions Wed, 11 Feb 2026 12:19:54 +0000 + meshtasticd (2.7.19.0) unstable; urgency=medium * Version 2.7.19 diff --git a/debian/ci_pack_sdeb.sh b/debian/ci_pack_sdeb.sh index 81e681e0c..7b2418ff6 100755 --- a/debian/ci_pack_sdeb.sh +++ b/debian/ci_pack_sdeb.sh @@ -3,10 +3,17 @@ export DEBEMAIL="jbennett@incomsystems.biz" export PLATFORMIO_LIBDEPS_DIR=pio/libdeps export PLATFORMIO_PACKAGES_DIR=pio/packages export PLATFORMIO_CORE_DIR=pio/core +export PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 +export PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 +export PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240 # Download libraries to `pio` platformio pkg install -e native-tft platformio pkg install -e native-tft -t platformio/tool-scons@4.40502.0 +# Mangle PlatformIO cache to prevent internet access at build-time +# Simply adds 1 to all expiry (epoch) timestamps, adding ~500 years to expiry date +cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak +jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak >pio/core/.cache/downloads/usage.db # Compress `pio` directory to prevent dh_clean from sanitizing it tar -cf pio.tar pio/ rm -rf pio @@ -20,5 +27,10 @@ rm -rf debian/changelog dch --create --distribution "$SERIES" --package "$package" --newversion "$PKG_VERSION~$SERIES" \ "GitHub Actions Automatic packaging for $PKG_VERSION~$SERIES" -# Build the source deb -debuild -S -nc -k"$GPG_KEY_ID" +if [[ -n $GPG_KEY_ID ]]; then + # Build and sign the source deb + debuild -S -nc -k"$GPG_KEY_ID" +else + # Build the source deb without signing (forks) + debuild -S -nc +fi diff --git a/debian/control b/debian/control index 46c932a80..8e5f17af9 100644 --- a/debian/control +++ b/debian/control @@ -26,7 +26,8 @@ Build-Depends: debhelper-compat (= 13), libx11-dev, libinput-dev, libxkbcommon-x11-dev, - libsqlite3-dev + libsqlite3-dev, + libsdl2-dev Standards-Version: 4.6.2 Homepage: https://github.com/meshtastic/firmware Rules-Requires-Root: no diff --git a/debian/rules b/debian/rules index ebb572153..68af9a9a5 100755 --- a/debian/rules +++ b/debian/rules @@ -9,7 +9,10 @@ PIO_ENV:=\ PLATFORMIO_CORE_DIR=pio/core \ PLATFORMIO_LIBDEPS_DIR=pio/libdeps \ - PLATFORMIO_PACKAGES_DIR=pio/packages + PLATFORMIO_PACKAGES_DIR=pio/packages \ + PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 \ + PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 \ + PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240 # Raspbian armhf builds should be compatible with armv6-hardfloat # https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..e7a4c7ff7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1766314097, + "narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..1af493c6d --- /dev/null +++ b/flake.nix @@ -0,0 +1,66 @@ +{ + description = "Nix flake to compile Meshtastic firmware"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + # Shim to make flake.nix work with stable Nix. + flake-compat = { + url = "github:NixOS/flake-compat"; + flake = false; + }; + }; + + outputs = + inputs: + let + lib = inputs.nixpkgs.lib; + + forAllSystems = + fn: + lib.genAttrs lib.systems.flakeExposed ( + system: + fn { + pkgs = import inputs.nixpkgs { + inherit system; + }; + inherit system; + } + ); + in + { + devShells = forAllSystems ( + { pkgs, ... }: + let + python3 = pkgs.python312.withPackages ( + ps: with ps; [ + google + ] + ); + in + { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + python3 + platformio + ]; + + shellHook = '' + # Set up PlatformIO to use a local core directory. + export PLATFORMIO_CORE_DIR=$PWD/.platformio + # Tell pip to put packages into $PIP_PREFIX instead of the usual + # location. This is especially necessary under NixOS to avoid having + # pip trying to write to the read-only Nix store. For more info, + # see https://wiki.nixos.org/wiki/Python + export PIP_PREFIX=$PWD/.python3 + export PYTHONPATH="$PIP_PREFIX/${python3.sitePackages}" + export PATH="$PIP_PREFIX/bin:$PATH" + # Avoids reproducibility issues with some Python packages + # See https://nixos.org/manual/nixpkgs/stable/#python-setup.py-bdist_wheel-cannot-create-.whl + unset SOURCE_DATE_EPOCH + ''; + }; + } + ); + }; +} diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index fc14ede7f..a9eb552d7 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -49,6 +49,7 @@ BuildRequires: pkgconfig(libulfius) BuildRequires: pkgconfig(x11) BuildRequires: pkgconfig(libinput) BuildRequires: pkgconfig(xkbcommon-x11) +BuildRequires: pkgconfig(sdl2) # libbsd is needed on older Fedora/RHEL to provide 'strlcpy' %if 0%{?fedora} >= 39 || 0%{?rhel} >= 10 @@ -59,8 +60,14 @@ BuildRequires: pkgconfig(libbsd-overlay) Requires: systemd-udev +# Declare that this package provides the user/group it creates in %pre +# Required for Fedora 43+ which tracks users/groups as RPM dependencies +Provides: user(%{meshtasticd_user}) +Provides: group(%{meshtasticd_user}) +Provides: group(spi) + %description -Meshtastic daemon for controlling Meshtastic devices. Meshtastic is an off-grid +Meshtastic daemon. Meshtastic is an off-grid text communication platform that uses inexpensive LoRa radios. %prep @@ -151,6 +158,7 @@ fi %license LICENSE %doc README.md %{_bindir}/meshtasticd +%{_bindir}/meshtasticd-start.sh %dir %{_localstatedir}/lib/meshtasticd %{_udevrulesdir}/99-meshtasticd-udev.rules %dir %{_sysconfdir}/meshtasticd diff --git a/platformio.ini b/platformio.ini index b7c46a6e1..0205d1ad8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,6 +50,7 @@ build_flags = -Wno-missing-field-initializers -DRADIOLIB_EXCLUDE_APRS=1 -DRADIOLIB_EXCLUDE_LORAWAN=1 -DMESHTASTIC_EXCLUDE_DROPZONE=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 @@ -65,7 +66,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master @@ -74,10 +75,10 @@ lib_deps = https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip # renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip - # renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb - nanopb/Nanopb@0.4.91 - # renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32 - erriez/ErriezCRC32@1.0.1 + # renovate: datasource=github-tags depName=Nanopb packageName=nanopb/nanopb + https://github.com/nanopb/nanopb/archive/refs/tags/nanopb-0.4.9.1.zip + # renovate: datasource=github-tags depName=ErriezCRC32 packageName=Erriez/ErriezCRC32 + https://github.com/Erriez/ErriezCRC32/archive/refs/tags/1.0.1.zip ; Used for the code analysis in PIO Home / Inspect check_tool = cppcheck @@ -92,150 +93,153 @@ check_flags = framework = arduino lib_deps = ${env.lib_deps} - # renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL - end2endzone/NonBlockingRTTTL@1.4.0 + # renovate: datasource=github-tags depName=NonBlockingRTTTL packageName=end2endzone/NonBlockingRTTTL + https://github.com/end2endzone/NonBlockingRTTTL/archive/refs/tags/1.4.0.zip +build_unflags = + -std=c++11 + -std=gnu++11 build_flags = ${env.build_flags} -Os + -std=gnu++17 build_src_filter = ${env.build_src_filter} - - ; Common libs for communicating over TCP/IP networks such as MQTT [networking_base] lib_deps = - # renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient - thingsboard/TBPubSubClient@2.12.1 - # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient - arduino-libraries/NTPClient@3.2.1 + # renovate: datasource=github-tags depName=TBPubSubClient packageName=thingsboard/pubsubclient + https://github.com/thingsboard/pubsubclient/archive/refs/tags/v2.12.1.zip + # renovate: datasource=github-tags depName=NTPClient packageName=arduino-libraries/NTPClient + https://github.com/arduino-libraries/NTPClient/archive/refs/tags/3.2.1.zip ; Extra TCP/IP networking libs for supported devices [networking_extra] lib_deps = - # renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog - arcao/Syslog@2.0.0 + # renovate: datasource=github-tags depName=Syslog packageName=arcao/Syslog + https://github.com/arcao/Syslog/archive/refs/tags/v2.0.zip [radiolib_base] lib_deps = - # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib - jgromes/RadioLib@7.5.0 + # renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib + https://github.com/jgromes/RadioLib/archive/refs/tags/7.6.0.zip [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/69739b84f87a91568d3c421498bc89977937a141.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.5 - # 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.6 - # 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.5 - # 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.5 + # 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.2 - # 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 -; (not included in native / portduino) +; Common environmental sensor libraries (not included in native / portduino) +[environmental_extra_common] +lib_deps = + # 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=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 = - # 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 SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library - adafruit/Adafruit SHTC3 Library@1.0.2 - # renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X - adafruit/Adafruit LPS2X@2.0.6 - # renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library - adafruit/Adafruit SHT31 Library@2.2.2 - # 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=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library - adafruit/Adafruit SHT4x Library@1.0.5 - # 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=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 - closedcube/ClosedCube OPT3001@1.1.2 - # 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=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.2 - # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x - sensirion/Sensirion I2C SCD4x@1.1.0 -; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets) + ${environmental_extra_common.lib_deps} + # 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 = - # 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 SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library - adafruit/Adafruit SHTC3 Library@1.0.2 - # renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X - adafruit/Adafruit LPS2X@2.0.6 - # renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library - adafruit/Adafruit SHT31 Library@2.2.2 - # 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=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library - adafruit/Adafruit SHT4x Library@1.0.5 - # 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=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.2 - # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x - sensirion/Sensirion I2C SCD4x@1.1.0 \ No newline at end of file + ${environmental_extra_common.lib_deps} + # renovate: datasource=github-tags depName=Adafruit_BME680 packageName=adafruit/Adafruit_BME680 + https://github.com/adafruit/Adafruit_BME680/archive/refs/tags/2.0.6.zip diff --git a/protobufs b/protobufs index bc63a57f9..e30092e61 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit bc63a57f9e5dba8a7c90ee0bd4a9840862d61f6d +Subproject commit e30092e6168b13341c2b7ec4be19c789ad5cd77f diff --git a/renovate.json b/renovate.json index 187cdc600..60c51b59e 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,8 @@ ":dependencyDashboard", ":semanticCommitTypeAll(chore)", ":ignoreModulesAndTests", + ":noUnscheduledUpdates", + "schedule:daily", "group:recommended", "replacements:all", "workarounds:all" diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..692cd4df8 --- /dev/null +++ b/shell.nix @@ -0,0 +1,12 @@ +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + nodeName = lock.nodes.root.inputs.flake-compat; + in + fetchTarball { + url = + lock.nodes.${nodeName}.locked.url + or "https://github.com/NixOS/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz"; + sha256 = lock.nodes.${nodeName}.locked.narHash; + } +) { src = ./.; }).shellNix diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index 947b1e054..d52b10a53 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -1,19 +1,23 @@ +#ifndef AMBIENTLIGHTINGTHREAD_H +#define AMBIENTLIGHTINGTHREAD_H + #include "Observer.h" #include "configuration.h" +#include "detect/ScanI2C.h" +#include "sleep.h" #ifdef HAS_NCP5623 -#include -NCP5623 rgb; +#include + +#include #endif #ifdef HAS_LP5562 #include -LP5562 rgbw; #endif #ifdef HAS_NEOPIXEL -#include -Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); +#include #endif #ifdef UNPHONE @@ -21,10 +25,24 @@ Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); extern unPhone unphone; #endif -namespace concurrency -{ class AmbientLightingThread : public concurrency::OSThread { + friend class StatusLEDModule; // Let the LEDStatusModule trigger the ambient lighting for notifications and battery status. + friend class ExternalNotificationModule; // Let the ExternalNotificationModule trigger the ambient lighting for notifications. + + private: +#ifdef HAS_NCP5623 + NCP5623 rgb; +#endif + +#ifdef HAS_LP5562 + LP5562 rgbw; +#endif + +#ifdef HAS_NEOPIXEL + Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); +#endif + public: explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting") { @@ -36,14 +54,15 @@ class AmbientLightingThread : public concurrency::OSThread moduleConfig.ambient_lighting.led_state = true; #endif #endif - // Uncomment to test module - // moduleConfig.ambient_lighting.led_state = true; - // moduleConfig.ambient_lighting.current = 10; +#if AMBIENT_LIGHTING_TEST + // define to enable test + moduleConfig.ambient_lighting.led_state = true; + moduleConfig.ambient_lighting.current = 10; // Default to a color based on our node number - // moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; - // moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; - // moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; - + moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; + moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; + moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; +#endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) _type = type; if (_type == ScanI2C::DeviceType::NONE) { @@ -53,11 +72,6 @@ class AmbientLightingThread : public concurrency::OSThread } #endif #ifdef HAS_RGB_LED - if (!moduleConfig.ambient_lighting.led_state) { - LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); - disable(); - return; - } LOG_DEBUG("AmbientLighting init"); #ifdef HAS_NCP5623 if (_type == ScanI2C::NCP5623) { @@ -77,7 +91,13 @@ class AmbientLightingThread : public concurrency::OSThread pixels.clear(); // Set all pixel colors to 'off' pixels.setBrightness(moduleConfig.ambient_lighting.current); #endif - setLighting(); + if (!moduleConfig.ambient_lighting.led_state) { + LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); + disable(); + return; + } + setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) } @@ -91,7 +111,8 @@ class AmbientLightingThread : public concurrency::OSThread #if defined(HAS_NCP5623) || defined(HAS_LP5562) if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { #endif - setLighting(); + setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification #if defined(HAS_NCP5623) || defined(HAS_LP5562) } @@ -148,65 +169,53 @@ class AmbientLightingThread : public concurrency::OSThread return 0; } - void setLighting() + protected: + void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue) { #ifdef HAS_NCP5623 - rgb.setCurrent(moduleConfig.ambient_lighting.current); - rgb.setRed(moduleConfig.ambient_lighting.red); - rgb.setGreen(moduleConfig.ambient_lighting.green); - rgb.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", - moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgb.setCurrent(current); + rgb.setRed(red); + rgb.setGreen(green); + rgb.setBlue(blue); + LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(moduleConfig.ambient_lighting.current); - rgbw.setRed(moduleConfig.ambient_lighting.red); - rgbw.setGreen(moduleConfig.ambient_lighting.green); - rgbw.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgbw.setCurrent(current); + rgbw.setRed(red); + rgbw.setGreen(green); + rgbw.setBlue(blue); + LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue), - 0, NEOPIXEL_COUNT); + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); // RadioMaster Bandit has addressable LED at the two buttons // this allow us to set different lighting for them in variant.h file. -#ifdef RADIOMASTER_900_BANDIT #if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX) pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); #endif #if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX) pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); -#endif #endif pixels.show(); - // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", - // moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - // moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d", + // current, red, green, blue); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, 255 - red); + analogWrite(RGBLED_GREEN, 255 - green); + analogWrite(RGBLED_BLUE, 255 - blue); + LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, red); + analogWrite(RGBLED_GREEN, green); + analogWrite(RGBLED_BLUE, blue); + LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue); #endif #ifdef UNPHONE - unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + unphone.rgb(red, green, blue); + LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue); #endif } }; - -} // namespace concurrency +#endif // AMBIENTLIGHTINGTHREAD_H \ No newline at end of file diff --git a/src/AudioThread.h b/src/AudioThread.h index 23552c421..1129ee087 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -4,6 +4,7 @@ #include "configuration.h" #include "main.h" #include "sleep.h" +#include #ifdef HAS_I2S #include @@ -29,9 +30,9 @@ class AudioThread : public concurrency::OSThread io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif setCPUFast(true); - rtttlFile = new AudioFileSourcePROGMEM(data, len); - i2sRtttl = new AudioGeneratorRTTTL(); - i2sRtttl->begin(rtttlFile, audioOut); + rtttlFile = std::unique_ptr(new AudioFileSourcePROGMEM(data, len)); + i2sRtttl = std::unique_ptr(new AudioGeneratorRTTTL()); + i2sRtttl->begin(rtttlFile.get(), audioOut.get()); } // Also handles actually playing the RTTTL, needs to be called in loop @@ -47,14 +48,10 @@ class AudioThread : public concurrency::OSThread { if (i2sRtttl != nullptr) { i2sRtttl->stop(); - delete i2sRtttl; i2sRtttl = nullptr; } - if (rtttlFile != nullptr) { - delete rtttlFile; - rtttlFile = nullptr; - } + rtttlFile = nullptr; setCPUFast(false); #ifdef T_LORA_PAGER @@ -66,16 +63,14 @@ class AudioThread : public concurrency::OSThread { if (i2sRtttl != nullptr) { i2sRtttl->stop(); - delete i2sRtttl; i2sRtttl = nullptr; } #ifdef T_LORA_PAGER io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif - ESP8266SAM *sam = new ESP8266SAM; - sam->Say(audioOut, text); - delete sam; + auto sam = std::unique_ptr(new ESP8266SAM); + sam->Say(audioOut.get(), text); setCPUFast(false); #ifdef T_LORA_PAGER io.digitalWrite(EXPANDS_AMP_EN, LOW); @@ -96,15 +91,15 @@ class AudioThread : public concurrency::OSThread private: void initOutput() { - audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S); + audioOut = std::unique_ptr(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S)); audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK); audioOut->SetGain(0.2); }; - AudioGeneratorRTTTL *i2sRtttl = nullptr; - AudioOutputI2S *audioOut = nullptr; + std::unique_ptr i2sRtttl = nullptr; + std::unique_ptr audioOut = nullptr; - AudioFileSourcePROGMEM *rtttlFile = nullptr; + std::unique_ptr rtttlFile = nullptr; }; #endif diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index 680aec929..4ea4a95ac 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -89,22 +89,14 @@ class BluetoothStatus : public Status case ConnectionState::CONNECTED: LOG_DEBUG("BluetoothStatus CONNECTED"); #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, LOW); -#else - digitalWrite(BLE_LED, HIGH); -#endif + digitalWrite(BLE_LED, LED_STATE_ON); #endif break; case ConnectionState::DISCONNECTED: LOG_DEBUG("BluetoothStatus DISCONNECTED"); #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif + digitalWrite(BLE_LED, LED_STATE_OFF); #endif break; } diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index 08c7abc04..207feb8c0 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -98,7 +98,6 @@ Syslog &Syslog::logMask(uint8_t priMask) void Syslog::enable() { - this->_client->begin(this->_port); this->_enabled = true; } @@ -166,14 +165,21 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess if ((pri & LOG_FACMASK) == 0) pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri); + // W5100S: acquire UDP socket on-demand to avoid permanent socket consumption + if (!this->_client->begin(this->_port)) { + return false; + } + if (this->_server != NULL) { result = this->_client->beginPacket(this->_server, this->_port); } else { result = this->_client->beginPacket(this->_ip, this->_port); } - if (result != 1) + if (result != 1) { + this->_client->stop(); return false; + } this->_client->print('<'); this->_client->print(pri); @@ -193,6 +199,8 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess this->_client->print(message); this->_client->endPacket(); + this->_client->stop(); // W5100S: release UDP socket for other services + return true; } diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index d88f9fc9f..fdcf840dc 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -4,7 +4,8 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC bool usePreset) { - // If use_preset is false, always return "Custom" + // If use_preset is false, always return "Custom" — callers such as RadioInterface and Channels + // rely on this being a stable literal for channel-name hashing and default-channel detection. if (!usePreset) { return "Custom"; } diff --git a/src/Led.cpp b/src/Led.cpp deleted file mode 100644 index 6406cd2f7..000000000 --- a/src/Led.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "Led.h" -#include "PowerMon.h" -#include "main.h" -#include "power.h" - -GpioVirtPin ledForceOn, ledBlink; - -#if defined(LED_PIN) -// Most boards have a GPIO for LED control -static GpioHwPin ledRawHwPin(LED_PIN); -#else -static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware -#endif - -#if LED_STATE_ON == 0 -static GpioVirtPin ledHwPin; -static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin); -#else -static GpioPin &ledHwPin = ledRawHwPin; -#endif - -#if defined(HAS_PMU) -/** - * A GPIO controlled by the PMU - */ -class GpioPmuPin : public GpioPin -{ - public: - void set(bool value) - { - if (pmu_found && PMU) { - // blink the axp led - PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); - } - } -} ledPmuHwPin; - -// In some cases we need to drive a PMU LED and a normal LED -static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin); -#else -static GpioPin &ledFinalPin = ledHwPin; -#endif - -#ifdef USE_POWERMON -/** - * We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff. - */ -class MonitoredLedPin : public GpioPin -{ - public: - void set(bool value) - { - if (powerMon) { - if (value) - powerMon->setState(meshtastic_PowerMon_State_LED_On); - else - powerMon->clearState(meshtastic_PowerMon_State_LED_On); - } - ledFinalPin.set(value); - } -} monitoredLedPin; -#else -static GpioPin &monitoredLedPin = ledFinalPin; -#endif - -static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or); \ No newline at end of file diff --git a/src/Led.h b/src/Led.h deleted file mode 100644 index 68833e041..000000000 --- a/src/Led.h +++ /dev/null @@ -1,7 +0,0 @@ -#include "GpioLogic.h" -#include "configuration.h" - -/** - * ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main) - */ -extern GpioVirtPin ledForceOn, ledBlink; \ No newline at end of file diff --git a/src/MessageStore.h b/src/MessageStore.h index 6203d8ed0..77271f1c9 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -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 diff --git a/src/Power.cpp b/src/Power.cpp index 11c5fbd02..2e3217a1c 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -35,6 +35,27 @@ #include "nrfx_power.h" #endif +#if defined(ARCH_NRF52) +#include "Nrf52SaadcLock.h" +#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" @@ -323,11 +344,20 @@ 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 for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) { raw += analogRead(BATTERY_PIN); } @@ -459,6 +489,8 @@ class AnalogBatteryLevel : public HasBatteryLevel } // if it's not HIGH - check the battery #endif + // If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it. + return false; // technically speaking this should work for all(?) NRF52 boards // but needs testing across multiple devices. NRF52 USB would not even work if @@ -520,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) @@ -629,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; @@ -639,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 @@ -669,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 @@ -690,7 +729,9 @@ bool Power::setup() bool found = false; if (axpChipInit()) { found = true; - } else if (lipoInit()) { + } else if (cw2015Init()) { + found = true; + } else if (max17048Init()) { found = true; } else if (lipoChargerInit()) { found = true; @@ -700,11 +741,11 @@ bool Power::setup() found = true; } else if (analogInit()) { found = true; - } - + } else { #ifdef NRF_APM - found = true; + found = true; #endif + } #ifdef EXT_PWR_DETECT attachInterrupt( EXT_PWR_DETECT, @@ -816,6 +857,9 @@ void Power::shutdown() #endif #ifdef PIN_LED3 ledOff(PIN_LED3); +#endif +#ifdef LED_NOTIFICATION + ledOff(LED_NOTIFICATION); #endif doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) @@ -839,8 +883,10 @@ void Power::readPowerStatus() if (batteryLevel) { hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse; +#ifndef NRF_APM usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse; isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse; +#endif if (hasBattery) { batteryVoltageMv = batteryLevel->getBattVoltage(); // If the AXP192 returns a valid battery percentage, use it @@ -1358,7 +1404,7 @@ bool Power::axpChipInit() /** * Wrapper class for an I2C MAX17048 Lipo battery sensor. */ -class LipoBatteryLevel : public HasBatteryLevel +class MAX17048BatteryLevel : public HasBatteryLevel { private: MAX17048Singleton *max17048 = nullptr; @@ -1406,18 +1452,18 @@ class LipoBatteryLevel : public HasBatteryLevel virtual bool isCharging() override { return max17048->isBatteryCharging(); } }; -LipoBatteryLevel lipoLevel; +MAX17048BatteryLevel max17048Level; /** * Init the Lipo battery level sensor */ -bool Power::lipoInit() +bool Power::max17048Init() { - bool result = lipoLevel.runOnce(); - LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet"); + bool result = max17048Level.runOnce(); + LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet"); if (!result) return false; - batteryLevel = &lipoLevel; + batteryLevel = &max17048Level; return true; } @@ -1425,7 +1471,88 @@ bool Power::lipoInit() /** * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel */ -bool Power::lipoInit() +bool Power::max17048Init() +{ + return false; +} +#endif + +#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015 + +class CW2015BatteryLevel : public AnalogBatteryLevel +{ + public: + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override + { + int data = -1; + Wire.beginTransmission(CW2015_ADDR); + Wire.write(0x04); + if (Wire.endTransmission() == 0) { + if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) { + data = Wire.read(); + } + } + return data; + } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override + { + uint16_t mv = 0; + Wire.beginTransmission(CW2015_ADDR); + Wire.write(0x02); + if (Wire.endTransmission() == 0) { + if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) { + mv = Wire.read(); + mv <<= 8; + mv |= Wire.read(); + // Voltage is read in 305uV units, convert to mV + mv = mv * 305 / 1000; + } + } + return mv; + } +}; + +CW2015BatteryLevel cw2015Level; + +/** + * Init the CW2015 battery level sensor + */ +bool Power::cw2015Init() +{ + + Wire.beginTransmission(CW2015_ADDR); + uint8_t getInfo[] = {0x0a, 0x00}; + Wire.write(getInfo, 2); + Wire.endTransmission(); + delay(10); + Wire.beginTransmission(CW2015_ADDR); + Wire.write(0x00); + bool result = false; + if (Wire.endTransmission() == 0) { + if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) { + uint8_t data = Wire.read(); + LOG_DEBUG("CW2015 init read data: 0x%x", data); + if (data == 0x73) { + result = true; + batteryLevel = &cw2015Level; + } + } + } + return result; +} + +#else +/** + * The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel + */ +bool Power::cw2015Init() { return false; } diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 9f8097b84..b11f37cf0 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -9,13 +9,13 @@ */ #include "PowerFSM.h" #include "Default.h" -#include "Led.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" #include "configuration.h" #include "graphics/Screen.h" #include "main.h" +#include "modules/StatusLEDModule.h" #include "sleep.h" #include "target_specific.h" @@ -38,7 +38,10 @@ static bool isPowered() return true; #endif - bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); + bool isRouter = ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) + ? 1 + : 0); // If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON // We assume routers might be powered all the time, but from a low current (solar) source @@ -103,7 +106,7 @@ static void lsIdle() uint32_t sleepTime = SLEEP_TIME; powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); - ledBlink.set(false); // Never leave led on while in light sleep + statusLEDModule->setPowerLED(false); esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL); powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); @@ -111,7 +114,7 @@ static void lsIdle() case ESP_SLEEP_WAKEUP_TIMER: // Normal case: timer expired, we should just go back to sleep ASAP - ledBlink.set(true); // briefly turn on led + statusLEDModule->setPowerLED(true); wakeCause2 = doLightSleep(100); // leave led on for 1ms secsSlept += sleepTime; @@ -146,7 +149,7 @@ static void lsIdle() } } else { // Time to stop sleeping! - ledBlink.set(false); + statusLEDModule->setPowerLED(false); LOG_INFO("Reached ls_secs, service loop()"); powerFSM.trigger(EVENT_WAKE_TIMER); } @@ -262,7 +265,10 @@ Fsm powerFSM(&stateBOOT); void PowerFSM_setup() { - bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); + bool isRouter = ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) + ? 1 + : 0); bool hasPower = isPowered(); LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index e15d56912..9450f8990 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -137,7 +137,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, if (color) { ::printf("\u001b[0m"); } - ::printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); + ::printf("| %02d:%02d:%02d %u.%03u ", hour, min, sec, millis() / 1000, millis() % 1000); #else printf("%s ", logLevel); if (color) { @@ -151,7 +151,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, if (color) { ::printf("\u001b[0m"); } - ::printf("| ??:??:?? %u ", millis() / 1000); + ::printf("| ??:??:?? %u.%03u ", millis() / 1000, millis() % 1000); #else printf("%s ", logLevel); if (color) { @@ -227,34 +227,21 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_ isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); #endif if (isBleConnected) { - char *message; - size_t initialLen; - size_t len; - initialLen = strlen(format); - message = new char[initialLen + 1]; - len = vsnprintf(message, initialLen + 1, format, arg); - if (len > initialLen) { - delete[] message; - message = new char[len + 1]; - vsnprintf(message, len + 1, format, arg); - } auto thread = concurrency::OSThread::currentThread; meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero; logRecord.level = getLogLevel(logLevel); - strcpy(logRecord.message, message); + vsprintf(logRecord.message, format, arg); if (thread) strcpy(logRecord.source, thread->ThreadName.c_str()); logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true); - uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size]; - size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); + auto buffer = std::unique_ptr(new uint8_t[meshtastic_LogRecord_size]); + size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); #ifdef ARCH_ESP32 - nimbleBluetooth->sendLog(buffer, size); + nimbleBluetooth->sendLog(buffer.get(), size); #elif defined(ARCH_NRF52) - nrf52Bluetooth->sendLog(buffer, size); + nrf52Bluetooth->sendLog(buffer.get(), size); #endif - delete[] message; - delete[] buffer; } } #else @@ -292,8 +279,8 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) // append \n to format size_t len = strlen(format); - char *newFormat = new char[len + 2]; - strcpy(newFormat, format); + auto newFormat = std::unique_ptr(new char[len + 2]); + strcpy(newFormat.get(), format); newFormat[len] = '\n'; newFormat[len + 1] = '\0'; @@ -310,23 +297,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) va_end(arg); } if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - delete[] newFormat; return; } } if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - delete[] newFormat; return; } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { - delete[] newFormat; return; } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { - delete[] newFormat; return; } #endif if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - delete[] newFormat; return; } @@ -338,11 +320,19 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) #endif va_list arg; + va_list arg_copy; + va_start(arg, format); - log_to_serial(logLevel, newFormat, arg); - log_to_syslog(logLevel, newFormat, arg); - log_to_ble(logLevel, newFormat, arg); + va_copy(arg_copy, arg); + log_to_serial(logLevel, newFormat.get(), arg_copy); + va_end(arg_copy); + + va_copy(arg_copy, arg); + log_to_syslog(logLevel, newFormat.get(), arg_copy); + va_end(arg_copy); + + log_to_ble(logLevel, newFormat.get(), arg); va_end(arg); #ifdef HAS_FREE_RTOS @@ -352,11 +342,10 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) #endif } - delete[] newFormat; return; } -void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len) +void RedirectablePrint::hexDump(const char *logLevel, const unsigned char *buf, uint16_t len) { const char alphabet[17] = "0123456789abcdef"; log(logLevel, " +------------------------------------------------+ +----------------+"); diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index 45b62b7af..c66226171 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -44,7 +44,7 @@ class RedirectablePrint : public Print /** like printf but va_list based */ size_t vprintf(const char *logLevel, const char *format, va_list arg); - void hexDump(const char *logLevel, unsigned char *buf, uint16_t len); + void hexDump(const char *logLevel, const unsigned char *buf, uint16_t len); std::string mt_sprintf(const std::string fmt_str, ...); diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index dd2acb599..2a3f08cbc 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -30,11 +30,16 @@ 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) // onReceive does only exist for HardwareSerial not for USB CDC serial Port.onReceive([sc]() { sc->rxInt(); }); +#else + (void)sc; #endif DEBUG_PORT.rpInit(); // Simply sets up semaphore } diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6fb28a6ac..6692d996d 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -6,6 +6,11 @@ #include "Tone.h" #endif +#if defined(HAS_I2S) +#include "main.h" +#include +#endif + #if !defined(ARCH_PORTDUINO) extern "C" void delay(uint32_t dwMs); #endif @@ -50,6 +55,50 @@ const int DURATION_1_2 = 500; // 1/2 note const int DURATION_3_4 = 750; // 3/4 note const int DURATION_1_1 = 1000; // 1/1 note +#ifdef HAS_I2S +void playTonesRTTTL(const ToneDuration *tone_durations, int size) +{ + // translate ToneDuration[] to RTTTL string and play using audioThread + static std::unordered_map freqToNote = { + {NOTE_C3, "c4"}, {NOTE_CS3, "c#4"}, {NOTE_D3, "d4"}, {NOTE_DS3, "d#4"}, {NOTE_E3, "e4"}, {NOTE_F3, "f4"}, + {NOTE_FS3, "f#4"}, {NOTE_G3, "g4"}, {NOTE_GS3, "g#4"}, {NOTE_A3, "a4"}, {NOTE_AS3, "a#4"}, {NOTE_B3, "b4"}, + {NOTE_C4, "c5"}, {NOTE_E4, "e5"}, {NOTE_G4, "g5"}, {NOTE_A4, "a5"}, {NOTE_C5, "c6"}, {NOTE_E5, "e6"}, + {NOTE_G5, "g6"}, {NOTE_F5, "f6"}, {NOTE_G6, "g7"}, {NOTE_E7, "e8"}}; + + char rtttl[128] = "tone:d=32,o=4,b=200:"; // default duration and octave + for (int i = 0; i < size; i++) { + const auto &td = tone_durations[i]; + std::string note = "b4"; + if (freqToNote.find(td.frequency_khz) != freqToNote.end()) { + note = freqToNote[td.frequency_khz]; + } + int dur = 32; // default duration + if (td.duration_ms >= 1000) + dur = 1; + else if (td.duration_ms >= 500) + dur = 2; + else if (td.duration_ms >= 250) + dur = 4; + else if (td.duration_ms >= 125) + dur = 8; + else if (td.duration_ms >= 62) + dur = 16; + else + dur = 32; + + char noteStr[64]; + snprintf(noteStr, sizeof(noteStr), "%s,%d", note.c_str(), dur); + strncat(rtttl, noteStr, sizeof(rtttl) - strlen(rtttl) - 1); + + audioThread->beginRttl(rtttl, strlen(rtttl)); + while (audioThread->isPlaying()) { + delay(10); + } + return; + } +} +#endif + void playTones(const ToneDuration *tone_durations, int size) { if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || @@ -57,7 +106,13 @@ void playTones(const ToneDuration *tone_durations, int size) // Buzzer is disabled or not set to system tones return; } -#ifdef PIN_BUZZER +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer && audioThread) { + playTonesRTTTL(tone_durations, size); + return; + } +#endif +#if defined(PIN_BUZZER) if (!config.device.buzzer_gpio) config.device.buzzer_gpio = PIN_BUZZER; #endif diff --git a/src/concurrency/BinarySemaphorePosix.cpp b/src/concurrency/BinarySemaphorePosix.cpp index dc49a489b..4bc60c31f 100644 --- a/src/concurrency/BinarySemaphorePosix.cpp +++ b/src/concurrency/BinarySemaphorePosix.cpp @@ -1,10 +1,85 @@ #include "concurrency/BinarySemaphorePosix.h" #include "configuration.h" +#include +#include + #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 \ No newline at end of file +#endif diff --git a/src/concurrency/BinarySemaphorePosix.h b/src/concurrency/BinarySemaphorePosix.h index 475b29874..80edb567b 100644 --- a/src/concurrency/BinarySemaphorePosix.h +++ b/src/concurrency/BinarySemaphorePosix.h @@ -2,6 +2,10 @@ #include "../freertosinc.h" +#ifdef ARCH_PORTDUINO +#include +#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 \ No newline at end of file +} // namespace concurrency diff --git a/src/concurrency/NotifiedWorkerThread.cpp b/src/concurrency/NotifiedWorkerThread.cpp index 0e4e31d9b..29aff32a5 100644 --- a/src/concurrency/NotifiedWorkerThread.cpp +++ b/src/concurrency/NotifiedWorkerThread.cpp @@ -76,8 +76,10 @@ bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrit void NotifiedWorkerThread::checkNotification() { - auto n = notification; - notification = 0; // clear notification + // Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification + // after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes + // in.) + auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe if (n) { onNotify(n); } diff --git a/src/concurrency/NotifiedWorkerThread.h b/src/concurrency/NotifiedWorkerThread.h index 7a150b0b0..166b9ea65 100644 --- a/src/concurrency/NotifiedWorkerThread.h +++ b/src/concurrency/NotifiedWorkerThread.h @@ -1,6 +1,7 @@ #pragma once #include "OSThread.h" +#include namespace concurrency { @@ -13,7 +14,7 @@ class NotifiedWorkerThread : public OSThread /** * The notification that was most recently used to wake the thread. Read from runOnce() */ - uint32_t notification = 0; + std::atomic notification{0}; public: NotifiedWorkerThread(const char *name) : OSThread(name) {} diff --git a/src/concurrency/Periodic.h b/src/concurrency/Periodic.h index db07145a6..8576be7ea 100644 --- a/src/concurrency/Periodic.h +++ b/src/concurrency/Periodic.h @@ -1,24 +1,29 @@ #pragma once +#include +#include + #include "concurrency/OSThread.h" namespace concurrency { /** - * @brief Periodically invoke a callback. This just provides C-style callback conventions - * rather than a virtual function - FIXME, remove? + * @brief Periodically invoke a callback. + * Supports both legacy function pointers and modern callables. */ class Periodic : public OSThread { - int32_t (*callback)(); - public: // callback returns the period for the next callback invocation (or 0 if we should no longer be called) - Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {} + Periodic(const char *name, int32_t (*cb)()) : OSThread(name), callback(cb) {} + Periodic(const char *name, std::function cb) : OSThread(name), callback(std::move(cb)) {} protected: - int32_t runOnce() override { return callback(); } + int32_t runOnce() override { return callback ? callback() : 0; } + + private: + std::function callback; }; } // namespace concurrency diff --git a/src/configuration.h b/src/configuration.h index f7b438272..84dabee4e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -78,6 +78,11 @@ along with this program. If not, see . // Configuration // ----------------------------------------------------------------------------- +// Pre-hop drop handling (compile-time flag). +#ifndef MESHTASTIC_PREHOP_DROP +#define MESHTASTIC_PREHOP_DROP 0 +#endif + /// Convert a preprocessor name into a quoted string #define xstr(s) ystr(s) #define ystr(s) #s @@ -149,6 +154,23 @@ along with this program. If not, see . #define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7 #endif +#ifdef USE_KCT8103L_PA +// Power Amps are often non-linear, so we can use an array of values for the power curve +#if defined(HELTEC_WIRELESS_TRACKER_V2) +#define NUM_PA_POINTS 22 +#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 12, 11, 10, 9, 8, 7 +#elif defined(HELTEC_MESH_NODE_T096) +#define NUM_PA_POINTS 22 +#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 11, 10, 9, 8, 7 +#else +// If a board enables USE_KCT8103L_PA but does not match a known variant and has +// not already provided a PA curve, fail at compile time to avoid unsafe defaults. +#if !defined(NUM_PA_POINTS) || !defined(TX_GAIN_LORA) +#error "USE_KCT8103L_PA is defined, but no PA gain curve (NUM_PA_POINTS / TX_GAIN_LORA) is configured for this board." +#endif +#endif +#endif + #ifdef RAK13302 #define NUM_PA_POINTS 22 #define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8 @@ -163,6 +185,10 @@ along with this program. If not, see . #define TX_GAIN_LORA 0 #endif +#ifndef HAS_LORA_FEM +#define HAS_LORA_FEM 0 +#endif + // ----------------------------------------------------------------------------- // Feature toggles // ----------------------------------------------------------------------------- @@ -205,7 +231,7 @@ along with this program. If not, see . #define BME_ADDR 0x76 #define BME_ADDR_ALTERNATE 0x77 #define MCP9808_ADDR 0x18 -#define INA_ADDR 0x40 +#define INA_ADDR 0x40 // same as SHT2X #define INA_ADDR_ALTERNATE 0x41 #define INA_ADDR_WAVESHARE_UPS 0x43 #define INA3221_ADDR 0x42 @@ -217,8 +243,9 @@ along with this program. If not, see . #define SHTC3_ADDR 0x70 #define LPS22HB_ADDR 0x5C #define LPS22HB_ADDR_ALT 0x5D -#define SHT31_4x_ADDR 0x44 -#define SHT31_4x_ADDR_ALT 0x45 +#define SFA30_ADDR 0x5D +#define SHTXX_ADDR 0x44 +#define SHTXX_ADDR_ALT 0x45 #define PMSA003I_ADDR 0x12 #define QMA6100P_ADDR 0x12 #define AHT10_ADDR 0x38 @@ -233,6 +260,7 @@ along with this program. If not, see . #define NAU7802_ADDR 0x2A #define MAX30102_ADDR 0x57 #define SCD4X_ADDR 0x62 +#define CW2015_ADDR 0x62 #define MLX90614_ADDR_DEF 0x5A #define CGRADSENS_ADDR 0x66 #define LTR390UV_ADDR 0x53 @@ -241,6 +269,8 @@ along with this program. If not, see . #define BQ27220_ADDR 0x55 // same address as TDECK_KB #define BQ25896_ADDR 0x6B #define LTR553ALS_ADDR 0x23 +#define SEN5X_ADDR 0x69 +#define SCD30_ADDR 0x61 // ----------------------------------------------------------------------------- // ACCELEROMETER @@ -257,6 +287,8 @@ along with this program. If not, see . #define BHI260AP_ADDR 0x28 #define BMM150_ADDR 0x13 #define DA217_ADDR 0x26 +#define BMI270_ADDR 0x68 +#define BMI270_ADDR_ALT 0x69 // ----------------------------------------------------------------------------- // LED @@ -390,9 +422,6 @@ along with this program. If not, see . #ifndef HAS_RADIO #define HAS_RADIO 0 #endif -#ifndef HAS_RTC -#define HAS_RTC 0 -#endif #ifndef HAS_CPU_SHUTDOWN #define HAS_CPU_SHUTDOWN 0 #endif @@ -428,12 +457,16 @@ along with this program. If not, see . #define HAS_RGB_LED #endif -#ifndef LED_STATE_OFF -#define LED_STATE_OFF 0 -#endif #ifndef LED_STATE_ON #define LED_STATE_ON 1 #endif +#ifndef LED_STATE_OFF +#define LED_STATE_OFF (LED_STATE_ON ^ 1) +#endif + +#ifndef ledOff +#define ledOff(pin) pinMode(pin, INPUT) +#endif // default mapping of pins #if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN) @@ -482,6 +515,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1 #define MESHTASTIC_EXCLUDE_STOREFORWARD 1 #define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1 +#define MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT 1 #define MESHTASTIC_EXCLUDE_ATAK 1 #define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1 #define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1 diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 4795d2abc..75eabc954 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -37,14 +37,14 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150}; - return firstOfOrNONE(9, types); + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270}; + return firstOfOrNONE(10, types); } ScanI2C::FoundDevice ScanI2C::firstAQI() const { - ScanI2C::DeviceType types[] = {PMSA003I, SCD4X}; - return firstOfOrNONE(2, types); + ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30}; + return firstOfOrNONE(4, types); } ScanI2C::FoundDevice ScanI2C::firstRGBLED() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index dffcd8fb6..d451d3948 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -31,9 +31,6 @@ class ScanI2C INA3221, MAX17048, MCP9808, - SHT31, - SHT4X, - SHTC3, LPS22HB, QMC6310U, QMC6310N, @@ -88,7 +85,14 @@ class ScanI2C BH1750, DA217, CHSC6X, - CST226SE + CST226SE, + BMI270, + SEN5X, + SFA30, + CW2015, + SCD30, + ADS1115, + SHTXX } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index c6ef34846..052b2245a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -1,4 +1,6 @@ #include "ScanI2CTwoWire.h" +#include "configuration.h" +#include "detect/ScanI2C.h" #if !MESHTASTIC_EXCLUDE_I2C @@ -8,6 +10,7 @@ #endif #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) #include "meshUtils.h" // vformat + #endif bool in_array(uint8_t *array, int size, uint8_t lookfor) @@ -114,6 +117,106 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation return value; } +bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const +{ + TwoWire *i2cBus = fetchI2CBus(addr); + i2cBus->beginTransmission(addr.address); + if (command > 0xFF) { + i2cBus->write((uint8_t)(command >> 8)); + } + i2cBus->write((uint8_t)(command & 0xFF)); + if (i2cBus->endTransmission() != 0) { + return false; + } + delay(20); + uint8_t received = i2cBus->requestFrom(addr.address, expectedLength); + bool match = (received == expectedLength); + while (i2cBus->available()) + i2cBus->read(); + return match; +} + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +// FIXME Move to a separate file for detection of sensors that require more complex interactions? +// For SEN5X detection +// Note, this code needs to be called before setting the I2C bus speed +// for the screen at high speed. The speed needs to be at 100kHz, otherwise +// detection will not work +String readSEN5xProductName(TwoWire *i2cBus, uint8_t address) +{ + uint8_t cmd[] = {0xD0, 0x14}; + uint8_t response[48] = {0}; + + i2cBus->beginTransmission(address); + i2cBus->write(cmd, 2); + if (i2cBus->endTransmission() != 0) + return ""; + + delay(20); + if (i2cBus->requestFrom(address, (uint8_t)48) != 48) + return ""; + + for (int i = 0; i < 48 && i2cBus->available(); ++i) { + response[i] = i2cBus->read(); + } + + char productName[33] = {0}; + int j = 0; + for (int i = 0; i < 48 && j < 32; i += 3) { + if (response[i] >= 32 && response[i] <= 126) + productName[j++] = response[i]; + else + break; + + if (response[i + 1] >= 32 && response[i + 1] <= 126) + productName[j++] = response[i + 1]; + else + break; + } + + return String(productName); +} +#endif + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address) +{ + + i2cBus->beginTransmission(address); + i2cBus->write(0xFA); + i2cBus->write(0x0F); + + if (i2cBus->endTransmission() != 0) + return false; + + if (i2cBus->requestFrom(address, (uint8_t)8) != 8) + return false; + + // Just flush the data + while (i2cBus->available() < 8) { + i2cBus->read(); + } + + i2cBus->beginTransmission(address); + i2cBus->write(0xFC); + i2cBus->write(0xC9); + + if (i2cBus->endTransmission() != 0) + return false; + + if (i2cBus->requestFrom(address, (uint8_t)6) != 6) + return false; + + // Just flush the data + while (i2cBus->available() < 6) { + i2cBus->read(); + } + + // Assume we detect the SHT21 if something came back from the request + return true; +} +#endif + #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ @@ -280,7 +383,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = DPS310; break; } - break; + if (type == DPS310) { + break; + } default: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID switch (registerValue) { @@ -308,7 +413,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; #endif #if !defined(M5STACK_UNITC6L) - case INA_ADDR: + case INA_ADDR: // Same as SHT2X case INA_ADDR_ALTERNATE: case INA_ADDR_WAVESHARE_UPS: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); @@ -324,7 +429,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("INA260", (uint8_t)addr.address); type = INA260; } - } else { // Assume INA219 if INA260 ID is not found +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + } else if (detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) { + logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address); + type = SHTXX; +#endif + } else { // Assume INA219 if none of the above ones are found logFoundDevice("INA219", (uint8_t)addr.address); type = INA219; } @@ -385,23 +495,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; } - case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT - case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR + case SHTXX_ADDR: // same as OPT3001_ADDR_ALT + case SHTXX_ADDR_ALT: // same as OPT3001_ADDR if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != - 0) { // unique SHT4x serial number (6 bytes inc. CRC) - type = SHT4X; - logFoundDevice("SHT4X", (uint8_t)addr.address); - } else { - type = SHT31; - logFoundDevice("SHT31", (uint8_t)addr.address); + } else { // SHTXX + type = SHTXX; + logFoundDevice("SHTXX", (uint8_t)addr.address); } break; - SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTXX, "SHTXX", (uint8_t)addr.address) case RCWL9620_ADDR: // get MAX30102 PARTID registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1); @@ -416,6 +522,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; case LPS22HB_ADDR_ALT: + // SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response + if (i2cCommandResponseLength(addr, 0xD060, 48)) { + type = SFA30; + logFoundDevice("SFA30", (uint8_t)addr.address); + break; + } + // Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1) + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); + if (registerValue == 0xB1) { + type = LPS22HB; + logFoundDevice("LPS22HB", (uint8_t)addr.address); + } + break; SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address) @@ -508,6 +627,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address); case CST328_ADDR: // Do we have the CST328 or the CST226SE registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1); @@ -541,7 +661,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address); + case SCD4X_ADDR: { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1); + if (registerValue == 0x18) { + logFoundDevice("CW2015", (uint8_t)addr.address); + type = CW2015; + } else { + logFoundDevice("SCD4X", (uint8_t)addr.address); + type = SCD4X; + } + break; + } SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); #ifdef HAS_TPS65233 SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); @@ -568,8 +698,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; - case ICM20948_ADDR: // same as BMX160_ADDR - case ICM20948_ADDR_ALT: // same as MPU6050_ADDR + case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR + case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); #ifdef HAS_ICM20948 type = ICM20948; @@ -580,14 +710,41 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; - } else if (addr.address == BMX160_ADDR) { + } else if (registerValue == 0x24) { + type = BMI270; + logFoundDevice("BMI270", (uint8_t)addr.address); + break; + } else if (registerValue == 0xD8) { // BMX160 chip ID at register 0x00 type = BMX160; logFoundDevice("BMX160", (uint8_t)addr.address); break; } else { - type = MPU6050; - logFoundDevice("MPU6050", (uint8_t)addr.address); - break; +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + String prod = ""; + prod = readSEN5xProductName(i2cBus, addr.address); + if (prod.startsWith("SEN55")) { + type = SEN5X; + logFoundDevice("Sensirion SEN55", addr.address); + break; + } else if (prod.startsWith("SEN54")) { + type = SEN5X; + logFoundDevice("Sensirion SEN54", addr.address); + break; + } else if (prod.startsWith("SEN50")) { + type = SEN5X; + logFoundDevice("Sensirion SEN50", addr.address); + break; + } +#endif + if (addr.address == BMX160_ADDR) { + type = BMX160; + logFoundDevice("BMX160", (uint8_t)addr.address); + break; + } else { + type = MPU6050; + logFoundDevice("MPU6050", (uint8_t)addr.address); + break; + } } break; @@ -616,11 +773,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) if (len == 5 && memcmp(expectedInfo, info, len) == 0) { LOG_INFO("NXP SE050 crypto chip found"); type = NXP_SE050; - - } else { - LOG_INFO("FT6336U touchscreen found"); - type = FT6336U; + break; } + + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2); + if (registerValue == 0x8583 || registerValue == 0x8580) { + type = ADS1115; + logFoundDevice("ADS1115 ADC", (uint8_t)addr.address); + break; + } + + LOG_INFO("FT6336U touchscreen found"); + type = FT6336U; break; } diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index c5b791920..841a8b946 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -55,6 +55,8 @@ class ScanI2CTwoWire : public ScanI2C uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const; + bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const; + DeviceType probeOLED(ScanI2C::DeviceAddress) const; static void logFoundDevice(const char *device, uint8_t address); diff --git a/src/detect/reClockI2C.cpp b/src/detect/reClockI2C.cpp new file mode 100644 index 000000000..60cd3c808 --- /dev/null +++ b/src/detect/reClockI2C.cpp @@ -0,0 +1,31 @@ +#include "reClockI2C.h" +#include "ScanI2CTwoWire.h" + +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) +{ + + uint32_t currentClock = 0; + + /* See https://github.com/arduino/Arduino/issues/11457 + Currently, only ESP32 can getClock() + While all cores can setClock() + https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) + we need to reclock I2C and set it back to the previous desired speed. + Only for cases where we can know OR predefine the speed, we can do this. + */ + +// TODO add getClock function or return a predefined clock speed per variant? +#ifdef CAN_RECLOCK_I2C + currentClock = i2cBus->getClock(); +#endif + + if ((currentClock != desiredClock) || force) { + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + i2cBus->setClock(desiredClock); + } + + return currentClock; +} diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h index 689e88d6f..9c53efc4f 100644 --- a/src/detect/reClockI2C.h +++ b/src/detect/reClockI2C.h @@ -1,41 +1,11 @@ -#ifdef CAN_RECLOCK_I2C + +#ifndef RECLOCK_I2C_ +#define RECLOCK_I2C_ + #include "ScanI2CTwoWire.h" +#include +#include -uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) -{ +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force); - uint32_t currentClock; - - /* See https://github.com/arduino/Arduino/issues/11457 - Currently, only ESP32 can getClock() - While all cores can setClock() - https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 - https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 - https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 - For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) - we need to reclock I2C and set it back to the previous desired speed. - Only for cases where we can know OR predefine the speed, we can do this. - */ - -#ifdef ARCH_ESP32 - currentClock = i2cBus->getClock(); -#elif defined(ARCH_NRF52) - // TODO add getClock function or return a predefined clock speed per variant? - return 0; -#elif defined(ARCH_RP2040) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#elif defined(ARCH_STM32WL) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#else - return 0; -#endif - - if (currentClock != desiredClock) { - LOG_DEBUG("Changing I2C clock to %u", desiredClock); - i2cBus->setClock(desiredClock); - } - return currentClock; -} #endif diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 471cbff8f..33118a12a 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -52,7 +52,7 @@ SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT; HardwareSerial *GPS::_serial_gps = nullptr; #endif -GPS *gps = nullptr; +std::unique_ptr gps = nullptr; static GPSUpdateScheduling scheduling; @@ -93,7 +93,7 @@ static const char *getGPSPowerStateString(GPSPowerState state) #ifdef PIN_GPS_SWITCH // If we have a hardware switch, define a periodic watcher outside of the GPS runOnce thread, since this can be sleeping -// idefinitely +// indefinitely int lastState = LOW; bool firstrun = true; @@ -103,6 +103,14 @@ static int32_t gpsSwitch() if (gps) { int currentState = digitalRead(PIN_GPS_SWITCH); + // Respect explicit NOT_PRESENT mode and do not let the hardware switch re-enable GPS. + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + gps->disable(); + lastState = currentState; + firstrun = false; + return 1000; + } + // if the switch is set to zero, disable the GPS Thread if (firstrun) if (currentState == LOW) @@ -127,7 +135,7 @@ static int32_t gpsSwitch() return 1000; } -static concurrency::Periodic *gpsPeriodic; +static std::unique_ptr gpsPeriodic; #endif static void UBXChecksum(uint8_t *message, size_t length) @@ -586,14 +594,14 @@ bool GPS::setup() _serial_gps->write("$PMTK301,2*2E\r\n"); delay(250); } else if (gnssModel == GNSS_MODEL_ATGM336H) { - // Set the intial configuration of the device - these _should_ work for most AT6558 devices + // Set the initial configuration of the device - these _should_ work for most AT6558 devices msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF); _serial_gps->write(UBXscratch, msglen); if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) { LOG_WARN("ATGM336H: Could not set Config"); } - // Set the update frequence to 1Hz + // Set the update frequency to 1Hz msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ); _serial_gps->write(UBXscratch, msglen); if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) { @@ -700,7 +708,7 @@ bool GPS::setup() } else { // 8,9 LOG_INFO("GPS+SBAS+GLONASS+Galileo configured"); } - // Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next + // Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next // commands for the M8 it tends to be more... 1 sec should be enough ;>) delay(1000); } @@ -733,7 +741,7 @@ bool GPS::setup() SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500); SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); - // For M8 we want to enable NMEA vserion 4.10 so we can see the additional sats. + // For M8 we want to enable NMEA version 4.10 so we can see the additional sats. if (gnssModel == GNSS_MODEL_UBLOX8) { clearBuffer(); SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500); @@ -1211,7 +1219,7 @@ int32_t GPS::runOnce() return disable(); // This should trigger when we have a fixed position, and get that first position // 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms - // if not awake we can run super infrquently (once every 5 secs?) to see if we need to wake. + // if not awake we can run super infrequently (once every 5 secs?) to see if we need to wake. return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000; } @@ -1485,7 +1493,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector 2048) bufferSize = 2048; - char *response = new char[bufferSize](); // Dynamically allocate based on baud rate + auto response = std::unique_ptr(new char[bufferSize]); // Dynamically allocate based on baud rate uint16_t responseLen = 0; unsigned long start = millis(); while (millis() - start < timeout) { @@ -1501,19 +1509,18 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { // check if we can see our chips for (const auto &chipInfo : responseMap) { - if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { + if (strstr(response.get(), chipInfo.detectionString.c_str()) != nullptr) { #ifdef GPS_DEBUG - LOG_DEBUG(response); + LOG_DEBUG(response.get()); #endif LOG_INFO("%s detected", chipInfo.chipName.c_str()); - delete[] response; // Cleanup before return return chipInfo.driver; } } } if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { #ifdef GPS_DEBUG - LOG_DEBUG(response); + LOG_DEBUG(response.get()); #endif // Reset the response buffer for the next potential message responseLen = 0; @@ -1522,13 +1529,12 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector GPS::createGps() { int8_t _rx_gpio = config.position.rx_gpio; int8_t _tx_gpio = config.position.tx_gpio; @@ -1553,7 +1559,7 @@ GPS *GPS::createGps() if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all return nullptr; - GPS *new_gps = new GPS; + auto new_gps = std::unique_ptr(new GPS()); new_gps->rx_gpio = _rx_gpio; new_gps->tx_gpio = _tx_gpio; @@ -1581,7 +1587,7 @@ GPS *GPS::createGps() #ifdef PIN_GPS_SWITCH // toggle GPS via external GPIO switch pinMode(PIN_GPS_SWITCH, INPUT); - gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch); + gpsPeriodic = std::unique_ptr(new concurrency::Periodic("GPSSwitch", gpsSwitch)); #endif // Currently disabled per issue #525 (TinyGPS++ crash bug) diff --git a/src/gps/GPS.h b/src/gps/GPS.h index fcbf361d5..8d63ce82f 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -2,6 +2,8 @@ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_GPS +#include + #include "GPSStatus.h" #include "GpioLogic.h" #include "Observer.h" @@ -118,7 +120,7 @@ class GPS : private concurrency::OSThread // Creates an instance of the GPS class. // Returns the new instance or null if the GPS is not present. - static GPS *createGps(); + static std::unique_ptr createGps(); // Wake the GPS hardware - ready for an update void up(); @@ -256,5 +258,5 @@ class GPS : private concurrency::OSThread uint8_t fixeddelayCtr = 0; }; -extern GPS *gps; +extern std::unique_ptr gps; #endif // Exclude GPS diff --git a/src/gps/GeoCoord.cpp b/src/gps/GeoCoord.cpp index 6d1f2da6d..04c25caa3 100644 --- a/src/gps/GeoCoord.cpp +++ b/src/gps/GeoCoord.cpp @@ -12,7 +12,7 @@ GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _lon GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) { - // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 + // Change decimal representation to int32_t. I.e., 12.345 becomes 123450000 _latitude = int32_t(lat * 1e+7); _longitude = int32_t(lon * 1e+7); GeoCoord::setCoords(); @@ -20,7 +20,7 @@ GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt) { - // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 + // Change decimal representation to int32_t. I.e., 12.345 becomes 123450000 _latitude = int32_t(lat * 1e+7); _longitude = int32_t(lon * 1e+7); GeoCoord::setCoords(); @@ -467,10 +467,10 @@ int32_t GeoCoord::bearingTo(const GeoCoord &pointB) } /** - * Create a new point bassed on the passed in poin + * Create a new point based on the passed-in point * Ported from http://www.edwilliams.org/avform147.htm#LL * @param bearing - * The bearing in raidans + * The bearing in radians * @param range_meters * range in meters * @return GeoCoord object of point at bearing and range from initial point @@ -593,4 +593,4 @@ double GeoCoord::toRadians(double deg) double GeoCoord::toDegrees(double r) { return r * 180 / PI; -} \ No newline at end of file +} diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index ad26b55a4..a8288a069 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -2,6 +2,7 @@ #include "configuration.h" #include "detect/ScanI2C.h" #include "main.h" +#include "modules/NodeInfoModule.h" #include #include #include @@ -12,6 +13,14 @@ uint32_t lastSetFromPhoneNtpOrGps = 0; static uint32_t lastTimeValidationWarning = 0; static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds +static void triggerNodeInfoCheckOnTimeSource(RTCQuality oldQuality, RTCQuality newQuality) +{ + if (oldQuality == RTCQualityNone && newQuality > RTCQualityNone && nodeInfoModule) { + LOG_DEBUG("Time source acquired (%s -> %s), triggering NodeInfo recheck", RtcName(oldQuality), RtcName(newQuality)); + nodeInfoModule->triggerImmediateNodeInfoCheck(); + } +} + RTCQuality getRTCQuality() { return currentQuality; @@ -61,9 +70,11 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } else { @@ -72,11 +83,13 @@ RTCSetResult readFromRTC() #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { + SensorPCF8563 rtc; #elif defined(PCF85063_RTC) if (rtc_found.address == PCF85063_RTC) { + SensorPCF85063 rtc; + #endif uint32_t now = millis(); - SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); @@ -103,9 +116,11 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } else { @@ -137,9 +152,11 @@ RTCSetResult readFromRTC() } #endif if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } @@ -212,6 +229,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd } if (shouldSet) { + RTCQuality oldQuality = currentQuality; currentQuality = q; lastSetMsec = now; if (currentQuality >= RTCQualityNTP) { @@ -221,7 +239,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd // This delta value works on all platforms timeStartMsec = now; zeroOffsetSecs = tv->tv_sec; - // If this platform has a setable RTC, set it + // If this platform has a settable RTC, set it #ifdef RV3028_RTC if (rtc_found.address == RV3028_RTC) { Melopero_RV3028 rtc; @@ -240,10 +258,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { + SensorPCF8563 rtc; #elif defined(PCF85063_RTC) if (rtc_found.address == PCF85063_RTC) { + SensorPCF85063 rtc; + #endif - SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); @@ -276,11 +296,8 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd settimeofday(tv, NULL); #endif - // nrf52 doesn't have a readable RTC (yet - software not written) -#if HAS_RTC readFromRTC(); -#endif - + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); return RTCSetResultSuccess; } else { return RTCSetResultNotSet; // RTC was already set with a higher quality time @@ -312,7 +329,7 @@ const char *RtcName(RTCQuality quality) * @param t The time to potentially set the RTC to. * @return True if the RTC was set to the provided time, false otherwise. */ -RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) +RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t) { /* Convert to unix time The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 @@ -397,12 +414,23 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) return (currentQuality >= minQuality) ? getTime(local) : 0; } +#ifdef PIO_UNIT_TESTING +void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot) +{ + currentQuality = RTCQualityNone; + zeroOffsetSecs = 0; + timeStartMsec = millis() - (secondsSinceBoot * 1000); + lastSetFromPhoneNtpOrGps = 0; + lastTimeValidationWarning = 0; +} +#endif + time_t gm_mktime(const struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ time_t result = 0; - // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + // First, get us to the start of tm->year, by calculating the number of days since the Unix epoch. int year = 1900 + tm->tm_year; // tm_year is years since 1900 int year_minus_one = year - 1; int days_before_this_year = 0; diff --git a/src/gps/RTC.h b/src/gps/RTC.h index cf6db0239..cd1e1d002 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -41,7 +41,7 @@ extern uint32_t lastSetFromPhoneNtpOrGps; /// If we haven't yet set our RTC this boot, set it from a GPS derived time RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); -RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t); +RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t); /// Return a string name for the quality const char *RtcName(RTCQuality quality); @@ -54,6 +54,10 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false); RTCSetResult readFromRTC(); +#ifdef PIO_UNIT_TESTING +void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot); +#endif + time_t gm_mktime(const struct tm *tm); #define SEC_PER_DAY 86400 diff --git a/src/gps/cas.h b/src/gps/cas.h index 725fd07b3..2a30fd586 100644 --- a/src/gps/cas.h +++ b/src/gps/cas.h @@ -37,7 +37,7 @@ static const uint8_t _message_CAS_CFG_RATE_1HZ[] = { // CFG-NAVX (0x06, 0x07) // Initial ATGM33H-5N configuration, Updates for Dynamic Mode, Fix Mode, and SV system -// Qwirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable +// Quirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable // and use GPS+BDS+GLONASS iff the correct CFG_NAVX command is used. static const uint8_t _message_CAS_CFG_NAVX_CONF[] = { 0x03, 0x01, 0x00, 0x00, // Update Mask: Dynamic Mode, Fix Mode, Nav Settings diff --git a/src/gps/ubx.h b/src/gps/ubx.h index 0fe2f01fb..8c32ee151 100644 --- a/src/gps/ubx.h +++ b/src/gps/ubx.h @@ -57,7 +57,7 @@ static const uint8_t _message_CFG_PM2[] PROGMEM = { 0x00, 0x00, 0x00, 0x00 // 0x64, 0x40, 0x01, 0x00 // reserved 11 }; -// Constallation setup, none required for Neo-6 +// Constellation setup, none required for Neo-6 // For Neo-7 GPS & SBAS static const uint8_t _message_GNSS_7[] = { @@ -157,7 +157,7 @@ static const uint8_t _message_NAVX5[] = { 0x00, 0x00, 0x00, 0x00, // Reserved 9 0x00, // Reserved 10 0x00, // Reserved 11 - 0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true) + 0x00, // usePPP (Precise Point Positioning) (0 = false, 1 = true) 0x01, // useAOP (AssistNow Autonomous configuration) = 1 (enabled) 0x00, // Reserved 12 0x00, // Reserved 13 @@ -185,7 +185,7 @@ static const uint8_t _message_NAVX5_8[] = { 0x00, // Reserved 4 0x00, 0x00, // Reserved 5 0x00, 0x00, // Reserved 6 - 0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true) + 0x00, // usePPP (Precise Point Positioning) (0 = false, 1 = true) 0x01, // aopCfg (AssistNow Autonomous configuration) = 1 (enabled) 0x00, 0x00, // Reserved 7 0x00, 0x00, // aopOrbMaxErr = 0 to reset to firmware default @@ -314,7 +314,7 @@ static const uint8_t _message_DISABLE_TXT_INFO[] = { // This command applies to M8 products static const uint8_t _message_PMS[] = { 0x00, // Version (0) - 0x03, // Power setup value 3 = Agresssive 1Hz + 0x03, // Power setup value 3 = Agressive 1Hz 0x00, 0x00, // period: not applicable, set to 0 0x00, 0x00, // onTime: not applicable, set to 0 0x00, 0x00 // reserved, generated by u-center @@ -337,7 +337,7 @@ static const uint8_t _message_SAVE_10[] = { // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. // BBR will survive a restart, and power off for a while, but modules with small backup // batteries or super caps will not retain the config for a long power off time. -// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast aquisition after +// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast acquisition after // sleep // VALSET Commands for M10 @@ -462,7 +462,7 @@ Default GNSS configuration is: GPS, Galileo, BDS B1l, with QZSS and SBAS enabled The PMREQ puts the receiver to sleep and wakeup re-acquires really fast and seems to not need the PM config. Lets try without it. PMREQ sort of works with SBAS, but the awake time is too short to re-acquire any SBAS sats. -The defination of "Got Fix" doesn't seem to include SBAS. Much more too this... +The definition of "Got Fix" doesn't seem to include SBAS. Much more too this... Even if it was, it can take minutes (up to 12.5), even under good sat visibility conditions to re-acquire the SBAS data. diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 1678da793..704487bc8 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#ifdef USE_EINK +#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #include "EInkDisplay2.h" #include "SPILock.h" #include "main.h" @@ -101,7 +101,7 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) return true; } -// End the update process - virtual method, overriden in derived class +// End the update process - virtual method, overridden in derived class void EInkDisplay::endUpdate() { // Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep) @@ -143,6 +143,10 @@ bool EInkDisplay::connect() #ifdef ELECROW_ThinkNode_M1 // ThinkNode M1 has a hardware dimmable backlight. Start enabled digitalWrite(PIN_EINK_EN, HIGH); +#elif defined(MINI_EPAPER_S3) + // T-Mini Epaper S3 requires panel power rail enabled before SPI transfer. + digitalWrite(PIN_EINK_EN, HIGH); + delay(10); #else digitalWrite(PIN_EINK_EN, LOW); #endif @@ -202,7 +206,8 @@ bool EInkDisplay::connect() } #elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ - defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) + defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || \ + defined(MINI_EPAPER_S3) { // Start HSPI hspi = new SPIClass(HSPI); @@ -216,9 +221,13 @@ bool EInkDisplay::connect() // Init GxEPD2 adafruitDisplay->init(); +#if defined(MINI_EPAPER_S3) + adafruitDisplay->setRotation(3); +#else adafruitDisplay->setRotation(3); #if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) adafruitDisplay->setRotation(0); +#endif #endif } #elif defined(PCA10059) || defined(ME25LS01) diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index f5418b069..645a3f2d0 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_EINK +#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #include "GxEPD2_BW.h" #include @@ -89,7 +89,8 @@ class EInkDisplay : public OLEDDisplay // If display uses HSPI #if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ - defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) + defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) || \ + defined(MINI_EPAPER_S3) SPIClass *hspi = NULL; #endif diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp index 8e4adf87e..a48ba5c93 100644 --- a/src/graphics/EInkDynamicDisplay.cpp +++ b/src/graphics/EInkDynamicDisplay.cpp @@ -10,7 +10,7 @@ EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDI { // If tracking ghost pixels, grab memory #ifdef EINK_LIMIT_GHOSTING_PX - dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros + dirtyPixels = std::unique_ptr(new uint8_t[EInkDisplay::displayBufferSize]()); // Init with zeros #endif } @@ -19,7 +19,7 @@ EInkDynamicDisplay::~EInkDynamicDisplay() { // If we were tracking ghost pixels, free the memory #ifdef EINK_LIMIT_GHOSTING_PX - delete[] dirtyPixels; + dirtyPixels = nullptr; #endif } @@ -95,7 +95,7 @@ void EInkDynamicDisplay::adjustRefreshCounters() // Trigger the display update by calling base class bool EInkDynamicDisplay::update() { - // Detemine the refresh mode to use, and start the update + // Determine the refresh mode to use, and start the update bool refreshApproved = determineMode(); if (refreshApproved) { EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system @@ -317,7 +317,7 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious() LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags); } -// Have too many fast-refreshes occured consecutively, since last full refresh? +// Have too many fast-refreshes occurred consecutively, since last full refresh? void EInkDynamicDisplay::checkConsecutiveFastRefreshes() { // If a decision was already reached, don't run the check @@ -454,7 +454,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting() void EInkDynamicDisplay::resetGhostPixelTracking() { // Copy the current frame into dirtyPixels[] from the display buffer - memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize); + memcpy(dirtyPixels.get(), EInkDisplay::buffer, EInkDisplay::displayBufferSize); } #endif // EINK_LIMIT_GHOSTING_PX @@ -561,4 +561,4 @@ void EInkDynamicDisplay::awaitRefresh() } #endif // HAS_EINK_ASYNCFULL -#endif // USE_EINK_DYNAMICDISPLAY \ No newline at end of file +#endif // USE_EINK_DYNAMICDISPLAY diff --git a/src/graphics/EInkDynamicDisplay.h b/src/graphics/EInkDynamicDisplay.h index d5e29e3f0..b03061873 100644 --- a/src/graphics/EInkDynamicDisplay.h +++ b/src/graphics/EInkDynamicDisplay.h @@ -1,6 +1,7 @@ #pragma once #include "configuration.h" +#include #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) @@ -116,11 +117,11 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo // Optional - track ghosting, pixel by pixel // May 2024: no longer used by any display. Kept for possible future use. #ifdef EINK_LIMIT_GHOSTING_PX - void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh - void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit - void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. - uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) - uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use + void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh + void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit + void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. + std::unique_ptr dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) + uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use #endif // Conditional - async full refresh - only with modified meshtastic/GxEPD2 diff --git a/src/graphics/EInkParallelDisplay.cpp b/src/graphics/EInkParallelDisplay.cpp new file mode 100644 index 000000000..293f57e81 --- /dev/null +++ b/src/graphics/EInkParallelDisplay.cpp @@ -0,0 +1,427 @@ +#include "EInkParallelDisplay.h" + +#ifdef USE_EINK_PARALLELDISPLAY + +#include "Wire.h" +#include "variant.h" +#include +#include +#include +#include + +#include "FastEPD.h" + +// Thresholds for choosing partial vs full update +#ifndef EPD_PARTIAL_THRESHOLD_ROWS +#define EPD_PARTIAL_THRESHOLD_ROWS 128 // if changed region <= this many rows, prefer partial +#endif +#ifndef EPD_FULLSLOW_PERIOD +#define EPD_FULLSLOW_PERIOD 100 // every N full updates do a slow (CLEAR_SLOW) full refresh +#endif +#ifndef EPD_RESPONSIVE_MIN_MS +#define EPD_RESPONSIVE_MIN_MS 1000 // simple rate-limit (ms) for responsive updates +#endif + +EInkParallelDisplay::EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rot) : epaper(nullptr), rotation(rot) +{ + LOG_INFO("init EInkParallelDisplay"); + // Set dimensions in OLEDDisplay base class + this->geometry = GEOMETRY_RAWMODE; + this->displayWidth = width; + this->displayHeight = height; + + // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer + uint16_t shortSide = min(width, height); + uint16_t longSide = max(width, height); + if (shortSide % 8 != 0) + shortSide = (shortSide | 7) + 1; + + this->displayBufferSize = longSide * (shortSide / 8); + +#ifdef EINK_LIMIT_GHOSTING_PX + // allocate dirty pixel buffer same size as epaper buffers (rowBytes * height) + size_t rowBytes = (this->displayWidth + 7) / 8; + dirtyPixelsSize = rowBytes * this->displayHeight; + dirtyPixels = (uint8_t *)calloc(dirtyPixelsSize, 1); + ghostPixelCount = 0; +#endif +} + +EInkParallelDisplay::~EInkParallelDisplay() +{ +#ifdef EINK_LIMIT_GHOSTING_PX + if (dirtyPixels) { + free(dirtyPixels); + dirtyPixels = nullptr; + } +#endif + // If an async full update is running, wait for it to finish + if (asyncFullRunning.load()) { + // wait a short while for task to finish + for (int i = 0; i < 50 && asyncFullRunning.load(); ++i) { + delay(50); + } + if (asyncTaskHandle) { + // Let it finish or delete it + vTaskDelete(asyncTaskHandle); + asyncTaskHandle = nullptr; + } + } + + delete epaper; +} + +/* + * Called by the OLEDDisplay::init() path. + */ +bool EInkParallelDisplay::connect() +{ + LOG_INFO("Do EPD init"); + if (!epaper) { + epaper = new FASTEPD; +#if defined(T5_S3_EPAPER_PRO_V1) + epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000); +#elif defined(T5_S3_EPAPER_PRO_V2) + epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000); + // initialize all port 0 pins (0-7) as outputs / HIGH + for (int i = 0; i < 8; i++) { + epaper->ioPinMode(i, OUTPUT); + epaper->ioWrite(i, HIGH); + } +#else +#error "unsupported EPD device!" +#endif + } + + // epaper->setRotation(rotation); // does not work, messes up width/height + epaper->setMode(BB_MODE_1BPP); + epaper->clearWhite(); + epaper->fullUpdate(true); + +#ifdef EINK_LIMIT_GHOSTING_PX + // After a full/clear the dirty tracking should be reset + resetGhostPixelTracking(); +#endif + + return true; +} + +/* + * sendCommand - simple passthrough (not required for epd_driver-based path) + */ +void EInkParallelDisplay::sendCommand(uint8_t com) +{ + LOG_DEBUG("EInkParallelDisplay::sendCommand %d", (int)com); +} + +/* + * Start a background task that will perform a blocking fullUpdate(). This lets + * display() return quickly while the heavy refresh runs in the background. + */ +void EInkParallelDisplay::startAsyncFullUpdate(int clearMode) +{ + if (asyncFullRunning.load()) + return; // already running + + asyncFullRunning.store(true); + // pass 'this' as parameter + BaseType_t rc = xTaskCreatePinnedToCore(EInkParallelDisplay::asyncFullUpdateTask, "epd_full", 4096 / sizeof(StackType_t), + this, 2, &asyncTaskHandle, +#if CONFIG_FREERTOS_UNICORE + 0 +#else + 1 +#endif + ); + if (rc != pdPASS) { + LOG_WARN("Failed to create async full-update task, falling back to blocking update"); + epaper->fullUpdate(clearMode, false); + epaper->backupPlane(); + asyncFullRunning.store(false); + asyncTaskHandle = nullptr; + } +} + +/* + * FreeRTOS task entry: runs the full update and then backs up plane. + */ +void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters) +{ + EInkParallelDisplay *self = static_cast(pvParameters); + if (!self) { + vTaskDelete(nullptr); + return; + } + + // choose CLEAR_SLOW occasionally + int clearMode = CLEAR_FAST; + if (self->fastRefreshCount >= EPD_FULLSLOW_PERIOD) { + clearMode = CLEAR_SLOW; + self->fastRefreshCount = 0; + } else { + // when running async full, treat it as a full so reset fast count + self->fastRefreshCount = 0; + } + + self->epaper->fullUpdate(clearMode, false); + self->epaper->backupPlane(); + +#ifdef EINK_LIMIT_GHOSTING_PX + // A full refresh clears ghosting state + self->resetGhostPixelTracking(); +#endif + + self->asyncFullRunning.store(false); + self->asyncTaskHandle = nullptr; + + // delete this task + vTaskDelete(nullptr); +} + +/* + * Convert the OLEDDisplay buffer (vertical byte layout) into the 1bpp horizontal-bytes + * buffer used by the FASTEPD library. For performance we write directly into FASTEPD's + * currentBuffer() while comparing against previousBuffer() to detect changed rows. + * After conversion we call FASTEPD::partialUpdate() or FASTEPD::fullUpdate() according + * to a heuristic so only the minimal region is refreshed. + */ +void EInkParallelDisplay::display(void) +{ + const uint16_t w = this->displayWidth; + const uint16_t h = this->displayHeight; + + // Simple rate limiting: avoid very-frequent responsive updates + uint32_t nowMs = millis(); + if (lastUpdateMs != 0 && (nowMs - lastUpdateMs) < EPD_RESPONSIVE_MIN_MS) { + LOG_DEBUG("rate-limited, skipping update"); + return; + } + + // bytes per row in epd format (one byte = 8 horizontal pixels) + const uint32_t rowBytes = (w + 7) / 8; + + // Get pointers to internal buffers + uint8_t *cur = epaper->currentBuffer(); + 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) + int newBottom = -1; // max changed row + +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // Track changed byte column range (for clipped fullUpdate fallback) + int newLeftByte = (int)rowBytes; + int newRightByte = -1; +#endif + + // Compute a quick hash of the incoming OLED buffer (so we can skip identical frames) + uint32_t imageHash = 0; + uint32_t bufBytes = (w / 8) * h; // vertical-byte layout size + for (uint32_t bi = 0; bi < bufBytes; ++bi) { + imageHash ^= ((uint32_t)buffer[bi]) << (bi & 31); + } + if (imageHash == previousImageHash) { + // LOG_DEBUG("image identical to previous, skipping update"); + return; + } + +#ifdef EINK_LIMIT_GHOSTING_PX + // reset ghost count for this conversion pass; we'll mark bits that change + ghostPixelCount = 0; +#endif + + // Convert: OLED buffer layout -> FASTEPD 1bpp horizontal-bytes layout into cur, + // comparing against prev when available to detect changes. + for (uint32_t y = 0; y < h; ++y) { + const uint32_t base = (y >> 3) * w; // (y/8) * width + const uint8_t bitMask = (uint8_t)(1u << (y & 7)); // mask for this row in vertical-byte layout + const uint32_t rowBase = y * rowBytes; + + // process full 8-pixel bytes + for (uint32_t xb = 0; xb < rowBytes; ++xb) { + uint32_t x0 = xb * 8; + // read up to 8 source bytes (vertical-byte per column) + uint8_t b0 = (x0 + 0 < w) ? buffer[base + x0 + 0] : 0; + uint8_t b1 = (x0 + 1 < w) ? buffer[base + x0 + 1] : 0; + uint8_t b2 = (x0 + 2 < w) ? buffer[base + x0 + 2] : 0; + uint8_t b3 = (x0 + 3 < w) ? buffer[base + x0 + 3] : 0; + uint8_t b4 = (x0 + 4 < w) ? buffer[base + x0 + 4] : 0; + uint8_t b5 = (x0 + 5 < w) ? buffer[base + x0 + 5] : 0; + uint8_t b6 = (x0 + 6 < w) ? buffer[base + x0 + 6] : 0; + uint8_t b7 = (x0 + 7 < w) ? buffer[base + x0 + 7] : 0; + + // build output byte: MSB = leftmost pixel + uint8_t out = 0; + out |= (uint8_t)((b0 & bitMask) ? 0x80 : 0x00); + out |= (uint8_t)((b1 & bitMask) ? 0x40 : 0x00); + out |= (uint8_t)((b2 & bitMask) ? 0x20 : 0x00); + out |= (uint8_t)((b3 & bitMask) ? 0x10 : 0x00); + out |= (uint8_t)((b4 & bitMask) ? 0x08 : 0x00); + out |= (uint8_t)((b5 & bitMask) ? 0x04 : 0x00); + out |= (uint8_t)((b6 & bitMask) ? 0x02 : 0x00); + out |= (uint8_t)((b7 & bitMask) ? 0x01 : 0x00); + + // handle partial byte at end of row by masking off invalid bits + uint8_t mask = 0xFF; + uint32_t bitsRemain = (w > x0) ? (w - x0) : 0; + if (bitsRemain > 0 && bitsRemain < 8) { + mask = (uint8_t)(0xFF << (8 - bitsRemain)); + out &= mask; + } + + // invert to FASTEPD polarity + out = (~out) & mask; + + uint32_t pos = rowBase + xb; + uint8_t prevVal = prev ? (prev[pos] & mask) : 0x00; + // Consider this byte changed if previous buffer differs (or prev is null) + bool changed = (prev == nullptr) || (prevVal != out); + +#ifdef EINK_LIMIT_GHOSTING_PX + if (changed && prev) + markDirtyBits(prev, pos, mask, out); +#endif + + // mark row changed only if the previous buffer differs + if (changed) { + if (y < (uint32_t)newTop) + newTop = y; + if ((int)y > newBottom) + newBottom = y; +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // record changed column bytes + if ((int)xb < newLeftByte) + newLeftByte = (int)xb; + if ((int)xb > newRightByte) + newRightByte = (int)xb; +#endif + } + + // Always write the computed value into the current buffer (avoid leaving stale bytes) + cur[pos] = (cur[pos] & ~mask) | out; + } + } + + // If nothing changed, avoid any panel update + if (newBottom < 0) { + LOG_DEBUG("no pixel changes detected, skipping update (conv)"); + previousImageHash = imageHash; // still remember that frame + return; + } + + // Choose partial vs full update using heuristic + // Decide if we should force a full update after many fast updates + bool forceFull = (fastRefreshCount >= EPD_FULLSLOW_PERIOD); + +#ifdef EINK_LIMIT_GHOSTING_PX + // If ghost pixels exceed limit, force a full update to clear ghosting + if (ghostPixelCount > ghostPixelLimit) { + LOG_WARN("ghost pixels %u > limit %u, forcing full refresh", ghostPixelCount, ghostPixelLimit); + forceFull = true; + } +#endif + + // Compute pixel bounds from newTop/newBottom + int startRow = (newTop / 8) * 8; + int endRow = (newBottom / 8) * 8 + 7; + + LOG_DEBUG("EPD update rows=%d..%d alignedRows=%d..%d rowBytes=%u", newTop, newBottom, startRow, endRow, rowBytes); + + if (epaper->getMode() == BB_MODE_1BPP && !forceFull && (newBottom - newTop) <= EPD_PARTIAL_THRESHOLD_ROWS) { + // Prefer partial update path if driver is reliable; otherwise use clipped fullUpdate fallback. +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // Workaround for FastEPD partial update bug: use clipped fullUpdate instead + // Build a pixel rectangle for a clipped fullUpdate using the changed columns + int startCol = (newLeftByte <= newRightByte) ? (newLeftByte * 8) : 0; + int endCol = (newLeftByte <= newRightByte) ? ((newRightByte + 1) * 8 - 1) : (w - 1); + + BB_RECT rect{startCol, startRow, endCol - startCol + 1, endRow - startRow + 1}; + // LOG_DEBUG("Using clipped fullUpdate rect x=%d y=%d w=%d h=%d", rect.x, rect.y, rect.w, rect.h); + epaper->fullUpdate(CLEAR_FAST, false, &rect); +#else + // Use rows for partial update + LOG_DEBUG("calling partialUpdate startRow=%d endRow=%d", startRow, endRow); + epaper->partialUpdate(true, startRow, endRow); +#endif + epaper->backupPlane(); + fastRefreshCount++; + } else { + // Full update: run async if possible (startAsyncFullUpdate will fall back to blocking) + startAsyncFullUpdate(forceFull ? CLEAR_SLOW : CLEAR_FAST); + } + + lastUpdateMs = millis(); + previousImageHash = imageHash; + + // Keep same behavior as before + lastDrawMsec = millis(); +} + +#ifdef EINK_LIMIT_GHOSTING_PX +// markDirtyBits: mark per-bit dirty flags and update ghostPixelCount +void EInkParallelDisplay::markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out) +{ + // defensive: need dirtyPixels allocated and prevBuf valid + if (!dirtyPixels || !prevBuf) + return; + + // 'out' is in FASTEPD polarity (1 = black, 0 = white) + uint8_t newBlack = out & mask; // bits that will be black now + uint8_t newWhite = (~out) & mask; // bits that will be white now + + // previously recorded dirty bits for this byte + uint8_t before = dirtyPixels[pos]; + + // Ghost bits: bits that were previously marked dirty and are now being driven white + uint8_t ghostBits = before & newWhite; + if (ghostBits) { + ghostPixelCount += __builtin_popcount((unsigned)ghostBits); + } + + // Only mark bits dirty when they turn black now (accumulate until a full refresh) + uint8_t newlyDirty = newBlack & (~before); + if (newlyDirty) { + dirtyPixels[pos] |= newlyDirty; + } +} + +// reset ghost tracking (call after a full refresh) +void EInkParallelDisplay::resetGhostPixelTracking() +{ + if (!dirtyPixels) + return; + memset(dirtyPixels, 0, dirtyPixelsSize); + ghostPixelCount = 0; +} +#endif + +/* + * forceDisplay: use lastDrawMsec + */ +bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit) +{ + uint32_t now = millis(); + if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) { + display(); + return true; + } + return false; +} + +void EInkParallelDisplay::endUpdate() +{ + { + // ensure any async full update is started/completed + if (asyncFullRunning.load()) { + // nothing to do; background task will run and call backupPlane when done + } else { + epaper->fullUpdate(CLEAR_FAST, false); + epaper->backupPlane(); +#ifdef EINK_LIMIT_GHOSTING_PX + resetGhostPixelTracking(); +#endif + } + } +} + +#endif \ No newline at end of file diff --git a/src/graphics/EInkParallelDisplay.h b/src/graphics/EInkParallelDisplay.h new file mode 100644 index 000000000..81189e400 --- /dev/null +++ b/src/graphics/EInkParallelDisplay.h @@ -0,0 +1,69 @@ +#pragma once + +#include "configuration.h" + +#ifdef USE_EINK_PARALLELDISPLAY +#include + +#include +#include +#include + +class FASTEPD; + +/** + * Adapter for E-Ink 8-bit parallel displays (EPD), specifically devices supported by FastEPD library + */ +class EInkParallelDisplay : public OLEDDisplay +{ + public: + enum EpdRotation { + EPD_ROT_LANDSCAPE = 0, + EPD_ROT_PORTRAIT = 90, + EPD_ROT_INVERTED_LANDSCAPE = 180, + EPD_ROT_INVERTED_PORTRAIT = 270, + }; + + EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rotation); + virtual ~EInkParallelDisplay(); + + // OLEDDisplay virtuals + bool connect() override; + void sendCommand(uint8_t com) override; + int getBufferOffset(void) override { return 0; } + + void display(void) override; + bool forceDisplay(uint32_t msecLimit = 1000); + void endUpdate(); + + protected: + uint32_t lastDrawMsec = 0; + FASTEPD *epaper; + + private: + // Async full-refresh support + std::atomic asyncFullRunning{false}; + TaskHandle_t asyncTaskHandle = nullptr; + void startAsyncFullUpdate(int clearMode); + static void asyncFullUpdateTask(void *pvParameters); + +#ifdef EINK_LIMIT_GHOSTING_PX + // helpers + void resetGhostPixelTracking(); + void markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out); + void countGhostPixelsAndMaybePromote(int &newTop, int &newBottom, bool &forceFull); + + // per-bit dirty buffer (same format as epaper buffers): one bit == one pixel + uint8_t *dirtyPixels = nullptr; + size_t dirtyPixelsSize = 0; + uint32_t ghostPixelCount = 0; + uint32_t ghostPixelLimit = EINK_LIMIT_GHOSTING_PX; +#endif + + EpdRotation rotation; + uint32_t previousImageHash = 0; + uint32_t lastUpdateMs = 0; + int fastRefreshCount = 0; +}; + +#endif diff --git a/src/graphics/EmoteRenderer.cpp b/src/graphics/EmoteRenderer.cpp new file mode 100644 index 000000000..6fa0adb4c --- /dev/null +++ b/src/graphics/EmoteRenderer.cpp @@ -0,0 +1,434 @@ +#include "configuration.h" +#if HAS_SCREEN + +#include "graphics/EmoteRenderer.h" +#include +#include + +namespace graphics +{ +namespace EmoteRenderer +{ + +static inline int getStringWidth(OLEDDisplay *display, const char *text, size_t len) +{ +#if defined(OLED_UA) || defined(OLED_RU) + return display->getStringWidth(text, len, true); +#else + (void)len; + return display->getStringWidth(text); +#endif +} + +size_t utf8CharLen(uint8_t c) +{ + if ((c & 0xE0) == 0xC0) + return 2; + if ((c & 0xF0) == 0xE0) + return 3; + if ((c & 0xF8) == 0xF0) + return 4; + return 1; +} + +static inline bool isPossibleEmoteLead(uint8_t c) +{ + // All supported emoji labels in emotes.cpp are currently in these UTF-8 lead ranges. + return c == 0xE2 || c == 0xF0; +} + +static inline int getUtf8ChunkWidth(OLEDDisplay *display, const char *text, size_t len) +{ + char chunk[5] = {0, 0, 0, 0, 0}; + if (len > 4) + len = 4; + memcpy(chunk, text, len); + return getStringWidth(display, chunk, len); +} + +static inline bool isFE0FAt(const char *s, size_t pos, size_t len) +{ + return pos + 2 < len && static_cast(s[pos]) == 0xEF && static_cast(s[pos + 1]) == 0xB8 && + static_cast(s[pos + 2]) == 0x8F; +} + +static inline bool isSkinToneAt(const char *s, size_t pos, size_t len) +{ + return pos + 3 < len && static_cast(s[pos]) == 0xF0 && static_cast(s[pos + 1]) == 0x9F && + static_cast(s[pos + 2]) == 0x8F && + (static_cast(s[pos + 3]) >= 0xBB && static_cast(s[pos + 3]) <= 0xBF); +} + +static inline size_t ignorableModifierLenAt(const char *s, size_t pos, size_t len) +{ + // Skip modifiers that do not change which bitmap we render. + if (isFE0FAt(s, pos, len)) + return 3; + if (isSkinToneAt(s, pos, len)) + return 4; + return 0; +} + +const Emote *findEmoteByLabel(const char *label, const Emote *emoteSet, int emoteCount) +{ + if (!label || !*label) + return nullptr; + + for (int i = 0; i < emoteCount; ++i) { + if (emoteSet[i].label && strcmp(label, emoteSet[i].label) == 0) + return &emoteSet[i]; + } + + return nullptr; +} + +static bool matchAtIgnoringModifiers(const char *text, size_t textLen, size_t pos, const char *label, size_t &textConsumed, + size_t &matchScore) +{ + // Treat FE0F and skin-tone modifiers as optional while matching. + textConsumed = 0; + matchScore = 0; + if (!label || !*label || pos >= textLen) + return false; + + const size_t labelLen = strlen(label); + size_t ti = pos; + size_t li = 0; + + while (true) { + while (ti < textLen) { + const size_t skipLen = ignorableModifierLenAt(text, ti, textLen); + if (!skipLen) + break; + ti += skipLen; + } + while (li < labelLen) { + const size_t skipLen = ignorableModifierLenAt(label, li, labelLen); + if (!skipLen) + break; + li += skipLen; + } + + if (li >= labelLen) { + while (ti < textLen) { + const size_t skipLen = ignorableModifierLenAt(text, ti, textLen); + if (!skipLen) + break; + ti += skipLen; + } + textConsumed = ti - pos; + return textConsumed > 0; + } + + if (ti >= textLen) + return false; + + const uint8_t tc = static_cast(text[ti]); + const uint8_t lc = static_cast(label[li]); + const size_t tlen = utf8CharLen(tc); + const size_t llen = utf8CharLen(lc); + + if (tlen != llen || ti + tlen > textLen || li + llen > labelLen) + return false; + if (memcmp(text + ti, label + li, tlen) != 0) + return false; + + ti += tlen; + li += llen; + matchScore += llen; + } +} + +const Emote *findEmoteAt(const char *text, size_t textLen, size_t pos, size_t &matchLen, const Emote *emoteSet, int emoteCount) +{ + // Prefer the longest matching label at this byte offset. + const Emote *matched = nullptr; + matchLen = 0; + size_t bestScore = 0; + if (!text || pos >= textLen) + return nullptr; + + if (!isPossibleEmoteLead(static_cast(text[pos]))) + return nullptr; + + for (int i = 0; i < emoteCount; ++i) { + const char *label = emoteSet[i].label; + if (!label || !*label) + continue; + if (static_cast(label[0]) != static_cast(text[pos])) + continue; + + const size_t labelLen = strlen(label); + if (labelLen == 0) + continue; + + size_t candidateLen = 0; + size_t candidateScore = 0; + if (pos + labelLen <= textLen && memcmp(text + pos, label, labelLen) == 0) { + candidateLen = labelLen; + candidateScore = labelLen; + } else if (matchAtIgnoringModifiers(text, textLen, pos, label, candidateLen, candidateScore)) { + // Matched with FE0F/skin tone modifiers treated as optional. + } else { + continue; + } + + if (candidateScore > bestScore) { + matched = &emoteSet[i]; + matchLen = candidateLen; + bestScore = candidateScore; + } + } + + return matched; +} + +static LineMetrics analyzeLineInternal(OLEDDisplay *display, const char *line, size_t lineLen, int fallbackHeight, + const Emote *emoteSet, int emoteCount, int emoteSpacing) +{ + // Scan once to collect width and tallest emote for this line. + LineMetrics metrics{0, fallbackHeight, false}; + if (!line) + return metrics; + + for (size_t i = 0; i < lineLen;) { + size_t matchLen = 0; + const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); + if (matched) { + metrics.hasEmote = true; + metrics.tallestHeight = std::max(metrics.tallestHeight, matched->height); + if (display) + metrics.width += matched->width + emoteSpacing; + i += matchLen; + continue; + } + + const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); + if (skipLen) { + i += skipLen; + continue; + } + + const size_t charLen = utf8CharLen(static_cast(line[i])); + if (display) + metrics.width += getUtf8ChunkWidth(display, line + i, charLen); + i += charLen; + } + + return metrics; +} + +LineMetrics analyzeLine(OLEDDisplay *display, const char *line, int fallbackHeight, const Emote *emoteSet, int emoteCount, + int emoteSpacing) +{ + return analyzeLineInternal(display, line, line ? strlen(line) : 0, fallbackHeight, emoteSet, emoteCount, emoteSpacing); +} + +int maxEmoteHeight(const Emote *emoteSet, int emoteCount) +{ + int tallest = 0; + for (int i = 0; i < emoteCount; ++i) { + if (emoteSet[i].label && *emoteSet[i].label) + tallest = std::max(tallest, emoteSet[i].height); + } + return tallest; +} + +int measureStringWithEmotes(OLEDDisplay *display, const char *line, const Emote *emoteSet, int emoteCount, int emoteSpacing) +{ + if (!display) + return 0; + + if (!line || !*line) + return 0; + + return analyzeLine(display, line, 0, emoteSet, emoteCount, emoteSpacing).width; +} + +static int appendTextSpanAndMeasure(OLEDDisplay *display, int cursorX, int fontY, const char *text, size_t len, bool draw, + bool fauxBold) +{ + // Draw plain-text runs in chunks so UTF-8 stays intact. + if (!text || len == 0) + return cursorX; + + char chunk[33]; + size_t pos = 0; + while (pos < len) { + size_t chunkLen = 0; + while (pos + chunkLen < len) { + const size_t charLen = utf8CharLen(static_cast(text[pos + chunkLen])); + if (chunkLen + charLen >= sizeof(chunk)) + break; + chunkLen += charLen; + } + + if (chunkLen == 0) { + chunkLen = std::min(len - pos, sizeof(chunk) - 1); + } + + memcpy(chunk, text + pos, chunkLen); + chunk[chunkLen] = '\0'; + if (draw) { + if (fauxBold) + display->drawString(cursorX + 1, fontY, chunk); + display->drawString(cursorX, fontY, chunk); + } + cursorX += getStringWidth(display, chunk, chunkLen); + pos += chunkLen; + } + + return cursorX; +} + +size_t truncateToWidth(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, const char *ellipsis, + const Emote *emoteSet, int emoteCount, int emoteSpacing) +{ + if (!out || outSize == 0) + return 0; + + out[0] = '\0'; + if (!display || !line || maxWidth <= 0) + return 0; + + const size_t lineLen = strlen(line); + const int suffixWidth = + (ellipsis && *ellipsis) ? measureStringWithEmotes(display, ellipsis, emoteSet, emoteCount, emoteSpacing) : 0; + const char *suffix = (ellipsis && suffixWidth <= maxWidth) ? ellipsis : ""; + const size_t suffixLen = strlen(suffix); + const int availableWidth = maxWidth - (*suffix ? suffixWidth : 0); + + if (measureStringWithEmotes(display, line, emoteSet, emoteCount, emoteSpacing) <= maxWidth) { + strncpy(out, line, outSize - 1); + out[outSize - 1] = '\0'; + return strlen(out); + } + + int used = 0; + size_t cut = 0; + for (size_t i = 0; i < lineLen;) { + // Keep whole emotes together when deciding where to cut. + int tokenWidth = 0; + size_t advance = 0; + + if (isPossibleEmoteLead(static_cast(line[i]))) { + size_t matchLen = 0; + const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); + if (matched) { + tokenWidth = matched->width + emoteSpacing; + advance = matchLen; + } + } + + if (advance == 0) { + const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); + if (skipLen) { + i += skipLen; + cut = i; + continue; + } + + const size_t charLen = utf8CharLen(static_cast(line[i])); + tokenWidth = getUtf8ChunkWidth(display, line + i, charLen); + advance = charLen; + } + + if (used + tokenWidth > availableWidth) + break; + + used += tokenWidth; + i += advance; + cut = i; + } + + if (cut == 0) { + strncpy(out, suffix, outSize - 1); + out[outSize - 1] = '\0'; + return strlen(out); + } + + size_t copyLen = cut; + if (copyLen > outSize - 1) + copyLen = outSize - 1; + if (suffixLen > 0 && copyLen + suffixLen > outSize - 1) { + copyLen = (outSize - 1 > suffixLen) ? (outSize - 1 - suffixLen) : 0; + } + + memcpy(out, line, copyLen); + size_t totalLen = copyLen; + if (suffixLen > 0 && totalLen < outSize - 1) { + memcpy(out + totalLen, suffix, std::min(suffixLen, outSize - 1 - totalLen)); + totalLen += std::min(suffixLen, outSize - 1 - totalLen); + } + out[totalLen] = '\0'; + return totalLen; +} + +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, const Emote *emoteSet, + int emoteCount, int emoteSpacing, bool fauxBold) +{ + if (!line) + return; + + const size_t lineLen = strlen(line); + // Center text vertically when any emote is taller than the font. + const int maxIconHeight = + analyzeLineInternal(nullptr, line, lineLen, fontHeight, emoteSet, emoteCount, emoteSpacing).tallestHeight; + const int lineHeight = std::max(fontHeight, maxIconHeight); + const int fontY = y + (lineHeight - fontHeight) / 2; + + int cursorX = x; + bool inBold = false; + + for (size_t i = 0; i < lineLen;) { + // Toggle faux bold. + if (fauxBold && i + 1 < lineLen && line[i] == '*' && line[i + 1] == '*') { + inBold = !inBold; + i += 2; + continue; + } + + const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); + if (skipLen) { + i += skipLen; + continue; + } + + size_t matchLen = 0; + const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); + if (matched) { + const int iconY = y + (lineHeight - matched->height) / 2; + display->drawXbm(cursorX, iconY, matched->width, matched->height, matched->bitmap); + cursorX += matched->width + emoteSpacing; + i += matchLen; + continue; + } + + size_t next = i; + while (next < lineLen) { + // Stop the text run before the next emote or bold marker. + if (fauxBold && next + 1 < lineLen && line[next] == '*' && line[next + 1] == '*') + break; + + if (ignorableModifierLenAt(line, next, lineLen)) + break; + + size_t nextMatchLen = 0; + if (findEmoteAt(line, lineLen, next, nextMatchLen, emoteSet, emoteCount) != nullptr) + break; + + next += utf8CharLen(static_cast(line[next])); + } + + if (next == i) + next += utf8CharLen(static_cast(line[i])); + + cursorX = appendTextSpanAndMeasure(display, cursorX, fontY, line + i, next - i, true, fauxBold && inBold); + i = next; + } +} + +} // namespace EmoteRenderer +} // namespace graphics + +#endif // HAS_SCREEN diff --git a/src/graphics/EmoteRenderer.h b/src/graphics/EmoteRenderer.h new file mode 100644 index 000000000..93cee4b25 --- /dev/null +++ b/src/graphics/EmoteRenderer.h @@ -0,0 +1,79 @@ +#pragma once +#include "configuration.h" + +#if HAS_SCREEN +#include "graphics/emotes.h" +#include +#include +#include +#include + +namespace graphics +{ +namespace EmoteRenderer +{ + +struct LineMetrics { + int width; + int tallestHeight; + bool hasEmote; +}; + +size_t utf8CharLen(uint8_t c); + +const Emote *findEmoteByLabel(const char *label, const Emote *emoteSet = emotes, int emoteCount = numEmotes); +const Emote *findEmoteAt(const char *text, size_t textLen, size_t pos, size_t &matchLen, const Emote *emoteSet = emotes, + int emoteCount = numEmotes); +inline const Emote *findEmoteAt(const std::string &text, size_t pos, size_t &matchLen, const Emote *emoteSet = emotes, + int emoteCount = numEmotes) +{ + return findEmoteAt(text.c_str(), text.length(), pos, matchLen, emoteSet, emoteCount); +} + +LineMetrics analyzeLine(OLEDDisplay *display, const char *line, int fallbackHeight = 0, const Emote *emoteSet = emotes, + int emoteCount = numEmotes, int emoteSpacing = 1); +inline LineMetrics analyzeLine(OLEDDisplay *display, const std::string &line, int fallbackHeight = 0, + const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1) +{ + return analyzeLine(display, line.c_str(), fallbackHeight, emoteSet, emoteCount, emoteSpacing); +} +int maxEmoteHeight(const Emote *emoteSet = emotes, int emoteCount = numEmotes); + +int measureStringWithEmotes(OLEDDisplay *display, const char *line, const Emote *emoteSet = emotes, int emoteCount = numEmotes, + int emoteSpacing = 1); +inline int measureStringWithEmotes(OLEDDisplay *display, const std::string &line, const Emote *emoteSet = emotes, + int emoteCount = numEmotes, int emoteSpacing = 1) +{ + return measureStringWithEmotes(display, line.c_str(), emoteSet, emoteCount, emoteSpacing); +} +size_t truncateToWidth(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, + const char *ellipsis = "...", const Emote *emoteSet = emotes, int emoteCount = numEmotes, + int emoteSpacing = 1); +inline std::string truncateToWidth(OLEDDisplay *display, const std::string &line, int maxWidth, + const std::string &ellipsis = "...", const Emote *emoteSet = emotes, + int emoteCount = numEmotes, int emoteSpacing = 1) +{ + if (!display || maxWidth <= 0) + return ""; + if (measureStringWithEmotes(display, line.c_str(), emoteSet, emoteCount, emoteSpacing) <= maxWidth) + return line; + + std::vector out(line.length() + ellipsis.length() + 1, '\0'); + truncateToWidth(display, line.c_str(), out.data(), out.size(), maxWidth, ellipsis.c_str(), emoteSet, emoteCount, + emoteSpacing); + return std::string(out.data()); +} + +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, const Emote *emoteSet = emotes, + int emoteCount = numEmotes, int emoteSpacing = 1, bool fauxBold = true); +inline void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, int fontHeight, + const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1, + bool fauxBold = true) +{ + drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSet, emoteCount, emoteSpacing, fauxBold); +} + +} // namespace EmoteRenderer +} // namespace graphics + +#endif // HAS_SCREEN diff --git a/src/graphics/NeoPixel.h b/src/graphics/NeoPixel.h deleted file mode 100644 index dde74366e..000000000 --- a/src/graphics/NeoPixel.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifdef HAS_NEOPIXEL -#include -extern Adafruit_NeoPixel pixels; -#endif \ No newline at end of file diff --git a/src/graphics/NomadStarLED.h b/src/graphics/NomadStarLED.h index 0633a577e..6633db0c8 100644 --- a/src/graphics/NomadStarLED.h +++ b/src/graphics/NomadStarLED.h @@ -1,4 +1,6 @@ #ifdef HAS_LP5562 +#include + #include extern LP5562 rgbw; diff --git a/src/graphics/RAKled.h b/src/graphics/RAKled.h deleted file mode 100644 index 659ea9b72..000000000 --- a/src/graphics/RAKled.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifdef HAS_NCP5623 -#include -extern NCP5623 rgb; - -#endif \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 2468fafb0..5d5ed3686 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -27,6 +27,7 @@ along with this program. If not, see . #include "configuration.h" #include "meshUtils.h" #if HAS_SCREEN +#include "EInkParallelDisplay.h" #include #include "DisplayFormatters.h" @@ -365,12 +366,14 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O defined(HACKADAY_COMMUNICATOR) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY) dispdev = new EInkDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_EINK_PARALLELDISPLAY) + dispdev = new EInkParallelDisplay(EPD_WIDTH, EPD_HEIGHT, EInkParallelDisplay::EPD_ROT_PORTRAIT); #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -434,12 +437,15 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) PMU->enablePowerOutput(XPOWERS_ALDO2); #endif -#if defined(MUZI_BASE) +// some screens seem to need a kick in the pants to turn back on +#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV) dispdev->init(); dispdev->setBrightness(brightness); dispdev->flipScreenVertically(); dispdev->resetDisplay(); +#ifdef SCREEN_12V_ENABLE digitalWrite(SCREEN_12V_ENABLE, HIGH); +#endif delay(100); #endif #if !ARCH_PORTDUINO @@ -463,9 +469,11 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) #if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2) ui->init(); #endif -#ifdef USE_ST7789 +#if defined(USE_ST7789) && defined(VTFT_LEDA) +#ifdef VTFT_CTRL pinMode(VTFT_CTRL, OUTPUT); digitalWrite(VTFT_CTRL, LOW); +#endif ui->init(); #ifdef ESP_PLATFORM analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); @@ -507,8 +515,12 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) #ifdef USE_ST7789 SPI1.end(); #if defined(ARCH_ESP32) +#ifdef VTFT_LEDA pinMode(VTFT_LEDA, ANALOG); +#endif +#ifdef VTFT_CTRL pinMode(VTFT_CTRL, ANALOG); +#endif pinMode(ST7789_RESET, ANALOG); pinMode(ST7789_RS, ANALOG); pinMode(ST7789_NSS, ANALOG); @@ -752,7 +764,11 @@ void Screen::forceDisplay(bool forceUiUpdate) } // Tell EInk class to update the display +#if defined(USE_EINK_PARALLELDISPLAY) + static_cast(dispdev)->forceDisplay(); +#elif defined(USE_EINK) static_cast(dispdev)->forceDisplay(); +#endif #else // No delay between UI frame rendering if (forceUiUpdate) { @@ -877,6 +893,10 @@ int32_t Screen::runOnce() break; case Cmd::STOP_ALERT_FRAME: NotificationRenderer::pauseBanner = false; + // Return from one-off alert mode back to regular frames. + if (!showingNormalScreen && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + setFrames(); + } break; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame @@ -987,8 +1007,10 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) ui->update(); } while (ui->getUiState()->lastUpdate < startUpdate); +#if defined(USE_EINK_PARALLELDISPLAY) + static_cast(dispdev)->forceDisplay(0); +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) // Old EInkDisplay class -#if !defined(USE_EINK_DYNAMICDISPLAY) static_cast(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit #endif @@ -1000,7 +1022,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) #ifdef EINK_HASQUIRK_GHOSTING EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused" #else - EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh + EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh #endif } #endif @@ -1177,7 +1199,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); } } @@ -1206,7 +1228,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) { diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 31ddf1c84..e259f7691 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -765,7 +765,11 @@ class Screen : public concurrency::OSThread DebugInfo debugInfo; /// Display device +#ifdef USE_ST7789 + ST7789Spi *dispdev; +#else OLEDDisplay *dispdev; +#endif /// UI helper for rendering to frames and switching between them OLEDDisplayUi *ui; diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index 5dead69ae..a3c2c5354 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -20,7 +20,7 @@ #include "graphics/fonts/OLEDDisplayFontsGR.h" #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) +#if (defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)) && defined(USE_EINK) #include "graphics/fonts/EinkDisplayFonts.h" #endif @@ -106,7 +106,7 @@ #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) +#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO) #undef FONT_SMALL #undef FONT_MEDIUM #undef FONT_LARGE diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 8f06fcf9f..ec50654ae 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -121,11 +121,10 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } // === Screen Title === - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(SCREEN_WIDTH / 2, y, titleStr); - if (config.display.heading_bold) { - display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); - } + const char *headerTitle = titleStr ? titleStr : ""; + const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle); + const int titleX = (SCREEN_WIDTH - titleWidth) / 2; + UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold); } display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -221,7 +220,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti if (rtc_sec > 0) { // === Build Time String === - long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; int hour, minute, second; graphics::decomposeTime(rtc_sec, hour, minute, second); snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); @@ -428,39 +426,33 @@ const int *getTextPositions(OLEDDisplay *display) // ************************* void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) { - bool drawConnectionState = false; - if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI || - service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET || - service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) { - drawConnectionState = true; - } + if (!isAPIConnected(service->api_state)) + return; - if (drawConnectionState) { - const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; - display->setColor(BLACK); - display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), - (connection_icon_height * scale) + (2 * scale)); - display->setColor(WHITE); - if (currentResolution == ScreenResolution::High) { - const int bytesPerRow = (connection_icon_width + 7) / 8; - int iconX = 0; - int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); + const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; + display->setColor(BLACK); + display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), + (connection_icon_height * scale) + (2 * scale)); + display->setColor(WHITE); + if (currentResolution == ScreenResolution::High) { + const int bytesPerRow = (connection_icon_width + 7) / 8; + int iconX = 0; + int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); - for (int yy = 0; yy < connection_icon_height; ++yy) { - const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; - for (int xx = 0; xx < connection_icon_width; ++xx) { - const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); - const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first - if (byteVal & bitMask) { - display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); - } + for (int yy = 0; yy < connection_icon_height; ++yy) { + const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; + for (int xx = 0; xx < connection_icon_width; ++xx) { + const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); + const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first + if (byteVal & bitMask) { + display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); } } - - } else { - display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, - connection_icon); } + + } else { + display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, + connection_icon); } } @@ -522,4 +514,4 @@ std::string sanitizeString(const std::string &input) } } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index a8ecdfada..35e767056 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -63,4 +63,18 @@ bool isAllowedPunctuation(char c); std::string sanitizeString(const std::string &input); +static inline bool isAPIConnected(uint8_t state) +{ + static constexpr bool connectedStates[] = { + /* STATE_NONE */ false, + /* STATE_BLE */ true, + /* STATE_WIFI */ true, + /* STATE_SERIAL */ true, + /* STATE_PACKET */ true, + /* STATE_HTTP */ true, + /* STATE_ETH */ true, + }; + return state < sizeof(connectedStates) ? connectedStates[state] : false; +} + } // namespace graphics diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 000239886..f47e51720 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1312,8 +1312,8 @@ void TFTDisplay::display(bool fromBlank) bool somethingChanged = false; // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step - colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); - colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); + colorTftMesh = __builtin_bswap16(TFT_MESH); + colorTftBlack = __builtin_bswap16(TFT_BLACK); y = 0; while (y < displayHeight) { @@ -1357,14 +1357,14 @@ void TFTDisplay::display(bool fromBlank) // Did we find a pixel that needs updating on this row? if (x_FirstPixelUpdate < displayWidth) { + // Align the first pixel for update to an even number so the total alignment of + // the data will be at 32-bit boundary, which is required by GDMA SPI transfers. + x_FirstPixelUpdate &= ~1; - // Quickly write out the first changed pixel (saves another array lookup) - linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; - x_LastPixelUpdate = x_FirstPixelUpdate; - - // Step 3: copy all remaining pixels in this row into the pixel line buffer, - // while also recording the last pixel in the row that needs updating - for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { + // Step 3a: copy rest of the pixels in this row into the pixel line buffer, + // while also recording the last pixel in the row that needs updating. + // Since the first changed pixel will be looked up, the x_LastPixelUpdate will be set. + for (x = x_FirstPixelUpdate; x < displayWidth; x++) { isset = buffer[x + y_byteIndex] & y_byteMask; linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; @@ -1377,6 +1377,14 @@ void TFTDisplay::display(bool fromBlank) x_LastPixelUpdate = x; } } + // Step 3b: Round up the last pixel to odd number to maintain 32-bit alignment for SPIs. + // Most displays will have even number of pixels in a row -- this will be in bounds + // of the displayWidth. (Hopefully odd displays will just ignore that extra pixel.) + x_LastPixelUpdate |= 1; + // Ensure the last pixel index does not exceed the display width. + if (x_LastPixelUpdate >= displayWidth) { + x_LastPixelUpdate = displayWidth - 1; + } #if defined(HACKADAY_COMMUNICATOR) tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); @@ -1451,7 +1459,7 @@ void TFTDisplay::sendCommand(uint8_t com) digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOn(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) tft->wakeup(); tft->powerSaveOff(); #endif @@ -1462,8 +1470,9 @@ void TFTDisplay::sendCommand(uint8_t com) #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif -#ifdef RAK14014 -#elif !defined(M5STACK) && !defined(ST7789_CS) && !defined(HACKADAY_COMMUNICATOR) +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) +#elif !defined(M5STACK) && !defined(ST7789_CS) && \ + !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function tft->setBrightness(172); #endif break; @@ -1477,7 +1486,7 @@ void TFTDisplay::sendCommand(uint8_t com) digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOff(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) tft->sleep(); tft->powerSaveOn(); #endif @@ -1488,7 +1497,7 @@ void TFTDisplay::sendCommand(uint8_t com) #ifdef UNPHONE unphone.backlight(false); // using unPhone library #endif -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) #elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(0); #endif @@ -1503,7 +1512,7 @@ void TFTDisplay::sendCommand(uint8_t com) void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) // todo #elif !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(_brightness); @@ -1523,7 +1532,7 @@ bool TFTDisplay::hasTouch(void) { #ifdef RAK14014 return true; -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) return tft->touch() != nullptr; #else return false; @@ -1542,7 +1551,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y) } else { return false; } -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) return tft->getTouch(x, y); #else return false; @@ -1559,7 +1568,7 @@ bool TFTDisplay::connect() { concurrency::LockGuard g(spiLock); LOG_INFO("Do TFT init"); -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) tft = new TFT_eSPI; #elif defined(HACKADAY_COMMUNICATOR) bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); @@ -1596,7 +1605,7 @@ bool TFTDisplay::connect() ft6336u.begin(); pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); -#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) +#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) || defined(HELTEC_MESH_NODE_T096) tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) tft->setRotation(2); // T-Watch S3 left-handed orientation diff --git a/src/graphics/TimeFormatters.cpp b/src/graphics/TimeFormatters.cpp index 0a1c23341..02450efa3 100644 --- a/src/graphics/TimeFormatters.cpp +++ b/src/graphics/TimeFormatters.cpp @@ -110,14 +110,14 @@ void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, ui uint32_t secs = (uptimeMillis % 60000) / 1000; if (days) { - snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours); + snprintf(uptimeStr, maxLength, "%s%ud %uh", prefix, days, hours); } else if (hours) { - snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins); + snprintf(uptimeStr, maxLength, "%s%uh %um", prefix, hours, mins); } else if (!includeSecs) { - snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins); + snprintf(uptimeStr, maxLength, "%s%um", prefix, mins); } else if (mins) { - snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs); + snprintf(uptimeStr, maxLength, "%s%um %us", prefix, mins, secs); } else { - snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs); + snprintf(uptimeStr, maxLength, "%s%us", prefix, secs); } -} +} \ No newline at end of file diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index a24f5b15c..52f0195b3 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -429,6 +429,10 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool c = c - 'a' + 'A'; } keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); + // Show the common "/" pairing next to "?" like on a real keyboard + if (key.type == VK_CHAR && key.character == '?') { + keyText = "?/"; + } } int textWidth = display->getStringWidth(keyText.c_str()); @@ -518,9 +522,13 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) char c = key.character; - // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings - if (isLongPress && c >= 'a' && c <= 'z') { - c = (char)(c - 'a' + 'A'); + // Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard + if (isLongPress) { + if (c >= 'a' && c <= 'z') { + c = (char)(c - 'a' + 'A'); + } else if (c == '?') { + c = '/'; + } } return c; diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 811241eec..98e907255 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -408,7 +408,16 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(nameX, getTextPositions(display)[line++], device_role); // === Third Row: Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + // For custom modem settings show the actual parameters; for presets use the preset name. + char modeStr[16]; + if (!config.lora.use_preset) { + snprintf(modeStr, sizeof(modeStr), "BW%u-SF%u-CR%u", static_cast(config.lora.bandwidth), + static_cast(config.lora.spread_factor), static_cast(config.lora.coding_rate)); + } else { + strncpy(modeStr, DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, true), + sizeof(modeStr) - 1); + modeStr[sizeof(modeStr) - 1] = '\0'; + } char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; @@ -416,7 +425,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, if (currentResolution == ScreenResolution::UltraLow) { snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); } else { - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, modeStr); } } textWidth = display->getStringWidth(regionradiopreset); @@ -535,6 +544,9 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x #ifndef T_DECK_PRO barsOffset -= 12; #endif +#if defined(T5_S3_EPAPER_PRO) + barsOffset += 60; +#endif #endif int barX = x + barsOffset; if (currentResolution == ScreenResolution::UltraLow) { @@ -584,11 +596,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); uint32_t heapTotal = memGet.getHeapSize(); - uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); - uint32_t psramTotal = memGet.getPsramSize(); - uint32_t flashUsed = 0, flashTotal = 0; #ifdef ESP32 +#ifndef T5_S3_EPAPER_PRO + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); +#endif flashUsed = FSCom.usedBytes(); flashTotal = FSCom.totalBytes(); #endif @@ -607,10 +620,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); #ifdef ESP32 +#ifndef T5_S3_EPAPER_PRO if (psramUsed > 0) { line += 1; drawUsageRow("PSRAM:", psramUsed, psramTotal); } +#endif if (flashTotal > 0) { line += 1; drawUsageRow("Flash:", flashUsed, flashTotal); @@ -663,7 +678,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it char uptimeStr[32] = ""; - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr)); textWidth = display->getStringWidth(uptimeStr); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], uptimeStr); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 6d29e9f7f..e92ba4839 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -18,6 +18,7 @@ #include "main.h" #include "mesh/Default.h" #include "mesh/MeshTypes.h" +#include "mesh/RadioLibInterface.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/ExternalNotificationModule.h" @@ -25,6 +26,7 @@ #include "modules/TraceRouteModule.h" #include #include +#include #include #include @@ -58,7 +60,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp } // namespace -menuHandler::screenMenus menuHandler::menuQueue = menu_none; +menuHandler::screenMenus menuHandler::menuQueue = MenuNone; uint32_t menuHandler::pickedNodeNum = 0; bool test_enabled = false; uint8_t test_count = 0; @@ -66,7 +68,7 @@ uint8_t test_count = 0; void menuHandler::loraMenu() { static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"}; - enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 }; + enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; @@ -74,14 +76,14 @@ void menuHandler::loraMenu() bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action - } else if (selected == device_role_picker) { - menuHandler::menuQueue = menuHandler::device_role_picker; - } else if (selected == radio_preset_picker) { - menuHandler::menuQueue = menuHandler::radio_preset_picker; - } else if (selected == frequency_slot) { - menuHandler::menuQueue = menuHandler::frequency_slot; - } else if (selected == lora_picker) { - menuHandler::menuQueue = menuHandler::lora_picker; + } else if (selected == DeviceRolePicker) { + menuHandler::menuQueue = menuHandler::DeviceRolePicker; + } else if (selected == RadioPresetPicker) { + menuHandler::menuQueue = menuHandler::RadioPresetPicker; + } else if (selected == FrequencySlot) { + menuHandler::menuQueue = menuHandler::FrequencySlot; + } else if (selected == LoraPicker) { + menuHandler::menuQueue = menuHandler::LoraPicker; } }; screen->showOverlayBanner(bannerOptions); @@ -102,7 +104,7 @@ void menuHandler::OnboardMessage() bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { - menuHandler::menuQueue = menuHandler::no_timeout_lora_picker; + menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker; screen->runNow(); }; screen->showOverlayBanner(bannerOptions); @@ -159,31 +161,22 @@ void menuHandler::LoraRegionPicker(uint32_t duration) return; } + // Guard: without a reboot, reconfigure() applies the region directly. + // Reject LORA_24 on sub-GHz-only hardware — getRadio() used to catch this post-reboot. + // TODO: change this to either use the validateLoraConfig() logic or at least check the region for wideLora + // rather than a hardcoded check for LORA_24. + if (selectedRegion == meshtastic_Config_LoRaConfig_RegionCode_LORA_24 && + !(RadioLibInterface::instance && RadioLibInterface::instance->wideLora())) { + LOG_WARN("Radio hardware does not support 2.4 GHz; ignoring region selection"); + return; + } + config.lora.region = selectedRegion; auto changes = SEGMENT_CONFIG; - // FIXME: This should be a method consolidated with the same logic in the admin message as well - // This is needed as we wait til picking the LoRa region to generate keys for the first time. #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - // public key is derived from private, so this will always have the same result. - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } + if (crypto) { + crypto->ensurePkiKeys(config.security, owner); } #endif config.lora.tx_enabled = true; @@ -199,7 +192,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration) } service->reloadConfig(changes); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }); bannerOptions.durationMs = duration; @@ -216,7 +208,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration) screen->showOverlayBanner(bannerOptions); } -void menuHandler::DeviceRolePicker() +void menuHandler::deviceRolePicker() { static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; enum optionsNumbers { @@ -232,7 +224,7 @@ void menuHandler::DeviceRolePicker() bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } else if (selected == devicerole_client) { @@ -265,13 +257,24 @@ void menuHandler::FrequencySlotPicker() optionsEnumArray[options++] = 0; // Calculate number of channels (copied from RadioInterface::applyModemConfig()) + meshtastic_Config_LoRaConfig &loraConfig = config.lora; double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora) : bwCodeToKHz(loraConfig.bandwidth); uint32_t numChannels = 0; if (myRegion) { - numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0))); + // Match RadioInterface::applyModemConfig(): include padding, add spacing in numerator, and use round() + const double spacing = myRegion->profile->spacing; + const double padding = myRegion->profile->padding; + const double channelBandwidthMHz = bw / 1000.0; + const double numerator = (myRegion->freqEnd - myRegion->freqStart) + spacing; + const double denominator = spacing + (padding * 2) + channelBandwidthMHz; + if (denominator > 0.0) { + numChannels = static_cast(round(numerator / denominator)); + } else { + LOG_WARN("Invalid region configuration: non-positive channel spacing/width"); + } } else { LOG_WARN("Region not set, cannot calculate number of channels"); return; @@ -300,20 +303,19 @@ void menuHandler::FrequencySlotPicker() bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } config.lora.channel_num = selected; service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }; screen->showOverlayBanner(bannerOptions); } -void menuHandler::RadioPresetPicker() +void menuHandler::radioPresetPicker() { static const RadioPresetOption presetOptions[] = { {"Back", OptionsAction::Back}, @@ -333,7 +335,7 @@ void menuHandler::RadioPresetPicker() auto bannerOptions = createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } @@ -346,13 +348,12 @@ void menuHandler::RadioPresetPicker() config.lora.channel_num = 0; // Reset to default channel for the preset config.lora.override_frequency = 0; // Clear any custom frequency service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }); screen->showOverlayBanner(bannerOptions); } -void menuHandler::TwelveHourPicker() +void menuHandler::twelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; @@ -362,7 +363,7 @@ void menuHandler::TwelveHourPicker() bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); } else if (selected == twelve) { config.display.use_12h_clock = true; @@ -390,7 +391,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::functionshowOverlayBanner(confirmBanner); } -void menuHandler::ClockFacePicker() +void menuHandler::clockFacePicker() { static const ClockFaceOption clockFaceOptions[] = { {"Back", OptionsAction::Back}, @@ -404,7 +405,7 @@ void menuHandler::ClockFacePicker() auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, [](const ClockFaceOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); return; } @@ -456,7 +457,7 @@ void menuHandler::TZPicker() auto bannerOptions = createStaticBannerOptions( "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); return; } @@ -503,13 +504,13 @@ void menuHandler::clockMenu() bannerOptions.optionsCount = 4; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Clock) { - menuHandler::menuQueue = menuHandler::clock_face_picker; + menuHandler::menuQueue = menuHandler::ClockFacePicker; screen->runNow(); } else if (selected == Time) { - menuHandler::menuQueue = menuHandler::twelve_hour_picker; + menuHandler::menuQueue = menuHandler::TwelveHourPicker; screen->runNow(); } else if (selected == Timezone) { - menuHandler::menuQueue = menuHandler::TZ_picker; + menuHandler::menuQueue = menuHandler::TzPicker; screen->runNow(); } }; @@ -539,7 +540,7 @@ void menuHandler::messageResponseMenu() // If viewing ALL chats, hide “Mute Chat” if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) { const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex(); - auto &chan = channels.getByIndex(chIndex); + const auto &chan = channels.getByIndex(chIndex); optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel"; optionsEnumArray[options++] = MuteChannel; @@ -572,12 +573,12 @@ void menuHandler::messageResponseMenu() LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); if (selected == ViewMode) { - menuHandler::menuQueue = menuHandler::message_viewmode_menu; + menuHandler::menuQueue = menuHandler::MessageViewModeMenu; screen->runNow(); // Reply submenu } else if (selected == ReplyMenu) { - menuHandler::menuQueue = menuHandler::reply_menu; + menuHandler::menuQueue = menuHandler::ReplyMenu; screen->runNow(); } else if (selected == MuteChannel) { @@ -589,7 +590,7 @@ void menuHandler::messageResponseMenu() } } else if (selected == DeleteMenu) { - menuHandler::menuQueue = menuHandler::delete_messages_menu; + menuHandler::menuQueue = menuHandler::DeleteMessagesMenu; screen->runNow(); #ifdef HAS_I2S @@ -649,7 +650,7 @@ void menuHandler::replyMenu() uint32_t peer = graphics::MessageRenderer::getThreadPeer(); if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); return; } @@ -737,7 +738,7 @@ void menuHandler::deleteMessagesMenu() uint32_t peer = graphics::MessageRenderer::getThreadPeer(); if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); return; } @@ -831,7 +832,7 @@ void menuHandler::messageViewModeMenu() // Gather unique peers auto dms = messageStore.getDirectMessages(); std::vector uniquePeers; - for (auto &m : dms) { + for (const auto &m : dms) { uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) uniquePeers.push_back(peer); @@ -901,7 +902,7 @@ void menuHandler::messageViewModeMenu() bannerOptions.bannerCallback = [=](int selected) -> void { LOG_DEBUG("messageViewModeMenu: selected=%d", selected); if (selected == -1) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); } else if (selected == -2) { graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); @@ -1083,23 +1084,23 @@ void menuHandler::systemBaseMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Notifications) { - menuHandler::menuQueue = menuHandler::buzzermodemenupicker; + menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker; screen->runNow(); } else if (selected == ScreenOptions) { - menuHandler::menuQueue = menuHandler::screen_options_menu; + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } else if (selected == PowerMenu) { - menuHandler::menuQueue = menuHandler::power_menu; + menuHandler::menuQueue = menuHandler::PowerMenu; screen->runNow(); } else if (selected == Test) { - menuHandler::menuQueue = menuHandler::test_menu; + menuHandler::menuQueue = menuHandler::TestMenu; screen->runNow(); } else if (selected == Bluetooth) { - menuQueue = bluetooth_toggle_menu; + menuQueue = BluetoothToggleMenu; screen->runNow(); #if HAS_WIFI && !defined(ARCH_PORTDUINO) } else if (selected == WiFiToggle) { - menuQueue = wifi_toggle_menu; + menuQueue = WifiToggleMenu; screen->runNow(); #endif } else if (selected == Back && !test_enabled) { @@ -1177,7 +1178,7 @@ void menuHandler::favoriteBaseMenu() evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; screen->handleUIFrameEvent(&evt); } else if (selected == Remove) { - menuHandler::menuQueue = menuHandler::remove_favorite; + menuHandler::menuQueue = menuHandler::RemoveFavorite; screen->runNow(); } else if (selected == TraceRoute) { if (traceRouteModule) { @@ -1222,9 +1223,11 @@ void menuHandler::positionBaseMenu() }; constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); - constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array baseLabels{}; +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array calibrateLabels{}; +#endif auto onSelection = [](const PositionMenuOption &option, int) -> void { if (option.action == OptionsAction::Back) { @@ -1238,43 +1241,49 @@ void menuHandler::positionBaseMenu() auto action = static_cast(option.value); switch (action) { case PositionAction::GpsToggle: - menuQueue = gps_toggle_menu; + menuQueue = GpsToggleMenu; screen->runNow(); break; case PositionAction::GpsFormat: - menuQueue = gps_format_menu; + menuQueue = GpsFormatMenu; screen->runNow(); break; case PositionAction::CompassMenu: - menuQueue = compass_point_north_menu; + menuQueue = CompassPointNorthMenu; screen->runNow(); break; case PositionAction::CompassCalibrate: +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER if (accelerometerThread) { accelerometerThread->calibrate(30); } +#endif break; case PositionAction::GPSSmartPosition: - menuQueue = gps_smart_position_menu; + menuQueue = GpsSmartPositionMenu; screen->runNow(); break; case PositionAction::GPSUpdateInterval: - menuQueue = gps_update_interval_menu; + menuQueue = GpsUpdateIntervalMenu; screen->runNow(); break; case PositionAction::GPSPositionBroadcast: - menuQueue = gps_position_broadcast_menu; + menuQueue = GpsPositionBroadcastMenu; screen->runNow(); break; } }; BannerOverlayOptions bannerOptions; +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER if (accelerometerThread) { bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); } else { bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); } +#else + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); +#endif screen->showOverlayBanner(bannerOptions); } @@ -1303,13 +1312,13 @@ void menuHandler::nodeListMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == NodePicker) { - menuQueue = NodePicker_menu; + menuQueue = NodePickerMenu; screen->runNow(); } else if (selected == Reset) { - menuQueue = reset_node_db_menu; + menuQueue = ResetNodeDbMenu; screen->runNow(); } else if (selected == NodeNameLength) { - menuHandler::menuQueue = menuHandler::node_name_length_menu; + menuHandler::menuQueue = menuHandler::NodeNameLengthMenu; screen->runNow(); } }; @@ -1330,12 +1339,12 @@ void menuHandler::NodePicker() menuHandler::pickedNodeNum = nodenum; // Keep UI favorite context in sync (used elsewhere for some node-based actions) graphics::UIRenderer::currentFavoriteNodeNum = nodenum; - menuQueue = Manage_Node_menu; + menuQueue = ManageNodeMenu; screen->runNow(); }); } -void menuHandler::ManageNodeMenu() +void menuHandler::manageNodeMenu() { // If we don't have a node selected yet, go fast exit auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum); @@ -1391,13 +1400,13 @@ void menuHandler::ManageNodeMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); return; } if (selected == Favorite) { - auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + const auto *n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); if (!n) { return; } @@ -1483,7 +1492,7 @@ void menuHandler::nodeNameLengthMenu() auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, [](const NodeNameOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); return; } @@ -1498,6 +1507,7 @@ void menuHandler::nodeNameLengthMenu() config.display.use_long_node_name = option.value; saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); LOG_INFO("Setting names to %s", option.value ? "long" : "short"); }); @@ -1528,7 +1538,7 @@ void menuHandler::resetNodeDBMenu() nodeDB->resetNodes(1); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } else if (selected == 0) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); } }; @@ -1550,7 +1560,7 @@ void menuHandler::compassNorthMenu() auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, [](const CompassOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1595,7 +1605,7 @@ void menuHandler::GPSToggleMenu() auto bannerOptions = createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1660,7 +1670,7 @@ void menuHandler::GPSFormatMenu() auto onSelection = [](const GPSFormatOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1715,7 +1725,7 @@ void menuHandler::GPSSmartPositionMenu() bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.position_broadcast_smart_enabled = true; @@ -1744,7 +1754,7 @@ void menuHandler::GPSUpdateIntervalMenu() bannerOptions.optionsCount = 16; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.gps_update_interval = 8; @@ -1832,7 +1842,7 @@ void menuHandler::GPSPositionBroadcastMenu() bannerOptions.optionsCount = 17; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.position_broadcast_secs = 60; @@ -1915,7 +1925,7 @@ void menuHandler::GPSPositionBroadcastMenu() #endif -void menuHandler::BluetoothToggleMenu() +void menuHandler::bluetoothToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; @@ -2043,7 +2053,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) auto bannerOptions = createStaticBannerOptions( "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); return; } @@ -2138,7 +2148,7 @@ void menuHandler::rebootMenu() messageStore.saveToFlash(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } else { - menuQueue = power_menu; + menuQueue = PowerMenu; screen->runNow(); } }; @@ -2160,7 +2170,7 @@ void menuHandler::shutdownMenu() InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else { - menuQueue = power_menu; + menuQueue = PowerMenu; screen->runNow(); } }; @@ -2203,9 +2213,9 @@ void menuHandler::traceRouteMenu() void menuHandler::testMenu() { - enum optionsNumbers { Back, NumberPicker, ShowChirpy }; - static const char *optionsArray[4] = {"Back"}; - static int optionsEnumArray[4] = {Back}; + enum optionsNumbers { Back, NumberPicker, ShowChirpy, TestAnnounce }; + static const char *optionsArray[5] = {"Back"}; + static int optionsEnumArray[5] = {Back}; int options = 1; optionsArray[options] = "Number Picker"; @@ -2213,6 +2223,10 @@ void menuHandler::testMenu() optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; optionsEnumArray[options++] = ShowChirpy; +#ifdef HAS_I2S + optionsArray[options] = "Test Announce"; + optionsEnumArray[options++] = TestAnnounce; +#endif BannerOverlayOptions bannerOptions; bannerOptions.message = "Hidden Test Menu"; @@ -2221,14 +2235,18 @@ void menuHandler::testMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == NumberPicker) { - menuQueue = number_test; + menuQueue = NumberTest; screen->runNow(); } else if (selected == ShowChirpy) { screen->toggleFrameVisibility("chirpy"); screen->setFrames(Screen::FOCUS_SYSTEM); + } else if (selected == TestAnnounce) { +#ifdef HAS_I2S + audioThread->readAloud("This is a test of the emergency broadcast system. This is only a test."); +#endif } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2252,7 +2270,7 @@ void menuHandler::wifiBaseMenu() bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Wifi_toggle) { - menuQueue = wifi_toggle_menu; + menuQueue = WifiToggleMenu; screen->runNow(); } }; @@ -2291,19 +2309,18 @@ void menuHandler::wifiToggleMenu() void menuHandler::screenOptionsMenu() { // Check if brightness is supported - bool hasSupportBrightness = false; -#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) - hasSupportBrightness = true; -#endif - #if defined(T_DECK) // TDeck Doesn't seem to support brightness at all, at least not reliably - hasSupportBrightness = false; + bool hasSupportBrightness = false; +#elif defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) + bool hasSupportBrightness = true; +#else + bool hasSupportBrightness = false; #endif - enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; - static const char *optionsArray[5] = {"Back"}; - static int optionsEnumArray[5] = {Back}; + enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles }; + static const char *optionsArray[6] = {"Back"}; + static int optionsEnumArray[6] = {Back}; int options = 1; // Only show brightness for B&W displays @@ -2325,6 +2342,9 @@ void menuHandler::screenOptionsMenu() optionsArray[options] = "Display Units"; optionsEnumArray[options++] = DisplayUnits; + optionsArray[options] = "Message Bubbles"; + optionsEnumArray[options++] = MessageBubbles; + BannerOverlayOptions bannerOptions; bannerOptions.message = "Display Options"; bannerOptions.optionsArrayPtr = optionsArray; @@ -2332,10 +2352,10 @@ void menuHandler::screenOptionsMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Brightness) { - menuHandler::menuQueue = menuHandler::brightness_picker; + menuHandler::menuQueue = menuHandler::BrightnessPicker; screen->runNow(); } else if (selected == ScreenColor) { - menuHandler::menuQueue = menuHandler::tftcolormenupicker; + menuHandler::menuQueue = menuHandler::TftColorMenuPicker; screen->runNow(); } else if (selected == FrameToggles) { menuHandler::menuQueue = menuHandler::FrameToggles; @@ -2343,8 +2363,11 @@ void menuHandler::screenOptionsMenu() } else if (selected == DisplayUnits) { menuHandler::menuQueue = menuHandler::DisplayUnits; screen->runNow(); + } else if (selected == MessageBubbles) { + menuHandler::menuQueue = menuHandler::MessageBubblesMenu; + screen->runNow(); } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2380,16 +2403,16 @@ void menuHandler::powerMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Reboot) { - menuHandler::menuQueue = menuHandler::reboot_menu; + menuHandler::menuQueue = menuHandler::RebootMenu; screen->runNow(); } else if (selected == Shutdown) { - menuHandler::menuQueue = menuHandler::shutdown_menu; + menuHandler::menuQueue = menuHandler::ShutdownMenu; screen->runNow(); } else if (selected == MUI) { - menuHandler::menuQueue = menuHandler::mui_picker; + menuHandler::menuQueue = menuHandler::MuiPicker; screen->runNow(); } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2427,7 +2450,7 @@ void menuHandler::keyVerificationFinalPrompt() } } -void menuHandler::FrameToggles_menu() +void menuHandler::frameTogglesMenu() { enum optionsNumbers { Finish, @@ -2437,7 +2460,7 @@ void menuHandler::FrameToggles_menu() nodelist_hopsignal, nodelist_distance, nodelist_bearings, - gps, + gps_position, lora, clock, show_favorites, @@ -2475,7 +2498,7 @@ void menuHandler::FrameToggles_menu() #endif optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; - optionsEnumArray[options++] = gps; + optionsEnumArray[options++] = gps_position; #endif optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; @@ -2538,7 +2561,7 @@ void menuHandler::FrameToggles_menu() screen->toggleFrameVisibility("nodelist_bearings"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); - } else if (selected == gps) { + } else if (selected == gps_position) { screen->toggleFrameVisibility("gps"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); @@ -2571,7 +2594,7 @@ void menuHandler::FrameToggles_menu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::DisplayUnits_menu() +void menuHandler::displayUnitsMenu() { enum optionsNumbers { Back, MetricUnits, ImperialUnits }; @@ -2592,7 +2615,34 @@ void menuHandler::DisplayUnits_menu() config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; service->reloadConfig(SEGMENT_CONFIG); } else { - menuHandler::menuQueue = menuHandler::screen_options_menu; + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::messageBubblesMenu() +{ + enum optionsNumbers { Back, ShowBubbles, HideBubbles }; + + static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Bubbles"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == ShowBubbles) { + config.display.enable_message_bubbles = true; + service->reloadConfig(SEGMENT_CONFIG); + LOG_INFO("Message bubbles enabled"); + } else if (selected == HideBubbles) { + config.display.enable_message_bubbles = false; + service->reloadConfig(SEGMENT_CONFIG); + LOG_INFO("Message bubbles disabled"); + } else { + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } }; @@ -2601,153 +2651,156 @@ void menuHandler::DisplayUnits_menu() void menuHandler::handleMenuSwitch(OLEDDisplay *display) { - if (menuQueue != menu_none) + if (menuQueue != MenuNone) test_count = 0; switch (menuQueue) { - case menu_none: + case MenuNone: break; - case lora_Menu: + case LoraMenu: loraMenu(); break; - case lora_picker: + case LoraPicker: LoraRegionPicker(); break; - case device_role_picker: - DeviceRolePicker(); + case DeviceRolePicker: + deviceRolePicker(); break; - case radio_preset_picker: - RadioPresetPicker(); + case RadioPresetPicker: + radioPresetPicker(); break; - case frequency_slot: + case FrequencySlot: FrequencySlotPicker(); break; - case no_timeout_lora_picker: + case NoTimeoutLoraPicker: LoraRegionPicker(0); break; - case TZ_picker: + case TzPicker: TZPicker(); break; - case twelve_hour_picker: - TwelveHourPicker(); + case TwelveHourPicker: + twelveHourPicker(); break; - case clock_face_picker: - ClockFacePicker(); + case ClockFacePicker: + clockFacePicker(); break; - case clock_menu: + case ClockMenu: clockMenu(); break; - case system_base_menu: + case SystemBaseMenu: systemBaseMenu(); break; - case position_base_menu: + case PositionBaseMenu: positionBaseMenu(); break; - case node_base_menu: + case NodeBaseMenu: nodeListMenu(); break; #if !MESHTASTIC_EXCLUDE_GPS - case gps_toggle_menu: + case GpsToggleMenu: GPSToggleMenu(); break; - case gps_format_menu: + case GpsFormatMenu: GPSFormatMenu(); break; - case gps_smart_position_menu: + case GpsSmartPositionMenu: GPSSmartPositionMenu(); break; - case gps_update_interval_menu: + case GpsUpdateIntervalMenu: GPSUpdateIntervalMenu(); break; - case gps_position_broadcast_menu: + case GpsPositionBroadcastMenu: GPSPositionBroadcastMenu(); break; #endif - case compass_point_north_menu: + case CompassPointNorthMenu: compassNorthMenu(); break; - case reset_node_db_menu: + case ResetNodeDbMenu: resetNodeDBMenu(); break; - case buzzermodemenupicker: + case BuzzerModeMenuPicker: BuzzerModeMenu(); break; - case mui_picker: + case MuiPicker: switchToMUIMenu(); break; - case tftcolormenupicker: + case TftColorMenuPicker: TFTColorPickerMenu(display); break; - case brightness_picker: + case BrightnessPicker: BrightnessPickerMenu(); break; - case node_name_length_menu: + case NodeNameLengthMenu: nodeNameLengthMenu(); break; - case reboot_menu: + case RebootMenu: rebootMenu(); break; - case shutdown_menu: + case ShutdownMenu: shutdownMenu(); break; - case NodePicker_menu: + case NodePickerMenu: NodePicker(); break; - case Manage_Node_menu: - ManageNodeMenu(); + case ManageNodeMenu: + manageNodeMenu(); break; - case remove_favorite: + case RemoveFavorite: removeFavoriteMenu(); break; - case trace_route_menu: + case TraceRouteMenu: traceRouteMenu(); break; - case test_menu: + case TestMenu: testMenu(); break; - case number_test: + case NumberTest: numberTest(); break; - case wifi_toggle_menu: + case WifiToggleMenu: wifiToggleMenu(); break; - case key_verification_init: + case KeyVerificationInit: keyVerificationInitMenu(); break; - case key_verification_final_prompt: + case KeyVerificationFinalPrompt: keyVerificationFinalPrompt(); break; - case bluetooth_toggle_menu: - BluetoothToggleMenu(); + case BluetoothToggleMenu: + bluetoothToggleMenu(); break; - case screen_options_menu: + case ScreenOptionsMenu: screenOptionsMenu(); break; - case power_menu: + case PowerMenu: powerMenu(); break; case FrameToggles: - FrameToggles_menu(); + frameTogglesMenu(); break; case DisplayUnits: - DisplayUnits_menu(); + displayUnitsMenu(); break; - case throttle_message: + case ThrottleMessage: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; - case message_response_menu: + case MessageResponseMenu: messageResponseMenu(); break; - case reply_menu: + case ReplyMenu: replyMenu(); break; - case delete_messages_menu: + case DeleteMessagesMenu: deleteMessagesMenu(); break; - case message_viewmode_menu: + case MessageViewModeMenu: messageViewModeMenu(); break; + case MessageBubblesMenu: + messageBubblesMenu(); + break; } - menuQueue = menu_none; + menuQueue = MenuNone; } void menuHandler::saveUIConfig() diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 1b964678b..4a0360412 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -8,53 +8,54 @@ class menuHandler { public: enum screenMenus { - menu_none, - lora_Menu, - lora_picker, - device_role_picker, - radio_preset_picker, - frequency_slot, - no_timeout_lora_picker, - TZ_picker, - twelve_hour_picker, - clock_face_picker, - clock_menu, - position_base_menu, - node_base_menu, - gps_toggle_menu, - gps_format_menu, - gps_smart_position_menu, - gps_update_interval_menu, - gps_position_broadcast_menu, - compass_point_north_menu, - reset_node_db_menu, - buzzermodemenupicker, - mui_picker, - tftcolormenupicker, - brightness_picker, - reboot_menu, - shutdown_menu, - NodePicker_menu, - Manage_Node_menu, - remove_favorite, - test_menu, - number_test, - wifi_toggle_menu, - bluetooth_toggle_menu, - screen_options_menu, - power_menu, - system_base_menu, - key_verification_init, - key_verification_final_prompt, - trace_route_menu, - throttle_message, - message_response_menu, - message_viewmode_menu, - reply_menu, - delete_messages_menu, - node_name_length_menu, + MenuNone, + LoraMenu, + LoraPicker, + DeviceRolePicker, + RadioPresetPicker, + FrequencySlot, + NoTimeoutLoraPicker, + TzPicker, + TwelveHourPicker, + ClockFacePicker, + ClockMenu, + PositionBaseMenu, + NodeBaseMenu, + GpsToggleMenu, + GpsFormatMenu, + GpsSmartPositionMenu, + GpsUpdateIntervalMenu, + GpsPositionBroadcastMenu, + CompassPointNorthMenu, + ResetNodeDbMenu, + BuzzerModeMenuPicker, + MuiPicker, + TftColorMenuPicker, + BrightnessPicker, + RebootMenu, + ShutdownMenu, + NodePickerMenu, + ManageNodeMenu, + RemoveFavorite, + TestMenu, + NumberTest, + WifiToggleMenu, + BluetoothToggleMenu, + ScreenOptionsMenu, + PowerMenu, + SystemBaseMenu, + KeyVerificationInit, + KeyVerificationFinalPrompt, + TraceRouteMenu, + ThrottleMessage, + MessageResponseMenu, + MessageViewModeMenu, + ReplyMenu, + DeleteMessagesMenu, + NodeNameLengthMenu, FrameToggles, - DisplayUnits + DisplayUnits, + MessageBubblesMenu }; static screenMenus menuQueue; static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu @@ -62,15 +63,15 @@ class menuHandler static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void loraMenu(); - static void DeviceRolePicker(); - static void RadioPresetPicker(); + static void deviceRolePicker(); + static void radioPresetPicker(); static void FrequencySlotPicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); static void TZPicker(); - static void TwelveHourPicker(); - static void ClockFacePicker(); + static void twelveHourPicker(); + static void clockFacePicker(); static void messageResponseMenu(); static void messageViewModeMenu(); static void replyMenu(); @@ -95,7 +96,7 @@ class menuHandler static void rebootMenu(); static void shutdownMenu(); static void NodePicker(); - static void ManageNodeMenu(); + static void manageNodeMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); static void traceRouteMenu(); @@ -106,15 +107,16 @@ class menuHandler static void screenOptionsMenu(); static void powerMenu(); static void nodeNameLengthMenu(); - static void FrameToggles_menu(); - static void DisplayUnits_menu(); + static void frameTogglesMenu(); + static void displayUnitsMenu(); + static void messageBubblesMenu(); static void textMessageMenu(); private: static void saveUIConfig(); static void keyVerificationInitMenu(); static void keyVerificationFinalPrompt(); - static void BluetoothToggleMenu(); + static void bluetoothToggleMenu(); }; /* Generic Menu Options designations */ diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 193164439..2fd9bf541 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -7,6 +7,7 @@ #include "NodeDB.h" #include "UIRenderer.h" #include "gps/RTC.h" +#include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" @@ -34,44 +35,6 @@ static std::vector cachedLines; static std::vector cachedHeights; static bool manualScrolling = false; -// UTF-8 skip helper -static inline size_t utf8CharLen(uint8_t c) -{ - if ((c & 0xE0) == 0xC0) - return 2; - if ((c & 0xF0) == 0xE0) - return 3; - if ((c & 0xF8) == 0xF0) - return 4; - return 1; -} - -// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels -static std::string normalizeEmoji(const std::string &s) -{ - std::string out; - for (size_t i = 0; i < s.size();) { - uint8_t c = static_cast(s[i]); - size_t len = utf8CharLen(c); - - if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) { - i += 3; - continue; - } - - // Skip skin tone modifiers - if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F && - ((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) { - i += 4; - continue; - } - - out.append(s, i, len); - i += len; - } - return out; -} - // Scroll state (file scope so we can reset on new message) float scrollY = 0.0f; uint32_t lastTime = 0; @@ -110,102 +73,7 @@ void scrollDown() void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { - int cursorX = x; - const int fontHeight = FONT_HEIGHT_SMALL; - - // Step 1: Find tallest emote in the line - int maxIconHeight = fontHeight; - for (size_t i = 0; i < line.length();) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (line.compare(i, emojiLen, emotes[e].label) == 0) { - if (emotes[e].height > maxIconHeight) - maxIconHeight = emotes[e].height; - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - i += utf8CharLen(static_cast(line[i])); - } - } - - // Step 2: Baseline alignment - int lineHeight = std::max(fontHeight, maxIconHeight); - int baselineOffset = (lineHeight - fontHeight) / 2; - int fontY = y + baselineOffset; - - // Step 3: Render line in segments - size_t i = 0; - bool inBold = false; - - while (i < line.length()) { - // Check for ** start/end for faux bold - if (line.compare(i, 2, "**") == 0) { - inBold = !inBold; - i += 2; - continue; - } - - // Look ahead for the next emote match - size_t nextEmotePos = std::string::npos; - const Emote *matchedEmote = nullptr; - size_t emojiLen = 0; - - for (int e = 0; e < emoteCount; ++e) { - size_t pos = line.find(emotes[e].label, i); - if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { - nextEmotePos = pos; - matchedEmote = &emotes[e]; - emojiLen = strlen(emotes[e].label); - } - } - - // Render normal text segment up to the emote or bold toggle - size_t nextControl = std::min(nextEmotePos, line.find("**", i)); - if (nextControl == std::string::npos) - nextControl = line.length(); - - if (nextControl > i) { - std::string textChunk = line.substr(i, nextControl - i); - if (inBold) { - // Faux bold: draw twice, offset by 1px - display->drawString(cursorX + 1, fontY, textChunk.c_str()); - } - display->drawString(cursorX, fontY, textChunk.c_str()); -#if defined(OLED_UA) || defined(OLED_RU) - cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true); -#else - cursorX += display->getStringWidth(textChunk.c_str()); -#endif - i = nextControl; - continue; - } - - // Render the emote (if found) - if (matchedEmote && i == nextEmotePos) { - int iconY = y + (lineHeight - matchedEmote->height) / 2; - display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); - cursorX += matchedEmote->width + 1; - i += emojiLen; - continue; - } else { - // No more emotes — render the rest of the line - std::string remaining = line.substr(i); - if (inBold) { - display->drawString(cursorX + 1, fontY, remaining.c_str()); - } - display->drawString(cursorX, fontY, remaining.c_str()); -#if defined(OLED_UA) || defined(OLED_RU) - cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true); -#else - cursorX += display->getStringWidth(remaining.c_str()); -#endif - break; - } - } + graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, FONT_HEIGHT_SMALL, emotes, emoteCount); } // Reset scroll state when new messages arrive @@ -377,32 +245,7 @@ static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) { - std::string normalized = normalizeEmoji(line); - int totalWidth = 0; - - size_t i = 0; - while (i < normalized.length()) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (normalized.compare(i, emojiLen, emotes[e].label) == 0) { - totalWidth += emotes[e].width + 1; // +1 spacing - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - size_t charLen = utf8CharLen(static_cast(normalized[i])); -#if defined(OLED_UA) || defined(OLED_RU) - totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true); -#else - totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str()); -#endif - i += charLen; - } - } - return totalWidth; + return graphics::EmoteRenderer::analyzeLine(display, line, 0, emotes, emoteCount).width; } struct MessageBlock { @@ -417,13 +260,7 @@ static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool i return lineTopY + (FONT_HEIGHT_SMALL - 1); } - int tallest = FONT_HEIGHT_SMALL; - for (int e = 0; e < numEmotes; ++e) { - if (line.find(emotes[e].label) != std::string::npos) { - if (emotes[e].height > tallest) - tallest = emotes[e].height; - } - } + const int tallest = graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes).tallestHeight; const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest); const int iconTop = lineTopY + (lineHeight - tallest) / 2; @@ -527,35 +364,37 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 constexpr int BUBBLE_MIN_W = 24; constexpr int BUBBLE_TEXT_INDENT = 2; + // Check if bubbles are enabled + const bool showBubbles = config.display.enable_message_bubbles; + const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN; + // Derived widths - const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2); + const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0); const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; // Title string depending on mode - static char titleBuf[32]; - const char *titleStr = "Messages"; + char titleStr[48]; + snprintf(titleStr, sizeof(titleStr), "Messages"); switch (currentMode) { case ThreadMode::ALL: - titleStr = "Messages"; + snprintf(titleStr, sizeof(titleStr), "Messages"); break; case ThreadMode::CHANNEL: { const char *cname = channels.getName(currentChannel); if (cname && cname[0]) { - snprintf(titleBuf, sizeof(titleBuf), "#%s", cname); + snprintf(titleStr, sizeof(titleStr), "#%s", cname); } else { - snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel); + snprintf(titleStr, sizeof(titleStr), "Ch%d", currentChannel); } - titleStr = titleBuf; break; } case ThreadMode::DIRECT: { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); - if (node && node->has_user) { - snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name); + if (node && node->has_user && node->user.short_name[0]) { + snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name); } else { - snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer); + snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer); } - titleStr = titleBuf; break; } } @@ -583,6 +422,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 std::vector isMine; // track alignment std::vector isHeader; // track header lines std::vector 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; @@ -662,44 +512,50 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); - char senderBuf[48] = ""; + char senderName[64] = ""; if (node && node->has_user) { - // Use long name if present - strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1); - senderBuf[sizeof(senderBuf) - 1] = '\0'; - } else { - // No long/short name → show NodeID in parentheses - snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender); + if (node->user.long_name[0]) { + strncpy(senderName, node->user.long_name, sizeof(senderName) - 1); + } else if (node->user.short_name[0]) { + strncpy(senderName, node->user.short_name, sizeof(senderName) - 1); + } + senderName[sizeof(senderName) - 1] = '\0'; + } + if (!senderName[0]) { + snprintf(senderName, sizeof(senderName), "(%08x)", m.sender); } - // If this is *our own* message, override senderBuf to who the recipient was + // If this is *our own* message, override senderName to who the recipient was bool mine = (m.sender == nodeDB->getNodeNum()); if (mine && node_recipient && node_recipient->has_user) { - strcpy(senderBuf, node_recipient->user.long_name); + if (node_recipient->user.long_name[0]) { + strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1); + senderName[sizeof(senderName) - 1] = '\0'; + } else if (node_recipient->user.short_name[0]) { + strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1); + senderName[sizeof(senderName) - 1] = '\0'; + } + } + // If recipient info is missing/empty, prefer a recipient identifier for outbound messages. + if (mine && (!node_recipient || !node_recipient->has_user || + (!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) { + snprintf(senderName, sizeof(senderName), "(%08x)", m.dest); } // Shrink Sender name if needed int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) - - display->getStringWidth(chanType) - display->getStringWidth(" @..."); + display->getStringWidth(chanType) - graphics::UIRenderer::measureStringWithEmotes(display, " @..."); if (availWidth < 0) availWidth = 0; - - size_t origLen = strlen(senderBuf); - while (senderBuf[0] && display->getStringWidth(senderBuf) > availWidth) { - senderBuf[strlen(senderBuf) - 1] = '\0'; - } - - // If we actually truncated, append "..." - if (strlen(senderBuf) < origLen) { - strcat(senderBuf, "..."); - } + char truncatedSender[64]; + graphics::UIRenderer::truncateStringWithEmotes(display, senderName, truncatedSender, sizeof(truncatedSender), availWidth); // Final header line - char headerStr[96]; + char headerStr[128]; if (mine) { if (currentMode == ThreadMode::ALL) { if (strcmp(chanType, "(DM)") == 0) { - snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf); + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, truncatedSender); } else { snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); } @@ -707,11 +563,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); } } else { - snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType); + snprintf(headerStr, sizeof(headerStr), chanType[0] ? "%s @%s %s" : "%s @%s", timeBuf, truncatedSender, chanType); } // Push header line - allLines.push_back(std::string(headerStr)); + allLines.push_back(headerStr); isMine.push_back(mine); isHeader.push_back(true); ackForLine.push_back(m.ackStatus); @@ -720,16 +576,23 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int wrapWidth = mine ? rightTextWidth : leftTextWidth; std::vector 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 blocks = buildMessageBlocks(isHeader, isMine); @@ -796,114 +659,100 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } } - // Draw bubbles - for (size_t bi = 0; bi < blocks.size(); ++bi) { - const auto &b = blocks[bi]; - if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end) - continue; + // Draw bubbles (only if enabled) + if (showBubbles) { + for (size_t bi = 0; bi < blocks.size(); ++bi) { + const auto &b = blocks[bi]; + if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end) + continue; - int visualTop = lineTop[b.start]; + int visualTop = lineTop[b.start]; - int topY; - if (isHeader[b.start]) { - // Header start - constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2 - topY = visualTop - BUBBLE_PAD_TOP_HEADER; - } else { - // Body start - bool thisLineHasEmote = false; - for (int e = 0; e < numEmotes; ++e) { - if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) { - thisLineHasEmote = true; - break; + int topY; + if (isHeader[b.start]) { + // Header start + constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2 + topY = visualTop - BUBBLE_PAD_TOP_HEADER; + } else { + // Body start + const bool thisLineHasEmote = + graphics::EmoteRenderer::analyzeLine(nullptr, cachedLines[b.start].c_str(), 0, emotes, numEmotes).hasEmote; + if (thisLineHasEmote) { + constexpr int EMOTE_PADDING_ABOVE = 4; + visualTop -= EMOTE_PADDING_ABOVE; } + topY = visualTop - BUBBLE_PAD_Y; } - if (thisLineHasEmote) { - constexpr int EMOTE_PADDING_ABOVE = 4; - visualTop -= EMOTE_PADDING_ABOVE; + int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); + int bottomY = visualBottom + BUBBLE_PAD_Y; + + if (bi + 1 < blocks.size()) { + int nextHeaderIndex = (int)blocks[bi + 1].start; + int nextTop = lineTop[nextHeaderIndex]; + int maxBottom = nextTop - 1 - bubbleGapY; + if (bottomY > maxBottom) + bottomY = maxBottom; } - topY = visualTop - BUBBLE_PAD_Y; - } - int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); - int bottomY = visualBottom + BUBBLE_PAD_Y; - if (bi + 1 < blocks.size()) { - int nextHeaderIndex = (int)blocks[bi + 1].start; - int nextTop = lineTop[nextHeaderIndex]; - int maxBottom = nextTop - 1 - bubbleGapY; - if (bottomY > maxBottom) - bottomY = maxBottom; - } + if (bottomY <= topY + 2) + continue; - if (bottomY <= topY + 2) - continue; + if (bottomY < contentTop || topY > contentBottom - 1) + continue; - if (bottomY < contentTop || topY > contentBottom - 1) - continue; + int maxLineW = 0; - int maxLineW = 0; - - for (size_t i = b.start; i <= b.end; ++i) { - int w = 0; - if (isHeader[i]) { - w = display->getStringWidth(cachedLines[i].c_str()); - if (b.mine) - w += 12; // room for ACK/NACK/relay mark - } else { - w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + for (size_t i = b.start; i <= b.end; ++i) { + int w = 0; + if (isHeader[i]) { + w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str()); + if (b.mine) + w += 12; // room for ACK/NACK/relay mark + } else { + w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + } + if (w > maxLineW) + maxLineW = w; } - if (w > maxLineW) - maxLineW = w; - } - - int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2)); - int bubbleH = (bottomY - topY) + 1; - int bubbleX = 0; - if (b.mine) { - bubbleX = rightEdge - bubbleW; - } else { - bubbleX = x; - } - if (bubbleX < x) - bubbleX = x; - if (bubbleX + bubbleW > rightEdge) - bubbleW = std::max(1, rightEdge - bubbleX); - - if (bubbleW > 1 && bubbleH > 1) { - int x1 = bubbleX + bubbleW - 1; - int y1 = topY + bubbleH - 1; + int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2)); + int bubbleH = (bottomY - topY) + 1; + int bubbleX = 0; if (b.mine) { - // Send Message (Right side) - display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH); - // Top Right Corner - display->drawRect(x1, topY, 2, 1); - display->drawRect(x1, topY, 1, 2); - // Bottom Right Corner - display->drawRect(x1 - 1, bottomY - 2, 2, 1); - display->drawRect(x1, bottomY - 3, 1, 2); - // Knock the corners off to make a bubble - display->setColor(BLACK); - display->drawRect(x1 - bubbleW, topY - 1, 1, 1); - display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1); - display->setColor(WHITE); + bubbleX = rightEdge - bubbleW; } else { - // Received Message (Left Side) - display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH); - // Top Left Corner - display->drawRect(bubbleX + 1, topY + 1, 2, 1); - display->drawRect(bubbleX + 1, topY + 1, 1, 2); - // Bottom Left Corner - display->drawRect(bubbleX + 1, bottomY - 1, 2, 1); - display->drawRect(bubbleX + 1, bottomY - 2, 1, 2); - // Knock the corners off to make a bubble - display->setColor(BLACK); - display->drawRect(bubbleX + bubbleW, topY, 1, 1); - display->drawRect(bubbleX + bubbleW, bottomY, 1, 1); - display->setColor(WHITE); + bubbleX = x; + } + if (bubbleX < x) + bubbleX = x; + if (bubbleX + bubbleW > rightEdge) + bubbleW = std::max(1, rightEdge - bubbleX); + + // Draw rounded rectangle bubble + if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) { + const int r = BUBBLE_RADIUS; + const int bx = bubbleX; + const int by = topY; + const int bw = bubbleW; + const int bh = bubbleH; + + // Draw the 4 corner arcs using drawCircleQuads + display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left + display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right + display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left + display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right + + // Draw the 4 edges between corners + display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge + display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge + display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge + display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge + } else if (bubbleW > 1 && bubbleH > 1) { + // Fallback to simple rectangle for very small bubbles + display->drawRect(bubbleX, topY, bubbleW, bubbleH); } } - } + } // end if (showBubbles) // Render visible lines int lineY = yOffset; @@ -912,17 +761,18 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 if (lineY > -cachedHeights[i] && lineY < scrollBottom) { if (isHeader[i]) { - int w = display->getStringWidth(cachedLines[i].c_str()); + int w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str()); int headerX; if (isMine[i]) { // push header left to avoid overlap with scrollbar - headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT; + headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0); if (headerX < LEFT_MARGIN) headerX = LEFT_MARGIN; } else { - headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT; + headerX = x + textIndent; } - display->drawString(headerX, lineY, cachedLines[i].c_str()); + graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1, + false); // Draw underline just under header text int underlineY = lineY + FONT_HEIGHT_SMALL; @@ -960,14 +810,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 if (isMine[i]) { // Calculate actual rendered width including emotes int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); - int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT; + int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0); if (rightX < LEFT_MARGIN) rightX = LEFT_MARGIN; drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); } else { - drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes, - numEmotes); + drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes); } } } @@ -1011,11 +860,7 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS } else { word += ch; std::string test = line + word; -#if defined(OLED_UA) || defined(OLED_RU) - uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); -#else - uint16_t strWidth = display->getStringWidth(test.c_str()); -#endif + uint16_t strWidth = graphics::UIRenderer::measureStringWithEmotes(display, test.c_str()); if (strWidth > textWidth) { if (!line.empty()) lines.push_back(line); @@ -1044,31 +889,20 @@ std::vector calculateLineHeights(const std::vector &lines, con std::vector rowHeights; rowHeights.reserve(lines.size()); + std::vector lineMetrics; + lineMetrics.reserve(lines.size()); + + for (const auto &line : lines) { + lineMetrics.push_back(graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes)); + } for (size_t idx = 0; idx < lines.size(); ++idx) { - const auto &line = lines[idx]; const int baseHeight = FONT_HEIGHT_SMALL; int lineHeight = baseHeight; - // Detect if THIS line or NEXT line contains an emote - bool hasEmote = false; - int tallestEmote = baseHeight; - for (int i = 0; i < numEmotes; ++i) { - if (line.find(emotes[i].label) != std::string::npos) { - hasEmote = true; - tallestEmote = std::max(tallestEmote, emotes[i].height); - } - } - - bool nextHasEmote = false; - if (idx + 1 < lines.size()) { - for (int i = 0; i < numEmotes; ++i) { - if (lines[idx + 1].find(emotes[i].label) != std::string::npos) { - nextHasEmote = true; - break; - } - } - } + const int tallestEmote = lineMetrics[idx].tallestHeight; + const bool hasEmote = lineMetrics[idx].hasEmote; + const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote; if (isHeaderVec[idx]) { // Header line spacing @@ -1118,22 +952,22 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht // Banner logic const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); - char longName[48] = "?"; - if (node && node->user.long_name) { - strncpy(longName, node->user.long_name, sizeof(longName) - 1); - longName[sizeof(longName) - 1] = '\0'; + char longName[64] = "?"; + if (node && node->has_user) { + if (node->user.long_name[0]) { + strncpy(longName, node->user.long_name, sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + } else if (node->user.short_name[0]) { + strncpy(longName, node->user.short_name, sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + } } int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); if (availWidth < 0) availWidth = 0; - - size_t origLen = strlen(longName); - while (longName[0] && display->getStringWidth(longName) > availWidth) { - longName[strlen(longName) - 1] = '\0'; - } - if (strlen(longName) < origLen) { - strcat(longName, "..."); - } + char truncatedLongName[64]; + graphics::UIRenderer::truncateStringWithEmotes(display, longName, truncatedLongName, sizeof(truncatedLongName), + availWidth); const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); char banner[256]; @@ -1151,8 +985,8 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht } if (isAlert) { - if (longName && longName[0]) - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + if (truncatedLongName[0]) + snprintf(banner, sizeof(banner), "Alert Received from\n%s", truncatedLongName); else strcpy(banner, "Alert Received"); } else { @@ -1160,11 +994,11 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht if (isChannelMuted) return; - if (longName && longName[0]) { + if (truncatedLongName[0]) { if (currentResolution == ScreenResolution::UltraLow) { strcpy(banner, "New Message"); } else { - snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + snprintf(banner, sizeof(banner), "New Message from\n%s", truncatedLongName); } } else strcpy(banner, "New Message"); @@ -1227,4 +1061,4 @@ void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) } // namespace MessageRenderer } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 9d6780130..654c27222 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -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 @@ -79,57 +82,60 @@ void scrollDown() // Utility Functions // ============================= -const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) +std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) { - static char nodeName[25]; // single static buffer we return - nodeName[0] = '\0'; + (void)display; + (void)columnWidth; - auto writeFallbackId = [&] { - std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); + auto fallbackId = [&] { + char id[12]; + std::snprintf(id, sizeof(id), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); + return std::string(id); }; // 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; - } - // 2) Sanitize (empty if raw is null/empty) - std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{}; - - // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed) - if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) { - writeFallbackId(); - } else { - // %.*s ensures null-termination and safe truncation to buffer size - 1 - std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast(sizeof(nodeName) - 1), s.c_str()); - } - - // 4) Width-based truncation + ellipsis (long-name mode only) - if (config.display.use_long_node_name && display) { - int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); - if (availWidth < 0) - availWidth = 0; - - const size_t beforeLen = std::strlen(nodeName); - - // Trim from the end until it fits or is empty - size_t len = beforeLen; - while (len && display->getStringWidth(nodeName) > availWidth) { - nodeName[--len] = '\0'; - } - - // If truncated, append "..." (respect buffer size) - if (len < beforeLen) { - // Make sure there's room for "..." and '\0' - const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' - const size_t needed = 3; // "..." - if (len > capForText - needed) { - len = capForText - needed; - nodeName[len] = '\0'; +#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; } - std::strcat(nodeName, "..."); } + + 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. + std::string nodeName = (raw && *raw) ? std::string(raw) : std::string{}; + if (nodeName.empty()) { + nodeName = fallbackId(); } return nodeName; @@ -163,6 +169,15 @@ const char *getCurrentModeTitle_Location(int screenWidth) } } +static int getNodeNameMaxWidth(int columnWidth, int baseWidth) +{ + if (!config.display.use_long_node_name) + return baseWidth; + + const int legacyLongNameWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); + return std::max(0, std::min(baseWidth, legacyLongNameWidth)); +} + // Use dynamic timing based on mode unsigned long getModeCycleIntervalMs() { @@ -171,7 +186,7 @@ unsigned long getModeCycleIntervalMs() int calculateMaxScroll(int totalEntries, int visibleRows) { - return std::max(0, (totalEntries - 1) / (visibleRows * 2)); + return max(0, (totalEntries - 1) / (visibleRows * 2)); } void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) @@ -187,13 +202,12 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, if (totalEntries <= visibleNodeRows * columns) return; - int scrollbarX = display->getWidth() - 2; int scrollbarHeight = display->getHeight() - scrollStartY - 10; - int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); - int perPage = visibleNodeRows * columns; - int maxScroll = std::max(0, (totalEntries - 1) / perPage); - int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); + int thumbHeight = max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); + int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / + max(1, max(0, (totalEntries - 1) / (visibleNodeRows * columns))); + int scrollbarX = display->getWidth() - 2; for (int i = 0; i < thumbHeight; i++) { display->setPixel(scrollbarX, thumbY + i); } @@ -206,10 +220,13 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - 25; + int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); + char nodeName[96]; + UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), + nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char timeStr[10]; @@ -229,7 +246,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); + UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -256,19 +273,22 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - 25; + int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int barsXOffset = columnWidth - barsOffset; - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); + char nodeName[96]; + UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), + nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -313,9 +333,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 { bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = - columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) + : (isLeftCol ? 20 : 22))); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); + char nodeName[96]; + UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), + nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char distStr[10] = ""; @@ -369,7 +393,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -415,14 +439,18 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 // Adjust max text width depending on column and screen width int nameMaxWidth = - columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) + : (isLeftCol ? 20 : 22))); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); + char nodeName[96]; + UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), + nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -556,13 +584,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int maxScroll = 0; if (perPage > 0) { - maxScroll = std::max(0, (totalEntries - 1) / perPage); + maxScroll = max(0, (totalEntries - 1) / perPage); } if (scrollIndex > maxScroll) scrollIndex = maxScroll; int startIndex = scrollIndex * visibleNodeRows * totalColumns; - int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + int endIndex = min(startIndex + visibleNodeRows * totalColumns, totalEntries); int yOffset = 0; int col = 0; int lastNodeY = y; @@ -580,7 +608,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t if (extras) extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); - lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; shownCount++; rowCount++; @@ -613,13 +641,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t if (millis() - popupTime < POPUP_DURATION_MS) { popupTotal = totalEntries; - int perPage = visibleNodeRows * totalColumns; - popupStart = startIndex + 1; - popupEnd = std::min(startIndex + perPage, totalEntries); + popupEnd = min(startIndex + perPage, totalEntries); popupPage = (scrollIndex + 1); - popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); + popupMaxPage = max(1, (totalEntries + perPage - 1) / perPage); char buf[32]; snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); @@ -831,4 +857,4 @@ void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields } // namespace NodeListRenderer } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index e212c031b..be80a7d80 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -4,6 +4,7 @@ #include "mesh/generated/meshtastic/mesh.pb.h" #include #include +#include namespace graphics { @@ -56,7 +57,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, // Utility functions const char *getCurrentModeTitle_Nodes(int screenWidth); const char *getCurrentModeTitle_Location(int screenWidth); -const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth); +std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); // Scrolling controls diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 8d76b4592..31eb2c3c8 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -4,6 +4,7 @@ #include "DisplayFormatters.h" #include "NodeDB.h" #include "NotificationRenderer.h" +#include "UIRenderer.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" @@ -43,7 +44,7 @@ InputEvent NotificationRenderer::inEvent; int8_t NotificationRenderer::curSelected = 0; char NotificationRenderer::alertBannerMessage[256] = {0}; uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever -uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options +uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are selectable options const char **NotificationRenderer::optionsArrayPtr = nullptr; const int *NotificationRenderer::optionsEnumPtr = nullptr; std::function NotificationRenderer::alertBannerCallback = NULL; @@ -95,7 +96,7 @@ void NotificationRenderer::resetBanner() inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.kbchar = 0; curSelected = 0; - alertBannerOptions = 0; // last x lines are seelctable options + alertBannerOptions = 0; // last x lines are selectable options optionsArrayPtr = nullptr; optionsEnumPtr = nullptr; alertBannerCallback = NULL; @@ -299,7 +300,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta for (int i = 0; i < lineCount; i++) { linePointers[i] = lineStarts[i]; } - char scratchLineBuffer[visibleTotalLines - lineCount][40]; + char scratchLineBuffer[visibleTotalLines - lineCount][64]; uint8_t firstOptionToShow = 0; if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { @@ -312,28 +313,47 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta } int scratchLineNum = 0; for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { - char temp_name[16] = {0}; - if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { - std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); - strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); + char tempName[48] = {0}; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1); + if (node && node->has_user) { + const char *rawName = nullptr; + if (node->user.long_name[0]) { + rawName = node->user.long_name; + } else if (node->user.short_name[0]) { + rawName = node->user.short_name; + } + if (rawName) { + const int arrowWidth = (currentResolution == ScreenResolution::High) + ? UIRenderer::measureStringWithEmotes(display, "> <") + : UIRenderer::measureStringWithEmotes(display, "><"); + const int maxTextWidth = std::max(0, display->getWidth() - 28 - arrowWidth); + UIRenderer::truncateStringWithEmotes(display, rawName, tempName, sizeof(tempName), maxTextWidth); + } } else { - snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); + snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0)); + } + if (!tempName[0]) { + snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0)); } if (i == curSelected) { - selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; + selectedNodenum = node ? node->num : 0; if (currentResolution == ScreenResolution::High) { strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); - strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); - strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); + strncpy(scratchLineBuffer[scratchLineNum] + 2, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 3); + scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; + const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1); + strncpy(scratchLineBuffer[scratchLineNum] + used, " <", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1); } else { strncpy(scratchLineBuffer[scratchLineNum], ">", 2); - strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); - strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); + strncpy(scratchLineBuffer[scratchLineNum] + 1, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 2); + scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; + const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1); + strncpy(scratchLineBuffer[scratchLineNum] + used, "<", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1); } - scratchLineBuffer[scratchLineNum][39] = '\0'; + scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; } else { - strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39); - scratchLineBuffer[scratchLineNum][39] = '\0'; + strncpy(scratchLineBuffer[scratchLineNum], tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 1); + scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; } linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; } @@ -501,7 +521,13 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay else // if the newline wasn't found, then pull string length from strlen lineLengths[lineCount] = strlen(lines[lineCount]); - lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + if (current_notification_type == notificationTypeEnum::node_picker) { + char measureBuffer[64] = {0}; + strncpy(measureBuffer, lines[lineCount], std::min(lineLengths[lineCount], sizeof(measureBuffer) - 1)); + lineWidths[lineCount] = UIRenderer::measureStringWithEmotes(display, measureBuffer); + } else { + lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + } // Consider extra width for signal bars on lines that contain "Signal:" uint16_t potentialWidth = lineWidths[lineCount]; @@ -607,7 +633,11 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); display->setColor(BLACK); int yOffset = 3; - display->drawString(textX, lineY - yOffset, lineBuffer); + if (current_notification_type == notificationTypeEnum::node_picker) { + UIRenderer::drawStringWithEmotes(display, textX, lineY - yOffset, lineBuffer, FONT_HEIGHT_SMALL, 1, false); + } else { + display->drawString(textX, lineY - yOffset, lineBuffer); + } display->setColor(WHITE); lineY += (effectiveLineHeight - 2 - background_yOffset); } else { @@ -626,7 +656,11 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay int totalWidth = textWidth + barsWidth; int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; - display->drawString(groupStartX, lineY, lineBuffer); + if (current_notification_type == notificationTypeEnum::node_picker) { + UIRenderer::drawStringWithEmotes(display, groupStartX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false); + } else { + display->drawString(groupStartX, lineY, lineBuffer); + } int baseX = groupStartX + textWidth + gap; int baseY = lineY + effectiveLineHeight - 1; @@ -642,7 +676,11 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay } } } else { - display->drawString(textX, lineY, lineBuffer); + if (current_notification_type == notificationTypeEnum::node_picker) { + UIRenderer::drawStringWithEmotes(display, textX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false); + } else { + display->drawString(textX, lineY, lineBuffer); + } } lineY += (effectiveLineHeight); } @@ -781,4 +819,4 @@ void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, } } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index e51bfa5ab..45b05be9c 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -22,7 +22,7 @@ class NotificationRenderer static uint32_t alertBannerUntil; // 0 is a special case meaning forever static const char **optionsArrayPtr; static const int *optionsEnumPtr; - static uint8_t alertBannerOptions; // last x lines are seelctable options + static uint8_t alertBannerOptions; // last x lines are selectable options static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 97cb4e85c..5d4616c08 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -2,11 +2,16 @@ #if HAS_SCREEN #include "CompassRenderer.h" #include "GPSStatus.h" +#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" +#include "graphics/EmoteRenderer.h" #include "graphics/SharedUIDisplay.h" #include "graphics/TimeFormatters.h" #include "graphics/images.h" @@ -287,7 +292,8 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes // ********************** // * Favorite Node Info * // ********************** -void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) +// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library +void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { if (favoritedNodes.empty()) return; @@ -311,9 +317,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st #endif currentFavoriteNodeNum = node->num; // === Create the shortName and title string === - const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; - char titlestr[32] = {0}; - snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); + const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node"; + char titlestr[40]; + snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); // === Draw battery/time/mail header (common across screens) === graphics::drawCommonHeader(display, x, y, titlestr); @@ -326,7 +332,6 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st // List of available macro Y positions in order, from top to bottom. int line = 1; // which slot to use next - std::string usernameStr; // === 1. Long Name (always try to show first) === const char *username; if (currentResolution == ScreenResolution::UltraLow) { @@ -336,40 +341,218 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st } if (username) { - usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case // Print node's long name (e.g. "Backpack Node") - display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); + UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false); } - // === 2. Signal and Hops (combined on one line, if available) === - // If both are present: "Sig: 97% [2hops]" - // If only one: show only that one - char signalHopsStr[32] = ""; - bool haveSignal = false; - int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); +#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; - // Always use "Sig" for the label - const char *signalLabel = " Sig"; + // Search newest-to-oldest + for (auto it = recent.rbegin(); it != recent.rend(); ++it) { + if (it->fromNodeId == node->num && !it->statusText.empty()) { + found = &(*it); + break; + } + } - // --- Build the Signal/Hops line --- - // If SNR looks reasonable, show signal - if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { - snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); - haveSignal = true; - } - // If hops is valid (>0), show right after signal - if (node->hops_away > 0) { - size_t len = strlen(signalHopsStr); - // Decide between "1 Hop" and "N Hops" - if (haveSignal) { - snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, - (node->hops_away == 1 ? "Hop" : "Hops")); - } else { - snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + 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()); } } - if (signalHopsStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], signalHopsStr); +#endif + + // === 2. Signal and Hops (combined on one line, if available) === + char signalHopsStr[32] = ""; + bool haveSignal = false; + int bars = 0; + + // Helper to get SNR limit based on modem preset + auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float { + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + return -6.0f; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + return -5.5f; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + return -4.5f; + default: + return -6.0f; + } + }; + + // Calculate signal grade using modem preset and SNR only + float snrLimit = getSnrLimit(config.lora.modem_preset); + float snr = node->snr; + + // Determine signal quality label and bars using SNR-only grading + const char *qualityLabel = nullptr; + + if (snr > snrLimit + 10) { + qualityLabel = "Good"; + bars = 4; + } else if (snr > snrLimit + 6) { + qualityLabel = "Good"; + bars = 3; + } else if (snr > snrLimit + 2) { + qualityLabel = "Good"; + bars = 2; + } else if (snr > snrLimit - 4) { + qualityLabel = "Fair"; + bars = 1; + } else { + qualityLabel = "Bad"; + bars = 1; + } + + // Add extra spacing on the left if we have an API connection to account for the common footer icons + const char *leftSideSpacing = + graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " "; + + // --- Build the Signal/Hops line --- + // Only show signal if we have valid SNR + if (snr > -100 && snr != 0) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel); + haveSignal = true; + } + + if (node->hops_away > 0) { + size_t len = strlen(signalHopsStr); + if (haveSignal) { + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]"); + } else { + snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]"); + } + } + + if (signalHopsStr[0]) { + int yPos = getTextPositions(display)[line++]; + int curX = x; + + // Split combined string into signal text and hop suffix + char sigPart[20] = ""; + const char *hopPart = nullptr; + + char *bracket = strchr(signalHopsStr, '['); + if (bracket) { + size_t n = (size_t)(bracket - signalHopsStr); + if (n >= sizeof(sigPart)) + n = sizeof(sigPart) - 1; + memcpy(sigPart, signalHopsStr, n); + sigPart[n] = '\0'; + + // Trim trailing spaces + while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') { + sigPart[strlen(sigPart) - 1] = '\0'; + } + + hopPart = bracket; // "[n Hop(s)]" + } else { + strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1); + sigPart[sizeof(sigPart) - 1] = '\0'; + } + + // Draw signal quality text + display->drawString(curX, yPos, sigPart); + curX += display->getStringWidth(sigPart) + 4; + + // Draw signal bars (skip on UltraLow, text only) + if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) { + const int kMaxBars = 4; + if (bars < 1) + bars = 1; + if (bars > kMaxBars) + bars = kMaxBars; + + int barX = curX; + + const bool hi = (currentResolution == ScreenResolution::High); + int barWidth = hi ? 2 : 1; + int barGap = hi ? 2 : 1; + int maxBarHeight = FONT_HEIGHT_SMALL - 7; + if (!hi) + maxBarHeight -= 1; + int barY = yPos + (FONT_HEIGHT_SMALL - maxBarHeight) / 2; + + for (int bi = 0; bi < kMaxBars; bi++) { + int barHeight = maxBarHeight * (bi + 1) / kMaxBars; + if (barHeight < 2) + barHeight = 2; + + int bx = barX + bi * (barWidth + barGap); + int by = barY + maxBarHeight - barHeight; + + if (bi < bars) { + display->fillRect(bx, by, barWidth, barHeight); + } else { + int baseY = barY + maxBarHeight - 1; + display->drawHorizontalLine(bx, baseY, barWidth); + } + } + + curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2; + } + + // Draw hops AFTER the bars as: [ number + hop icon ] + if (hopPart && node->hops_away > 0) { + + // open bracket + display->drawString(curX, yPos, "["); + curX += display->getStringWidth("[") + 1; + + // hop count + char hopCount[6]; + snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away); + display->drawString(curX, yPos, hopCount); + curX += display->getStringWidth(hopCount) + 2; + + // hop icon + const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2; + display->drawXbm(curX, iconY, hop_width, hop_height, hop); + curX += hop_width + 1; + + // closing bracket + display->drawString(curX, yPos, "]"); + } } // === 3. Heard (last seen, skip if node never seen) === @@ -377,8 +560,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st uint32_t seconds = sinceLastSeen(node); if (seconds != 0 && seconds != UINT32_MAX) { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" - snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + // Format as "Heard:Xm ago", "Heard:Xh ago", or "Heard:Xd ago" + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard:?" : "%sHeard:%d%c ago"), leftSideSpacing, (days ? days : hours ? hours : minutes), @@ -386,16 +569,18 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st : hours ? 'h' : 'm')); } - if (seenStr[0] && line < 5) { + if (seenStr[0]) { display->drawString(x, getTextPositions(display)[line++], seenStr); } #if !defined(M5STACK_UNITC6L) // === 4. Uptime (only show if metric is present) === char uptimeStr[32] = ""; if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { - getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr)); + char upPrefix[12]; // enough for leftSideSpacing + "Up:" + snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing); + getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr)); } - if (uptimeStr[0] && line < 5) { + if (uptimeStr[0]) { display->drawString(x, getTextPositions(display)[line++], uptimeStr); } @@ -422,16 +607,16 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st if (miles < 0.1) { int feet = (int)(miles * 5280); if (feet > 0 && feet < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); + snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet); haveDistance = true; } else if (feet >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: ¼mi"); + snprintf(distStr, sizeof(distStr), "%sDistance:¼mi", leftSideSpacing); haveDistance = true; } } else { int roundedMiles = (int)(miles + 0.5); if (roundedMiles > 0 && roundedMiles < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); + snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, roundedMiles); haveDistance = true; } } @@ -439,26 +624,74 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st if (distanceKm < 1.0) { int meters = (int)(distanceKm * 1000); if (meters > 0 && meters < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); + snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters); haveDistance = true; } else if (meters >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: 1km"); + snprintf(distStr, sizeof(distStr), "%sDistance:1km", leftSideSpacing); haveDistance = true; } } else { int km = (int)(distanceKm + 0.5); if (km > 0 && km < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); + snprintf(distStr, sizeof(distStr), "%sDistance:%dkm", leftSideSpacing, km); haveDistance = true; } } } } - // Only display if we actually have a value! - if (haveDistance && distStr[0] && line < 5) { + if (haveDistance && distStr[0]) { display->drawString(x, getTextPositions(display)[line++], distStr); } + // === 6. Battery after Distance line, otherwise next available line === + char batLine[32] = ""; + bool haveBatLine = false; + + if (node->has_device_metrics) { + bool hasPct = node->device_metrics.has_battery_level; + bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f; + + int pct = 0; + float volt = 0.0f; + + if (hasPct) { + pct = (int)node->device_metrics.battery_level; + } + + if (hasVolt) { + volt = node->device_metrics.voltage; + } + + if (hasPct && pct > 0 && pct <= 100) { + // Normal battery percentage + if (hasVolt) { + snprintf(batLine, sizeof(batLine), "%sBat:%d%% (%.2fV)", leftSideSpacing, pct, volt); + } else { + snprintf(batLine, sizeof(batLine), "%sBat:%d%%", leftSideSpacing, pct); + } + haveBatLine = true; + } else if (hasPct && pct > 100) { + // Plugged in + if (hasVolt) { + snprintf(batLine, sizeof(batLine), "%sPlugged In (%.2fV)", leftSideSpacing, volt); + } else { + snprintf(batLine, sizeof(batLine), "%sPlugged In", leftSideSpacing); + } + haveBatLine = true; + } else if (!hasPct && hasVolt) { + // Voltage only + snprintf(batLine, sizeof(batLine), "%sBat:%.2fV", leftSideSpacing, volt); + haveBatLine = true; + } + } + + const int maxTextLines = (currentResolution == ScreenResolution::High) ? 6 : 5; + + // Only draw battery if it fits within the allowed lines + if (haveBatLine && line <= maxTextLines) { + display->drawString(x, getTextPositions(display)[line++], batLine); + } + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { bool showCompass = false; @@ -593,7 +826,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta } char uptimeStr[32] = ""; if (currentResolution != ScreenResolution::UltraLow) { - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr)); } display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); @@ -622,14 +855,12 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // === Node Identity === int textWidth = 0; int nameX = 0; - char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s", - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + const char *shortName = owner.short_name ? owner.short_name : ""; // === ShortName Centered === - textWidth = display->getStringWidth(shortnameble); + textWidth = UIRenderer::measureStringWithEmotes(display, shortName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, false); #else if (powerStatus->getHasBattery()) { char batStr[20]; @@ -724,36 +955,36 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta int textWidth = 0; int nameX = 0; int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; - std::string longNameStr; - - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - longNameStr = sanitizeString(ourNode->user.long_name); + const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : ""; + const char *shortName = owner.short_name ? owner.short_name : ""; + char combinedName[96]; + if (longName[0] && shortName[0]) { + snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortName); + } else if (longName[0]) { + strncpy(combinedName, longName, sizeof(combinedName) - 1); + combinedName[sizeof(combinedName) - 1] = '\0'; + } else { + strncpy(combinedName, shortName, sizeof(combinedName) - 1); + combinedName[sizeof(combinedName) - 1] = '\0'; } - char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s", - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - - char combinedName[50]; - snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble); - if (SCREEN_WIDTH - (display->getStringWidth(combinedName)) > 10) { - size_t len = strlen(combinedName); - if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { - combinedName[len - 3] = '\0'; // Remove the last three characters - } - textWidth = display->getStringWidth(combinedName); + if (SCREEN_WIDTH - UIRenderer::measureStringWithEmotes(display, combinedName) > 10) { + textWidth = UIRenderer::measureStringWithEmotes(display, combinedName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString( - nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName); + UIRenderer::drawStringWithEmotes( + display, nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, + combinedName, FONT_HEIGHT_SMALL, 1, false); } else { // === LongName Centered === - textWidth = display->getStringWidth(longNameStr.c_str()); + textWidth = UIRenderer::measureStringWithEmotes(display, longName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str()); + UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], longName, FONT_HEIGHT_SMALL, 1, + false); // === ShortName Centered === - textWidth = display->getStringWidth(shortnameble); + textWidth = UIRenderer::measureStringWithEmotes(display, shortName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, + false); } #endif graphics::drawCommonFooter(display, x, y); @@ -865,12 +1096,12 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState display->setTextAlignment(TEXT_ALIGN_LEFT); const char *pauseText = "Screen Paused"; const char *idText = owner.short_name; - const bool useId = haveGlyphs(idText); + const bool useId = (idText && idText[0]); constexpr uint8_t padding = 2; constexpr uint8_t dividerGap = 1; // Text widths - const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); + const uint16_t idTextWidth = useId ? UIRenderer::measureStringWithEmotes(display, idText) : 0; const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); @@ -895,7 +1126,7 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState // Draw: text if (useId) - display->drawString(idTextLeft, idTextTop, idText); + UIRenderer::drawStringWithEmotes(display, idTextLeft, idTextTop, idText, FONT_HEIGHT_SMALL, 1, false); display->drawString(pauseTextLeft, pauseTextTop, pauseText); display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold @@ -928,11 +1159,16 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED display->drawString(msgX, msgY, upperMsg); } // Draw version and short name in bottom middle - char buf[25]; - snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT), - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf); + char footer[64]; + if (owner.short_name && owner.short_name[0]) { + snprintf(footer, sizeof(footer), "%s %s", xstr(APP_VERSION_SHORT), owner.short_name); + } else { + snprintf(footer, sizeof(footer), "%s", xstr(APP_VERSION_SHORT)); + } + int footerW = UIRenderer::measureStringWithEmotes(display, footer); + int footerX = x + ((SCREEN_WIDTH - footerW) / 2); + UIRenderer::drawStringWithEmotes(display, footerX, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, footer, FONT_HEIGHT_SMALL, 1, + false); screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code @@ -950,12 +1186,15 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED display->drawString(x + 0, y + 0, upperMsg); // Draw version and short name in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); + const char *version = xstr(APP_VERSION_SHORT); + int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); + display->drawString(versionX, y + 0, version); + if (owner.short_name && owner.short_name[0]) { + const char *shortName = owner.short_name; + int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName); + int shortNameX = x + SCREEN_WIDTH - shortNameW; + UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false); + } screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code @@ -984,7 +1223,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU config.display.heading_bold = false; const char *displayLine = ""; // Initialize to empty string by default - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.fixed_position) { @@ -1029,10 +1267,10 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU char uptimeStr[32]; #if defined(USE_EINK) // E-Ink: skip seconds, show only days/hours/mins - getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false); + getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), false); #else // Non E-Ink: include seconds where useful - getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true); + getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true); #endif display->drawString(0, getTextPositions(display)[line++], uptimeStr); @@ -1186,11 +1424,15 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O display->drawString(x + 0, y + 0, upperMsg); // Draw version and shortname in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); + const char *version = xstr(APP_VERSION_SHORT); + int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); + display->drawString(versionX, y + 0, version); + if (owner.short_name && owner.short_name[0]) { + const char *shortName = owner.short_name; + int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName); + int shortNameX = x + SCREEN_WIDTH - shortNameW; + UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false); + } screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code @@ -1210,6 +1452,7 @@ static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; +// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) { int currentFrame = state->currentFrame; @@ -1378,6 +1621,25 @@ std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t mi return uptime; } +int UIRenderer::measureStringWithEmotes(OLEDDisplay *display, const char *line, int emoteSpacing) +{ + return graphics::EmoteRenderer::measureStringWithEmotes(display, line, graphics::emotes, graphics::numEmotes, emoteSpacing); +} + +size_t UIRenderer::truncateStringWithEmotes(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, + const char *ellipsis, int emoteSpacing) +{ + return graphics::EmoteRenderer::truncateToWidth(display, line, out, outSize, maxWidth, ellipsis, graphics::emotes, + graphics::numEmotes, emoteSpacing); +} + +void UIRenderer::drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, int emoteSpacing, + bool fauxBold) +{ + graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, fontHeight, graphics::emotes, graphics::numEmotes, + emoteSpacing, fauxBold); +} + } // namespace graphics #endif // HAS_SCREEN diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 6e37b68f2..a705d944d 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -1,6 +1,7 @@ #pragma once #include "NodeDB.h" +#include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/emotes.h" #include @@ -49,7 +50,7 @@ class UIRenderer // Navigation bar overlay static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNodeInfo(OLEDDisplay *display, const 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); @@ -80,6 +81,28 @@ class UIRenderer static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); + // Shared BaseUI emote helpers. + static int measureStringWithEmotes(OLEDDisplay *display, const char *line, int emoteSpacing = 1); + static inline int measureStringWithEmotes(OLEDDisplay *display, const std::string &line, int emoteSpacing = 1) + { + return measureStringWithEmotes(display, line.c_str(), emoteSpacing); + } + static size_t truncateStringWithEmotes(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, + const char *ellipsis = "...", int emoteSpacing = 1); + static inline std::string truncateStringWithEmotes(OLEDDisplay *display, const std::string &line, int maxWidth, + const std::string &ellipsis = "...", int emoteSpacing = 1) + { + return graphics::EmoteRenderer::truncateToWidth(display, line, maxWidth, ellipsis, graphics::emotes, graphics::numEmotes, + emoteSpacing); + } + static void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, int emoteSpacing = 1, + bool fauxBold = true); + static inline void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, int fontHeight, + int emoteSpacing = 1, bool fauxBold = true) + { + drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSpacing, fauxBold); + } + // Check if the display can render a string (detect special chars; emoji) static bool haveGlyphs(const char *str); }; // namespace UIRenderer diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.cpp b/src/graphics/fonts/OLEDDisplayFontsRU.cpp index 3a1159511..9766d36b2 100644 --- a/src/graphics/fonts/OLEDDisplayFontsRU.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsRU.cpp @@ -10,7 +10,7 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = { 0xE0, // Number of chars: 224 // Jump Table: - 0xFF, 0xFF, 0x00, 0x0A, // 32 + 0xFF, 0xFF, 0x00, 0x03, // 32 0x00, 0x00, 0x04, 0x03, // 33 0x00, 0x04, 0x05, 0x04, // 34 0x00, 0x09, 0x09, 0x06, // 35 @@ -1766,4 +1766,4 @@ const uint8_t ArialMT_Plain_24_RU[] PROGMEM = { 0x3F, // 255 }; -#endif // OLED_RU \ No newline at end of file +#endif // OLED_RU diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.cpp b/src/graphics/fonts/OLEDDisplayFontsUA.cpp index 8bc56ea94..deafa77aa 100644 --- a/src/graphics/fonts/OLEDDisplayFontsUA.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsUA.cpp @@ -9,7 +9,7 @@ const uint8_t ArialMT_Plain_10_UA[] PROGMEM = { 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: - 0xFF, 0xFF, 0x00, 0x0A, // 32 + 0xFF, 0xFF, 0x00, 0x03, // 32 0x00, 0x00, 0x04, 0x03, // 33 0x00, 0x04, 0x05, 0x04, // 34 0x00, 0x09, 0x09, 0x06, // 35 @@ -1924,4 +1924,4 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { 0xFF, // 1103 }; -#endif // OLED_UA \ No newline at end of file +#endif // OLED_UA diff --git a/src/graphics/images.h b/src/graphics/images.h index 42dee004d..c4170642a 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -83,6 +83,12 @@ static const unsigned char mail[] PROGMEM = { 0b11111111, 0b00 // Bottom line }; +// Hop icon (9x10) +#define hop_width 9 +#define hop_height 10 +const uint8_t hop[] PROGMEM = {0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, + 0x38, 0x00, 0xC0, 0x01, 0x40, 0x01, 0xC0, 0x01, 0x40, 0x00}; + // 📬 Mail / Message const uint8_t icon_mail[] PROGMEM = { 0b11111111, // ████████ top border diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp index 6d9b709b1..ad92e28ea 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp @@ -42,7 +42,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused) { // Contingency only // - pin wasn't set - if (pin != (uint8_t)-1) { + if (pin != static_cast(-1)) { off(); pinMode(pin, INPUT); // High impedance - unnecessary? } else @@ -55,7 +55,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused) // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling void LatchingBacklight::peek() { - assert(pin != (uint8_t)-1); + assert(pin != static_cast(-1)); digitalWrite(pin, logicActive); // On on = true; latched = false; @@ -67,7 +67,7 @@ void LatchingBacklight::peek() // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling void LatchingBacklight::latch() { - assert(pin != (uint8_t)-1); + assert(pin != static_cast(-1)); // Blink if moving from peek to latch // Indicates to user that the transition has taken place @@ -89,7 +89,7 @@ void LatchingBacklight::latch() // Suitable for ending both peek and latch void LatchingBacklight::off() { - assert(pin != (uint8_t)-1); + assert(pin != static_cast(-1)); digitalWrite(pin, !logicActive); // Off on = false; latched = false; diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h index 0097cae4c..87862ea1b 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h @@ -40,7 +40,7 @@ class LatchingBacklight CallbackObserver deepSleepObserver = CallbackObserver(this, &LatchingBacklight::beforeDeepSleep); - uint8_t pin = (uint8_t)-1; + uint8_t pin = static_cast(-1); bool logicActive = HIGH; // Is light active HIGH or active LOW bool on = false; // Is light on (either peek or latched) diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h index 3ce16e473..e37969edf 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h @@ -37,8 +37,8 @@ class DEPG0213BNS800 : public SSD16XX void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; - void finalizeUpdate() override; // Only overriden for a slight optimization + void finalizeUpdate() override; // Only overridden for a slight optimization }; } // namespace NicheGraphics::Drivers -#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h index 257fed1a6..761cf772a 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h @@ -35,8 +35,8 @@ class DEPG0290BNS800 : public SSD16XX void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; - void finalizeUpdate() override; // Only overriden for a slight optimization + void finalizeUpdate() override; // Only overridden for a slight optimization }; } // namespace NicheGraphics::Drivers -#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/GDEW0102T4.cpp b/src/graphics/niche/Drivers/EInk/GDEW0102T4.cpp new file mode 100644 index 000000000..a670db0d0 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEW0102T4.cpp @@ -0,0 +1,178 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./GDEW0102T4.h" + +#include + +using namespace NicheGraphics::Drivers; + +// LUTs from GxEPD2_102.cpp (GDEW0102T4 / UC8175). +static const uint8_t LUT_W_FULL[] = { + 0x60, 0x5A, 0x5A, 0x00, 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +static const uint8_t LUT_B_FULL[] = { + 0x90, 0x5A, 0x5A, 0x00, 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +static const uint8_t LUT_W_FAST[] = { + 0x60, 0x01, 0x01, 0x00, 0x00, 0x01, // + 0x80, 0x12, 0x00, 0x00, 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +static const uint8_t LUT_B_FAST[] = { + 0x90, 0x01, 0x01, 0x00, 0x00, 0x01, // + 0x40, 0x14, 0x00, 0x00, 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +GDEW0102T4::GDEW0102T4() : UC8175(width, height, supported) {} + +void GDEW0102T4::setFastConfig(FastConfig cfg) +{ + // Clamp out only clearly invalid PLL settings. + if (cfg.reg30 < 0x05) + cfg.reg30 = 0x05; + fastConfig = cfg; +} + +GDEW0102T4::FastConfig GDEW0102T4::getFastConfig() const +{ + return fastConfig; +} + +void GDEW0102T4::configCommon() +{ + // Init path aligned with GxEPD2_GDEW0102T4 (UC8175 family). + sendCommand(0xD2); + sendData(0x3F); + + sendCommand(0x00); + sendData(0x6F); + + sendCommand(0x01); + sendData(0x03); + sendData(0x00); + sendData(0x2B); + sendData(0x2B); + + sendCommand(0x06); + sendData(0x3F); + + sendCommand(0x2A); + sendData(0x00); + sendData(0x00); + + sendCommand(0x30); // PLL / drive clock + sendData(0x13); + + sendCommand(0x50); // Last border/data interval; subtle but can affect artifacts + sendData(0x57); + + sendCommand(0x60); + sendData(0x22); + + sendCommand(0x61); + sendData(width); + sendData(height); + + sendCommand(0x82); // VCOM DC setting + sendData(0x12); + + sendCommand(0xE3); + sendData(0x33); +} + +void GDEW0102T4::configFull() +{ + sendCommand(0x23); + sendData(LUT_W_FULL, sizeof(LUT_W_FULL)); + sendCommand(0x24); + sendData(LUT_B_FULL, sizeof(LUT_B_FULL)); + + powerOn(); +} + +void GDEW0102T4::configFast() +{ + uint8_t lutW[sizeof(LUT_W_FAST)]; + uint8_t lutB[sizeof(LUT_B_FAST)]; + memcpy(lutW, LUT_W_FAST, sizeof(LUT_W_FAST)); + memcpy(lutB, LUT_B_FAST, sizeof(LUT_B_FAST)); + + // Second stage duration bytes are the main "darkness vs ghosting" control for this panel. + lutW[7] = fastConfig.lutW2; + lutB[7] = fastConfig.lutB2; + + sendCommand(0x30); + sendData(fastConfig.reg30); + + sendCommand(0x50); + sendData(fastConfig.reg50); + + sendCommand(0x82); + sendData(fastConfig.reg82); + + sendCommand(0x23); + sendData(lutW, sizeof(lutW)); + sendCommand(0x24); + sendData(lutB, sizeof(lutB)); + + powerOn(); +} + +void GDEW0102T4::writeOldImage() +{ + // On this panel, FULL refresh is most reliable when "old image" is all white. + if (updateType == FULL) { + sendCommand(0x10); + // Use buffered writes of 0xFF to avoid per-byte SPI transactions. + const uint16_t chunkSize = 64; + uint8_t ffBuf[chunkSize]; + memset(ffBuf, 0xFF, sizeof(ffBuf)); + + uint32_t remaining = bufferSize; + while (remaining > 0) { + uint16_t toSend = remaining > chunkSize ? chunkSize : static_cast(remaining); + sendData(ffBuf, toSend); + remaining -= toSend; + } + return; + } + + // FAST refresh uses differential data (previous frame as old image). + if (previousBuffer) { + writeImage(0x10, previousBuffer); + } else { + writeImage(0x10, buffer); + } +} + +void GDEW0102T4::finalizeUpdate() +{ + // Keep panel out of deep-sleep between updates for better reliability of repeated FAST refresh. + powerOff(); +} + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/GDEW0102T4.h b/src/graphics/niche/Drivers/EInk/GDEW0102T4.h new file mode 100644 index 000000000..02df8b4fe --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEW0102T4.h @@ -0,0 +1,55 @@ +/* + +E-Ink display driver + - GDEW0102T4 + - Controller: UC8175 + - Size: 1.02 inch + - Resolution: 80px x 128px + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./UC8175.h" + +namespace NicheGraphics::Drivers +{ + +class GDEW0102T4 : public UC8175 +{ + private: + static constexpr uint16_t width = 80; + static constexpr uint16_t height = 128; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + struct FastConfig { + uint8_t reg30; + uint8_t reg50; + uint8_t reg82; + uint8_t lutW2; + uint8_t lutB2; + }; + + GDEW0102T4(); + void setFastConfig(FastConfig cfg); + FastConfig getFastConfig() const; + + protected: + void configCommon() override; + void configFull() override; + void configFast() override; + void writeOldImage() override; + void finalizeUpdate() override; + + private: + FastConfig fastConfig = {0x13, 0xF2, 0x12, 0x0E, 0x14}; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/README.md b/src/graphics/niche/Drivers/EInk/README.md index eca91c6a8..33833f0cd 100644 --- a/src/graphics/niche/Drivers/EInk/README.md +++ b/src/graphics/niche/Drivers/EInk/README.md @@ -19,7 +19,7 @@ void setupNicheGraphics() SPIClass *hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // Setup Enk driver + // Setup EInk driver Drivers::EInk *driver = new Drivers::DEPG0290BNS800; driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); diff --git a/src/graphics/niche/Drivers/EInk/UC8175.cpp b/src/graphics/niche/Drivers/EInk/UC8175.cpp new file mode 100644 index 000000000..576b645bd --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/UC8175.cpp @@ -0,0 +1,203 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./UC8175.h" + +#include + +#include "SPILock.h" + +using namespace NicheGraphics::Drivers; + +UC8175::UC8175(uint16_t width, uint16_t height, UpdateTypes supported) : EInk(width, height, supported) +{ + bufferRowSize = ((width - 1) / 8) + 1; + bufferSize = bufferRowSize * height; +} + +void UC8175::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) +{ + this->spi = spi; + this->pin_dc = pin_dc; + this->pin_cs = pin_cs; + this->pin_busy = pin_busy; + this->pin_rst = pin_rst; + + pinMode(pin_dc, OUTPUT); + pinMode(pin_cs, OUTPUT); + pinMode(pin_busy, INPUT); + + // Reset is active LOW, hold HIGH when idle. + if (pin_rst != (uint8_t)-1) { + pinMode(pin_rst, OUTPUT); + digitalWrite(pin_rst, HIGH); + } + + if (!previousBuffer) { + previousBuffer = new uint8_t[bufferSize]; + if (previousBuffer) + memset(previousBuffer, 0xFF, bufferSize); + } +} + +void UC8175::update(uint8_t *imageData, UpdateTypes type) +{ + buffer = imageData; + updateType = (type == UpdateTypes::UNSPECIFIED) ? UpdateTypes::FULL : type; + + if (updateType == FAST && hasPreviousBuffer && previousBuffer && memcmp(previousBuffer, buffer, bufferSize) == 0) + return; + + reset(); + configCommon(); + + if (updateType == FAST) + configFast(); + else + configFull(); + + writeOldImage(); + writeNewImage(); + sendCommand(0x12); // Display refresh. + + if (previousBuffer) { + memcpy(previousBuffer, buffer, bufferSize); + hasPreviousBuffer = true; + } + + detachFromUpdate(); +} + +void UC8175::wait(uint32_t timeoutMs) +{ + if (failed) + return; + + uint32_t started = millis(); + while (digitalRead(pin_busy) == BUSY_ACTIVE) { + if ((millis() - started) > timeoutMs) { + failed = true; + break; + } + yield(); + } +} + +void UC8175::reset() +{ + if (pin_rst != (uint8_t)-1) { + digitalWrite(pin_rst, LOW); + delay(20); + digitalWrite(pin_rst, HIGH); + delay(20); + } else { + sendCommand(0x12); // Software reset. + delay(10); + } + + wait(3000); +} + +void UC8175::sendCommand(uint8_t command) +{ + if (failed) + return; + + spiLock->lock(); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, LOW); + digitalWrite(pin_cs, LOW); + spi->transfer(command); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); + spiLock->unlock(); +} + +void UC8175::sendData(uint8_t data) +{ + sendData(&data, 1); +} + +void UC8175::sendData(const uint8_t *data, uint32_t size) +{ + if (failed) + return; + + spiLock->lock(); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, HIGH); + digitalWrite(pin_cs, LOW); + +#if defined(ARCH_ESP32) + spi->transferBytes(data, NULL, size); +#elif defined(ARCH_NRF52) + spi->transfer(data, NULL, size); +#else + for (uint32_t i = 0; i < size; ++i) + spi->transfer(data[i]); +#endif + + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); + spiLock->unlock(); +} + +void UC8175::powerOn() +{ + sendCommand(0x04); + wait(2000); +} + +void UC8175::powerOff() +{ + sendCommand(0x02); // Power off. + wait(1500); +} + +void UC8175::writeImage(uint8_t command, const uint8_t *image) +{ + sendCommand(command); + sendData(image, bufferSize); +} + +void UC8175::writeOldImage() +{ + if (updateType == FAST && previousBuffer) + writeImage(0x10, previousBuffer); + else + writeImage(0x10, buffer); +} + +void UC8175::writeNewImage() +{ + writeImage(0x13, buffer); +} + +void UC8175::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 400); + case FULL: + default: + return beginPolling(100, 2000); + } +} + +bool UC8175::isUpdateDone() +{ + return digitalRead(pin_busy) != BUSY_ACTIVE; +} + +void UC8175::finalizeUpdate() +{ + powerOff(); + + if (pin_rst != (uint8_t)-1) { + sendCommand(0x07); // Deep sleep. + sendData(0xA5); + } +} + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/UC8175.h b/src/graphics/niche/Drivers/EInk/UC8175.h new file mode 100644 index 000000000..b248d4bea --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/UC8175.h @@ -0,0 +1,62 @@ +// E-Ink base class for displays based on UC8175 / UC8176 style controller ICs. + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./EInk.h" + +namespace NicheGraphics::Drivers +{ + +class UC8175 : public EInk +{ + public: + UC8175(uint16_t width, uint16_t height, UpdateTypes supported); + void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) override; + void update(uint8_t *imageData, UpdateTypes type) override; + + protected: + virtual void wait(uint32_t timeoutMs = 1000); + virtual void reset(); + virtual void sendCommand(uint8_t command); + virtual void sendData(uint8_t data); + virtual void sendData(const uint8_t *data, uint32_t size); + + virtual void configCommon() = 0; // Always run + virtual void configFull() = 0; // Run when updateType == FULL + virtual void configFast() = 0; // Run when updateType == FAST + + virtual void powerOn(); + virtual void powerOff(); + virtual void writeOldImage(); + virtual void writeNewImage(); + virtual void writeImage(uint8_t command, const uint8_t *image); + + virtual void detachFromUpdate(); + virtual bool isUpdateDone() override; + virtual void finalizeUpdate() override; + + protected: + static constexpr uint8_t BUSY_ACTIVE = LOW; + + uint16_t bufferRowSize = 0; + uint32_t bufferSize = 0; + uint8_t *buffer = nullptr; + uint8_t *previousBuffer = nullptr; + bool hasPreviousBuffer = false; + UpdateTypes updateType = UpdateTypes::UNSPECIFIED; + + uint8_t pin_dc = (uint8_t)-1; + uint8_t pin_cs = (uint8_t)-1; + uint8_t pin_busy = (uint8_t)-1; + uint8_t pin_rst = (uint8_t)-1; + SPIClass *spi = nullptr; + SPISettings spiSettings = SPISettings(8000000, MSBFIRST, SPI_MODE0); +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index 1e89ebe1b..0a9cd3add 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -1,3 +1,5 @@ +#include "graphics/niche/InkHUD/Tile.h" +#include #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Applet.h" @@ -32,7 +34,7 @@ void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) { // Only render pixels if they fall within user's cropped region if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) - assignedTile->handleAppletPixel(x, y, (Color)color); + assignedTile->handleAppletPixel(x, y, static_cast(color)); } // Link our applet to a tile @@ -55,7 +57,7 @@ InkHUD::Tile *InkHUD::Applet::getTile() } // Draw the applet -void InkHUD::Applet::render() +void InkHUD::Applet::render(bool full) { assert(assignedTile); // Ensure that we have a tile assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile @@ -65,10 +67,11 @@ void InkHUD::Applet::render() wantRender = false; // Flag set by requestUpdate wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. + wantFullRender = true; // Default to a full render updateDimensions(); resetDrawingSpace(); - onRender(); // Derived applet's drawing takes place here + onRender(full); // Draw the applet // Handle "Tile Highlighting" // Some devices may use an auxiliary button to switch between tiles @@ -115,6 +118,11 @@ Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() return wantUpdateType; } +bool InkHUD::Applet::wantsFullRender() +{ + return wantFullRender; +} + // Get size of the applet's drawing space from its tile // Performed immediately before derived applet's drawing code runs void InkHUD::Applet::updateDimensions() @@ -136,16 +144,32 @@ void InkHUD::Applet::resetDrawingSpace() setFont(fontSmall); } +// Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it +void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured) +{ + if (captured) + subscribedInputs |= input; + else + subscribedInputs &= ~input; +} + +// Checks if a specific input is enabled for this applet and should be sent to it +bool InkHUD::Applet::isInputSubscribed(InputMask input) +{ + return (subscribedInputs & input) == input; +} + // Tell InkHUD::Renderer that we want to render now // Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc // When an applet decides it has heard something important, and wants to redraw, it calls this method // Once the renderer has given other applets a chance to process whatever event we just detected, // it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground) // We should requestUpdate even if our applet is currently background, because this might be changed by autoshow -void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type) +void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full) { wantRender = true; wantUpdateType = type; + wantFullRender = full; inkhud->requestUpdate(); } @@ -303,7 +327,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalA } // Print text, specifying the position of any edge / corner of the textbox -void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) +void InkHUD::Applet::printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha, VerticalAlignment va) { printAt(x, y, text.c_str(), ha, va); } @@ -325,7 +349,7 @@ InkHUD::AppletFont InkHUD::Applet::getFont() // Parse any text which might have "special characters" // Re-encodes UTF-8 characters to match our 8-bit encoded fonts -std::string InkHUD::Applet::parse(std::string text) +std::string InkHUD::Applet::parse(const std::string &text) { return getFont().decodeUTF8(text); } @@ -352,10 +376,10 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) } // Determine if all characters of a string are printable using the current font -bool InkHUD::Applet::isPrintable(std::string text) +bool InkHUD::Applet::isPrintable(const std::string &text) { // Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled - for (char &c : text) { + for (const char &c : text) { if (c == '\x1A') return false; } @@ -378,7 +402,7 @@ uint16_t InkHUD::Applet::getTextWidth(const char *text) // Gets rendered width of a string // Wrapper for getTextBounds -uint16_t InkHUD::Applet::getTextWidth(std::string text) +uint16_t InkHUD::Applet::getTextWidth(const std::string &text) { return getTextWidth(text.c_str()); } @@ -426,7 +450,7 @@ std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) // Print text, with word wrapping // Avoids splitting words in half, instead moving the entire word to a new line wherever possible -void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text) +void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text) { // Place the AdafruitGFX cursor to suit our "top" coord setCursor(left, top + getFont().heightAboveCursor()); @@ -483,15 +507,15 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std // Todo: rewrite making use of AdafruitGFX native text wrapping char cstr[] = {0, 0}; - int16_t l, t; - uint16_t w, h; + int16_t bx, by; + uint16_t bw, bh; for (uint16_t c = 0; c < word.length(); c++) { // Shove next char into a c string cstr[0] = word[c]; - getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h); + getTextBounds(cstr, getCursorX(), getCursorY(), &bx, &by, &bw, &bh); // Manual newline, if next character will spill beyond screen edge - if ((l + w) > left + width) + if ((bx + bw) > left + width) setCursor(left, getCursorY() + getFont().lineHeight()); // Print next character @@ -510,7 +534,7 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std // Simulate running printWrapped, to determine how tall the block of text will be. // This is a wasteful way of handling things. Maybe some way to optimize in future? -uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text) +uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text) { // Cache the current crop region int16_t cL = cropLeft; @@ -640,7 +664,7 @@ uint16_t InkHUD::Applet::getActiveNodeCount() // For each node in db for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Check if heard recently, and not our own node if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) @@ -693,7 +717,7 @@ std::string InkHUD::Applet::localizeDistance(uint32_t meters) } // Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly -void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY) +void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, uint8_t thicknessY) { // How many times to draw along x axis int16_t xStart; @@ -761,7 +785,7 @@ bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) │ │ └───────────────────────────────┘ */ -void InkHUD::Applet::drawHeader(std::string text) +void InkHUD::Applet::drawHeader(const std::string &text) { // Y position for divider // - between header text and messages @@ -778,6 +802,16 @@ void InkHUD::Applet::drawHeader(std::string text) drawPixel(x, 0, BLACK); drawPixel(x, headerDivY, BLACK); // Dotted 50% } + + // Dither near battery + if (settings->optionalFeatures.batteryIcon) { + constexpr uint16_t ditherSizePx = 4; + Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile(); + const uint16_t batteryTileLeft = batteryTile->getLeft(); + const uint16_t batteryTileTop = batteryTile->getTop(); + const uint16_t batteryTileHeight = batteryTile->getHeight(); + hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE); + } } // Get the height of the standard applet header diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index b35ca5cc0..3c14c2607 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -3,7 +3,7 @@ /* Base class for InkHUD applets - Must be overriden + Must be overridden An applet is one "program" which may show info on the display. @@ -15,6 +15,7 @@ #include // GFXRoot drawing lib +#include "mesh/MeshModule.h" #include "mesh/MeshTypes.h" #include "./AppletFont.h" @@ -64,10 +65,11 @@ class Applet : public GFX // Rendering - void render(); // Draw the applet + void render(bool full); // Draw the applet bool wantsToRender(); // Check whether applet wants to render bool wantsToAutoshow(); // Check whether applet wants to become foreground Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer + bool wantsFullRender(); // Check whether applet wants to render over its previous render void updateDimensions(); // Get current size from tile void resetDrawingSpace(); // Makes sure every render starts with same parameters @@ -82,12 +84,15 @@ class Applet : public GFX // Event handlers - virtual void onRender() = 0; // All drawing happens here + virtual void onRender(bool full) = 0; // For drawing the applet virtual void onActivate() {} virtual void onDeactivate() {} virtual void onForeground() {} virtual void onBackground() {} virtual void onShutdown() {} + + // Input Events + virtual void onButtonShortPress() {} virtual void onButtonLongPress() {} virtual void onExitShort() {} @@ -96,6 +101,21 @@ class Applet : public GFX virtual void onNavDown() {} virtual void onNavLeft() {} virtual void onNavRight() {} + virtual void onFreeText(char c) {} + virtual void onFreeTextDone() {} + virtual void onFreeTextCancel() {} + // List of inputs which can be subscribed to + enum InputMask { // | No Joystick | With Joystick | + BUTTON_SHORT = 1, // | Button Click | Joystick Center Click | + BUTTON_LONG = 2, // | Button Hold | Joystick Center Hold | + EXIT_SHORT = 4, // | no-op | Back Button Click | + EXIT_LONG = 8, // | no-op | Back Button Hold | + NAV_UP = 16, // | no-op | Joystick Up | + NAV_DOWN = 32, // | no-op | Joystick Down | + NAV_LEFT = 64, // | no-op | Joystick Left | + NAV_RIGHT = 128 // | no-op | Joystick Right | + }; + bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded. virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification @@ -108,28 +128,37 @@ class Applet : public GFX protected: void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here - void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update - void requestAutoshow(); // Ask for applet to be moved to foreground + void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED, + bool full = true); // Ask WindowManager to schedule a display update + void requestAutoshow(); // Ask for applet to be moved to foreground uint16_t X(float f); // Map applet width, mapped from 0 to 1.0 uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0 void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region void resetCrop(); // Removes setCrop() + // User Input Handling + + uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs + void setInputsSubscribed(uint8_t input, + bool captured); // Set if an input should be handled by applet or not, this should not be + // overloaded. Can take multiple inputs at once if you OR/`|` them together + // Text void setFont(AppletFont f); AppletFont getFont(); - uint16_t getTextWidth(std::string text); + uint16_t getTextWidth(const std::string &text); uint16_t getTextWidth(const char *text); - uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped + uint32_t getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text); // Result of printWrapped void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); - void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); - void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold - void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping + void printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); + void printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, + uint8_t thicknessY); // Faux bold + void printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text); // Per-word line wrapping void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines - void drawHeader(std::string text); // Draw the standard applet header + void drawHeader(const std::string &text); // Draw the standard applet header // Meshtastic Logo @@ -145,9 +174,9 @@ class Applet : public GFX std::string getTimeString(); // Current time, human readable uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric - std::string parse(std::string text); // Handle text which might contain special chars + std::string parse(const std::string &text); // Handle text which might contain special chars std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars - bool isPrintable(std::string); // Check for characters which the font can't print + bool isPrintable(const std::string &text); // Check for characters which the font can't print // Convenient references @@ -164,6 +193,7 @@ class Applet : public GFX bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground? NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType = NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display + bool wantFullRender = true; // Render with a fresh canvas using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager. @@ -179,4 +209,4 @@ class Applet : public GFX }; // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index 93a621ee8..188671a0e 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -39,11 +39,11 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding // Caution: signed and unsigned types int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; if (glyphAscender > 0) - this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender); + this->ascenderHeight = max(this->ascenderHeight, static_cast(glyphAscender)); int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset; if (glyphDescender > 0) - this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender); + this->descenderHeight = max(this->descenderHeight, static_cast(glyphDescender)); } // Apply any manual padding to grow or shrink the line size @@ -52,7 +52,7 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding descenderHeight += paddingBottom; // Find how far the cursor advances when we "print" a space character - spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance; + spaceCharWidth = gfxFont->glyph[static_cast(' ') - gfxFont->first].xAdvance; } /* @@ -98,7 +98,7 @@ uint8_t InkHUD::AppletFont::widthBetweenWords() // Convert a unicode char from set of UTF-8 bytes to UTF-32 // Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value -uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) +uint32_t InkHUD::AppletFont::toUtf32(const std::string &utf8) { uint32_t utf32 = 0; @@ -132,7 +132,7 @@ uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) // Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII // Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars -std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) +std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded) { // Final processed output std::string decoded; @@ -141,7 +141,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) std::string utf8Char; uint8_t utf8CharSize = 0; - for (char &c : encoded) { + for (const char &c : encoded) { // If first byte if (utf8Char.empty()) { @@ -178,7 +178,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) // Re-encode a single UTF-8 character to extended ASCII // Target encoding depends on the font -char InkHUD::AppletFont::applyEncoding(std::string utf8) +char InkHUD::AppletFont::applyEncoding(const std::string &utf8) { // ##################################################### Syntactic Sugar ##################################################### #define REMAP(in, out) \ diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index 02ba13c31..8374c7f61 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -30,20 +30,21 @@ class AppletFont }; AppletFont(); - AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0); + explicit AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, + int8_t paddingBottom = 0); uint8_t lineHeight(); uint8_t heightAboveCursor(); uint8_t heightBelowCursor(); uint8_t widthBetweenWords(); // Width of the space character - std::string decodeUTF8(std::string encoded); + std::string decodeUTF8(const std::string &encoded); - const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font + const GFXfont *gfxFont = nullptr; // Default value: in-built AdafruitGFX font private: - uint32_t toUtf32(std::string utf8); - char applyEncoding(std::string utf8); + uint32_t toUtf32(const std::string &utf8); + char applyEncoding(const std::string &utf8); uint8_t height = 8; // Default value: in-built AdafruitGFX font uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index d383a11e4..06ddd5bb0 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -4,7 +4,7 @@ using namespace NicheGraphics; -void InkHUD::MapApplet::onRender() +void InkHUD::MapApplet::onRender(bool full) { // Abort if no markers to render if (!enoughMarkers()) { @@ -525,7 +525,7 @@ void InkHUD::MapApplet::calculateAllMarkers() } // Determine the conversion factor between metres, and pixels on screen -// May be overriden by derived applet, if custom scale required (fixed map size?) +// May be overridden by derived applet, if custom scale required (fixed map size?) void InkHUD::MapApplet::calculateMapScale() { // Aspect ratio of map and screen @@ -555,4 +555,4 @@ void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) drawLine(x0, y1, x1, y0, BLACK); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h index f45a36071..11dfb39d9 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h @@ -27,7 +27,7 @@ namespace NicheGraphics::InkHUD class MapApplet : public Applet { public: - void onRender() override; + void onRender(bool full) override; protected: virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 5c9906fba..607fd4ef7 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -81,29 +81,29 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke uint8_t InkHUD::NodeListApplet::maxCards() { // Cache result. Shouldn't change during execution - static uint8_t cards = 0; + static uint8_t maxCardCount = 0; - if (!cards) { + if (!maxCardCount) { const uint16_t height = Tile::maxDisplayDimension(); // Use a loop instead of arithmetic, because it's easier for my brain to follow // Add cards one by one, until the latest card extends below screen uint16_t y = cardH; // First card: no margin above - cards = 1; + maxCardCount = 1; while (y < height) { y += cardMarginH; y += cardH; - cards++; + maxCardCount++; } } - return cards; + return maxCardCount; } // Draw, using info which derived applet placed into NodeListApplet::cards for us -void InkHUD::NodeListApplet::onRender() +void InkHUD::NodeListApplet::onRender(bool full) { // ================================ @@ -120,9 +120,26 @@ void InkHUD::NodeListApplet::onRender() // Draw the main node list // ======================== - // Imaginary vertical line dividing left-side and right-side info - // Long-name will crop here - const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops"); + // Leave a small gutter between long-name text and right-side card content + constexpr uint8_t rightContentGap = 2; + + // Truncate with trailing "...", sized using the current font. + auto ellipsizeToWidth = [this](std::string text, uint16_t maxWidth) { + constexpr const char *ellipsis = "..."; + const uint16_t ellipsisW = getTextWidth(ellipsis); + uint16_t textW = getTextWidth(text); + if (maxWidth == 0) + return std::string(); + if (textW <= maxWidth) + return text; + if (ellipsisW > maxWidth) + return std::string(); + while (!text.empty() && (textW + ellipsisW > maxWidth)) { + text.pop_back(); + textW = getTextWidth(text); + } + return text + ellipsis; + }; // Y value (top) of the current card. Increases as we draw. uint16_t cardTopY = headerDivY + padDivH; @@ -137,12 +154,12 @@ void InkHUD::NodeListApplet::onRender() // Gather info // ======================================== - NodeNum &nodeNum = card->nodeNum; + const NodeNum &nodeNum = card->nodeNum; SignalStrength &signal = card->signal; std::string longName; // handled below std::string shortName; // handled below - std::string distance; // handled below; - uint8_t &hopsAway = card->hopsAway; + std::string distance; // handled below + const uint8_t &hopsAway = card->hopsAway; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); @@ -185,41 +202,49 @@ void InkHUD::NodeListApplet::onRender() setFont(fontMedium); printAt(0, lineAY, shortName, LEFT, MIDDLE); - // Print the distance + // Right-side labels and long name are rendered in small font. setFont(fontSmall); - printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); + uint16_t rightContentW = 0; - // If we have a direct connection to the node, draw the signal indicator + // Bottom row right: distance. + if (!distance.empty()) { + rightContentW = std::max(rightContentW, getTextWidth(distance)); + printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); + } + + // Top row right: direct-link signal only. if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { - uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label + uint16_t signalW = getTextWidth("Xkm"); // Indicator width tuned to a short right-side label uint16_t signalH = fontMedium.lineHeight() * 0.75; int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); int16_t signalX = width() - signalW; + rightContentW = std::max(rightContentW, signalW); drawSignalIndicator(signalX, signalY, signalW, signalH, signal); - } - // Otherwise, print "hops away" info, if available - else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) { - std::string hopString = to_string(node->hops_away); - hopString += " Hop"; - if (node->hops_away != 1) - hopString += "s"; // Append s for "Hops", rather than "Hop" - + } else if (hopsAway != CardInfo::HOPS_UNKNOWN) { + std::string hopString = to_string(hopsAway) + (hopsAway == 1 ? " Hop" : " Hops"); + rightContentW = std::max(rightContentW, getTextWidth(hopString)); printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); } - // Print the long name, cropping to prevent overflow onto the right-side info - setCrop(0, 0, dividerX - 1, height()); - printAt(0, lineBY, longName, LEFT, MIDDLE); + // Give long names as much room as possible while still avoiding right side signal and hop space + const uint16_t longNameMaxW = + (rightContentW + rightContentGap < width()) ? (width() - rightContentW - rightContentGap) : 0; + const std::string longNameShown = ellipsizeToWidth(longName, longNameMaxW); - // GFX effect: "hatch" the right edge of longName area - // If a longName has been cropped, it will appear to fade out, - // creating a soft barrier with the right-side info - const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight()); - const int16_t hatchWidth = fontSmall.lineHeight(); - hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE); + // Safety crop + setCrop(0, cardTopY, longNameMaxW, cardH); + printAt(0, lineBY, longNameShown, LEFT, MIDDLE); + + resetCrop(); + + // Draw separator between cards + const int16_t separatorY = cardTopY + cardH - 1; + if (separatorY < height() - 1 && (card + 1) != cards.end()) { + for (int16_t xSep = 0; xSep < width(); xSep += 2) + drawPixel(xSep, separatorY, BLACK); + } // Prepare to draw the next card - resetCrop(); cardTopY += cardH; // Once we've run out of screen, stop drawing cards @@ -288,4 +313,4 @@ void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t } } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h index c2340027b..baee3f0f4 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h @@ -46,7 +46,7 @@ class NodeListApplet : public Applet, public MeshModule public: NodeListApplet(const char *name); - void onRender() override; + void onRender(bool full) override; bool wantPacket(const meshtastic_MeshPacket *p) override; ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; @@ -65,10 +65,10 @@ class NodeListApplet : public Applet, public MeshModule // Card Dimensions // - for rendering and for maxCards calc - uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards + uint8_t cardMarginH = 1; // Gap between cards (minimal to fit more rows) uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card }; } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp index c52719e55..71b6d9a7a 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.cpp @@ -6,7 +6,7 @@ using namespace NicheGraphics; // All drawing happens here // Our basic example doesn't do anything useful. It just passively prints some text. -void InkHUD::BasicExampleApplet::onRender() +void InkHUD::BasicExampleApplet::onRender(bool full) { printAt(0, 0, "Hello, World!"); diff --git a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h index aed63cdc8..a36f6e8d5 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h @@ -28,7 +28,7 @@ class BasicExampleApplet : public Applet // You must have an onRender() method // All drawing happens here - void onRender() override; + void onRender(bool full) override; }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp index 6b02f4c92..cf3fd7714 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.cpp @@ -35,7 +35,7 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh // We can trigger a render by calling requestUpdate() // Render might be called by some external source // We should always be ready to draw -void InkHUD::NewMsgExampleApplet::onRender() +void InkHUD::NewMsgExampleApplet::onRender(bool full) { printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0) diff --git a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h index 22670a0f0..599f08a7a 100644 --- a/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h @@ -34,7 +34,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {} // All drawing happens here - void onRender() override; + void onRender(bool full) override; // Your applet might also want to use some of these // Useful for setting up or tidying up diff --git a/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.cpp b/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.cpp new file mode 100644 index 000000000..79133719a --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.cpp @@ -0,0 +1,79 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD +#include "./UserAppletInputExample.h" + +using namespace NicheGraphics; + +void InkHUD::UserAppletInputExampleApplet::onActivate() +{ + setGrabbed(false); +} + +void InkHUD::UserAppletInputExampleApplet::onRender(bool full) +{ + drawHeader("Input Example"); + uint16_t headerHeight = getHeaderHeight(); + + std::string buttonName; + if (settings->joystick.enabled) + buttonName = "joystick center button"; + else + buttonName = "user button"; + + std::string additional = " | Control is grabbed, long press " + buttonName + " to release controls"; + if (!isGrabbed) + additional = " | Control is released, long press " + buttonName + " to grab controls"; + + printWrapped(0, headerHeight, width(), "Last button: " + lastInput + additional); +} + +void InkHUD::UserAppletInputExampleApplet::setGrabbed(bool grabbed) +{ + isGrabbed = grabbed; + setInputsSubscribed(BUTTON_SHORT | EXIT_SHORT | EXIT_LONG | NAV_UP | NAV_DOWN | NAV_LEFT | NAV_RIGHT, + grabbed); // Enables/disables grabbing all inputs + setInputsSubscribed(BUTTON_LONG, true); // Always grab this input +} + +void InkHUD::UserAppletInputExampleApplet::onButtonShortPress() +{ + lastInput = "BUTTON_SHORT"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onButtonLongPress() +{ + lastInput = "BUTTON_LONG"; + setGrabbed(!isGrabbed); + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onExitShort() +{ + lastInput = "EXIT_SHORT"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onExitLong() +{ + lastInput = "EXIT_LONG"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onNavUp() +{ + lastInput = "NAV_UP"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onNavDown() +{ + lastInput = "NAV_DOWN"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onNavLeft() +{ + lastInput = "NAV_LEFT"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onNavRight() +{ + lastInput = "NAV_RIGHT"; + requestUpdate(); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h b/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h new file mode 100644 index 000000000..a99dec00c --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h @@ -0,0 +1,36 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class UserAppletInputExampleApplet : public Applet +{ + public: + void onActivate() override; + + void onRender(bool full) override; + void onButtonShortPress() override; + void onButtonLongPress() override; + void onExitShort() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; + + private: + std::string lastInput = "None"; + bool isGrabbed = false; + + void setGrabbed(bool grabbed); +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp index 67ef87f41..3afa80149 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -10,7 +10,7 @@ InkHUD::AlignStickApplet::AlignStickApplet() bringToForeground(); } -void InkHUD::AlignStickApplet::onRender() +void InkHUD::AlignStickApplet::onRender(bool full) { setFont(fontMedium); printAt(0, 0, "Align Joystick:"); @@ -152,19 +152,17 @@ void InkHUD::AlignStickApplet::onBackground() // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } void InkHUD::AlignStickApplet::onButtonLongPress() { sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::AlignStickApplet::onExitLong() { sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::AlignStickApplet::onNavUp() @@ -172,7 +170,6 @@ void InkHUD::AlignStickApplet::onNavUp() settings->joystick.aligned = true; sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::AlignStickApplet::onNavDown() @@ -181,7 +178,6 @@ void InkHUD::AlignStickApplet::onNavDown() settings->joystick.aligned = true; sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::AlignStickApplet::onNavLeft() @@ -190,7 +186,6 @@ void InkHUD::AlignStickApplet::onNavLeft() settings->joystick.aligned = true; sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::AlignStickApplet::onNavRight() @@ -199,7 +194,6 @@ void InkHUD::AlignStickApplet::onNavRight() settings->joystick.aligned = true; sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h index 8dba33165..7c8d00155 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -23,7 +23,7 @@ class AlignStickApplet : public SystemApplet public: AlignStickApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonLongPress() override; diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp index 4f99d99ee..0b9607133 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp @@ -6,6 +6,8 @@ using namespace NicheGraphics; InkHUD::BatteryIconApplet::BatteryIconApplet() { + alwaysRender = true; // render every time the screen is updated + // Show at boot, if user has previously enabled the feature if (settings->optionalFeatures.batteryIcon) bringToForeground(); @@ -27,10 +29,10 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta // If we get a different type of status, something has gone weird elsewhere assert(status->getStatusType() == STATUS_TYPE_POWER); - meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status; + const meshtastic::PowerStatus *pwrStatus = (const meshtastic::PowerStatus *)status; // Get the new state of charge %, and round to the nearest 10% - uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10; + uint8_t newSocRounded = ((pwrStatus->getBatteryChargePercent() + 5) / 10) * 10; // If rounded value has changed, trigger a display update // It's okay to requestUpdate before we store the new value, as the update won't run until next loop() @@ -44,39 +46,29 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta return 0; // Tell Observable to continue informing other observers } -void InkHUD::BatteryIconApplet::onRender() +void InkHUD::BatteryIconApplet::onRender(bool full) { - // Fill entire tile - // - size of icon controlled by size of tile - int16_t l = 0; - int16_t t = 0; - uint16_t w = width(); - int16_t h = height(); - - // Clear the region beneath the tile + // Clear the region beneath the tile, including the border // Most applets are drawing onto an empty frame buffer and don't need to do this // We do need to do this with the battery though, as it is an "overlay" - fillRect(l, t, w, h, WHITE); - - // Vertical centerline - const int16_t m = t + (h / 2); + fillRect(0, 0, width(), height(), WHITE); // ===================== // Draw battery outline // ===================== // Positive terminal "bump" - const int16_t &bumpL = l; - const uint16_t bumpH = h / 2; - const int16_t bumpT = m - (bumpH / 2); constexpr uint16_t bumpW = 2; + const int16_t &bumpL = 1; + const uint16_t bumpH = (height() - 2) / 2; + const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2); fillRect(bumpL, bumpT, bumpW, bumpH, BLACK); // Main body of battery - const int16_t bodyL = bumpL + bumpW; - const int16_t &bodyT = t; - const int16_t &bodyH = h; - const int16_t bodyW = w - bumpW; + const int16_t bodyL = 1 + bumpW; + const int16_t &bodyT = 1; + const int16_t &bodyH = height() - 2; // Handle top/bottom padding + const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); // Erase join between bump and body @@ -87,15 +79,16 @@ void InkHUD::BatteryIconApplet::onRender() // =================== constexpr int16_t slicePad = 2; - const int16_t sliceL = bodyL + slicePad; + int16_t sliceL = bodyL + slicePad; const int16_t sliceT = bodyT + slicePad; const uint16_t sliceH = bodyH - (slicePad * 2); uint16_t sliceW = bodyW - (slicePad * 2); - sliceW = (sliceW * socRounded) / 100; // Apply percentage + sliceW = (sliceW * socRounded) / 100; // Apply percentage + sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK); drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h index e5b4172be..ceaf88d7f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.h @@ -23,7 +23,7 @@ class BatteryIconApplet : public SystemApplet public: BatteryIconApplet(); - void onRender() override; + void onRender(bool full) override; int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available private: diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp new file mode 100644 index 000000000..57581d56b --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.cpp @@ -0,0 +1,257 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD +#include "./KeyboardApplet.h" + +using namespace NicheGraphics; + +InkHUD::KeyboardApplet::KeyboardApplet() +{ + // Calculate row widths + for (uint8_t row = 0; row < KBD_ROWS; row++) { + rowWidths[row] = 0; + for (uint8_t col = 0; col < KBD_COLS; col++) + rowWidths[row] += keyWidths[row * KBD_COLS + col]; + } +} + +void InkHUD::KeyboardApplet::onRender(bool full) +{ + uint16_t em = fontSmall.lineHeight(); // 16 pt + uint16_t keyH = Y(1.0) / KBD_ROWS; + int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2; + + if (full) { // Draw full keyboard + for (uint8_t row = 0; row < KBD_ROWS; row++) { + + // Calculate the remaining space to be used as padding + int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); + + // Draw keys + uint16_t xPos = 0; + for (uint8_t col = 0; col < KBD_COLS; col++) { + Color fgcolor = BLACK; + uint8_t index = row * KBD_COLS + col; + uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1)); + uint16_t keyY = row * keyH; + uint16_t keyW = (keyWidths[index] * em) >> 4; + if (index == selectedKey) { + fgcolor = WHITE; + fillRect(keyX, keyY, keyW, keyH, BLACK); + } + drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor); + xPos += keyWidths[index]; + } + } + } else { // Only draw the difference + if (selectedKey != prevSelectedKey) { + // Draw previously selected key + uint8_t row = prevSelectedKey / KBD_COLS; + int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); + uint16_t xPos = 0; + for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++) + xPos += keyWidths[i]; + uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1)); + uint16_t keyY = row * keyH; + uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4; + fillRect(keyX, keyY, keyW, keyH, WHITE); + drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK); + + // Draw newly selected key + row = selectedKey / KBD_COLS; + keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4); + xPos = 0; + for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++) + xPos += keyWidths[i]; + keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1)); + keyY = row * keyH; + keyW = (keyWidths[selectedKey] * em) >> 4; + fillRect(keyX, keyY, keyW, keyH, BLACK); + drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE); + } + } + + prevSelectedKey = selectedKey; +} + +// Draw the key label corresponding to the char +// for most keys it draws the character itself +// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs +void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color) +{ + if (key == '\b') { + // Draw backspace glyph: 13 x 9 px + /** + * [][][][][][][][][] + * [][] [] + * [][] [] [] [] + * [][] [] [] [] + * [][] [] [] + * [][] [] [] [] + * [][] [] [] [] + * [][] [] + * [][][][][][][][][] + */ + const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0, + 0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8}; + uint16_t leftPadding = (width - 13) >> 1; + drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color); + } else if (key == '\n') { + // Draw done glyph: 12 x 9 px + /** + * [][] + * [][] + * [][] + * [][] + * [][] + * [][] [][] + * [][] [][] + * [][][] + * [] + */ + const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03, + 0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00}; + uint16_t leftPadding = (width - 12) >> 1; + drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color); + } else if (key == ' ') { + // Draw space glyph: 13 x 9 px + /** + * + * + * + * + * [] [] + * [] [] + * [][][][][][][][][][][][][] + * + * + */ + const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00}; + uint16_t leftPadding = (width - 13) >> 1; + drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color); + } else if (key == '\x1b') { + setTextColor(color); + std::string keyText = "ESC"; + uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1; + printAt(left + leftPadding, top, keyText); + } else { + setTextColor(color); + if (key >= 0x61) + key -= 32; // capitalize + std::string keyText = std::string(1, key); + uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1; + printAt(left + leftPadding, top, keyText); + } +} + +void InkHUD::KeyboardApplet::onForeground() +{ + handleInput = true; // Intercept the button input for our applet + + // Select the first key + selectedKey = 0; + prevSelectedKey = 0; +} + +void InkHUD::KeyboardApplet::onBackground() +{ + handleInput = false; +} + +void InkHUD::KeyboardApplet::onButtonShortPress() +{ + char key = keys[selectedKey]; + if (key == '\n') { + inkhud->freeTextDone(); + inkhud->closeKeyboard(); + } else if (key == '\x1b') { + inkhud->freeTextCancel(); + inkhud->closeKeyboard(); + } else { + inkhud->freeText(key); + } +} + +void InkHUD::KeyboardApplet::onButtonLongPress() +{ + char key = keys[selectedKey]; + if (key == '\n') { + inkhud->freeTextDone(); + inkhud->closeKeyboard(); + } else if (key == '\x1b') { + inkhud->freeTextCancel(); + inkhud->closeKeyboard(); + } else { + if (key >= 0x61) + key -= 32; // capitalize + inkhud->freeText(key); + } +} + +void InkHUD::KeyboardApplet::onExitShort() +{ + inkhud->freeTextCancel(); + inkhud->closeKeyboard(); +} + +void InkHUD::KeyboardApplet::onExitLong() +{ + inkhud->freeTextCancel(); + inkhud->closeKeyboard(); +} + +void InkHUD::KeyboardApplet::onNavUp() +{ + if (selectedKey < KBD_COLS) // wrap + selectedKey += KBD_COLS * (KBD_ROWS - 1); + else // move 1 row back + selectedKey -= KBD_COLS; + + // Request rendering over the previously drawn render + requestUpdate(EInk::UpdateTypes::FAST, false); + // Force an update to bypass lockRequests + inkhud->forceUpdate(EInk::UpdateTypes::FAST); +} + +void InkHUD::KeyboardApplet::onNavDown() +{ + selectedKey += KBD_COLS; + selectedKey %= (KBD_COLS * KBD_ROWS); + + // Request rendering over the previously drawn render + requestUpdate(EInk::UpdateTypes::FAST, false); + // Force an update to bypass lockRequests + inkhud->forceUpdate(EInk::UpdateTypes::FAST); +} + +void InkHUD::KeyboardApplet::onNavLeft() +{ + if (selectedKey % KBD_COLS == 0) // wrap + selectedKey += KBD_COLS - 1; + else // move 1 column back + selectedKey--; + + // Request rendering over the previously drawn render + requestUpdate(EInk::UpdateTypes::FAST, false); + // Force an update to bypass lockRequests + inkhud->forceUpdate(EInk::UpdateTypes::FAST); +} + +void InkHUD::KeyboardApplet::onNavRight() +{ + if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap + selectedKey -= KBD_COLS - 1; + else // move 1 column forward + selectedKey++; + + // Request rendering over the previously drawn render + requestUpdate(EInk::UpdateTypes::FAST, false); + // Force an update to bypass lockRequests + inkhud->forceUpdate(EInk::UpdateTypes::FAST); +} + +uint16_t InkHUD::KeyboardApplet::getKeyboardHeight() +{ + const uint16_t keyH = fontSmall.lineHeight() * 1.2; + return keyH * KBD_ROWS; +} +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h new file mode 100644 index 000000000..0ae181a2c --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h @@ -0,0 +1,66 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +System Applet to render an on-screen keyboard + +*/ + +#pragma once + +#include "configuration.h" +#include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/SystemApplet.h" +#include +namespace NicheGraphics::InkHUD +{ + +class KeyboardApplet : public SystemApplet +{ + public: + KeyboardApplet(); + + void onRender(bool full) override; + void onForeground() override; + void onBackground() override; + void onButtonShortPress() override; + void onButtonLongPress() override; + void onExitShort() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; + + static uint16_t getKeyboardHeight(); // used to set the keyboard tile height + + private: + void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color); + + static const uint8_t KBD_COLS = 11; + static const uint8_t KBD_ROWS = 4; + + const char keys[KBD_COLS * KBD_ROWS] = { + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0 + 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1 + 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2 + 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3 + }; + + // This array represents the widths of each key in points + // 16 pt = line height of the text + const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = { + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0 + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1 + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2 + 16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3 + }; + + uint16_t rowWidths[KBD_ROWS]; + uint8_t selectedKey = 0; // selected key index + uint8_t prevSelectedKey = 0; +}; + +} // namespace NicheGraphics::InkHUD + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index 4b55529bb..1f3109413 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -30,7 +30,7 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") // This is then drawn with a FULL refresh by Renderer::begin } -void InkHUD::LogoApplet::onRender() +void InkHUD::LogoApplet::onRender(bool full) { // Size of the region which the logo should "scale to fit" uint16_t logoWLimit = X(0.8); @@ -45,7 +45,7 @@ void InkHUD::LogoApplet::onRender() int16_t logoCY = Y(0.5 - 0.05); // Invert colors if black-on-white - // Used during shutdown, to resport display health + // Used during shutdown, to report display health // Todo: handle this in InkHUD::Renderer instead if (inverted) { fillScreen(BLACK); @@ -120,7 +120,7 @@ void InkHUD::LogoApplet::onBackground() // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } // Begin displaying the screen which is shown at shutdown @@ -138,10 +138,10 @@ void InkHUD::LogoApplet::onShutdown() // Intention is to restore display health. inverted = true; - inkhud->forceUpdate(Drivers::EInk::FULL, false); + inkhud->forceUpdate(Drivers::EInk::FULL, true, false); delay(1000); // Cooldown. Back to back updates aren't great for health. inverted = false; - inkhud->forceUpdate(Drivers::EInk::FULL, false); + inkhud->forceUpdate(Drivers::EInk::FULL, true, false); delay(1000); // Cooldown // Prepare for the powered-off screen now @@ -176,7 +176,7 @@ void InkHUD::LogoApplet::onReboot() textTitle = "Rebooting..."; fontTitle = fontSmall; - inkhud->forceUpdate(Drivers::EInk::FULL, false); + inkhud->forceUpdate(Drivers::EInk::FULL, true, false); // Perform the update right now, waiting here until complete } @@ -186,4 +186,4 @@ int32_t InkHUD::LogoApplet::runOnce() return OSThread::disable(); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h index 37f940453..d70dcc7b2 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.h @@ -21,7 +21,7 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread { public: LogoApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; void onShutdown() override; diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index 74ad5c85f..7ec76292b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -19,10 +19,10 @@ namespace NicheGraphics::InkHUD enum MenuAction { NO_ACTION, SEND_PING, + FREE_TEXT, STORE_CANNEDMESSAGE_SELECTION, SEND_CANNEDMESSAGE, SHUTDOWN, - BACK, NEXT_TILE, TOGGLE_BACKLIGHT, TOGGLE_GPS, diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 93d2c6b83..d489d21ee 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -90,6 +90,8 @@ void InkHUD::MenuApplet::onForeground() OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); OSThread::enabled = true; + freeTextMode = false; + // Upgrade the refresh to FAST, for guaranteed responsiveness inkhud->forceUpdate(EInk::UpdateTypes::FAST); } @@ -116,6 +118,8 @@ void InkHUD::MenuApplet::onBackground() SystemApplet::lockRequests = false; SystemApplet::handleInput = false; + handleFreeText = false; + // Restore the user applet whose tile we borrowed if (borrowedTileOwner) borrowedTileOwner->bringToForeground(); @@ -173,24 +177,8 @@ static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region) auto changes = SEGMENT_CONFIG; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - - if (config.security.private_key.size == 32) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } + if (crypto) { + crypto->ensurePkiKeys(config.security, owner); } #endif @@ -325,10 +313,6 @@ void InkHUD::MenuApplet::execute(MenuItem item) } break; - case BACK: - showPage(item.nextPage); - return; - case NEXT_TILE: inkhud->nextTile(); // Unselect menu item after tile change @@ -344,12 +328,26 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); break; + case FREE_TEXT: + OSThread::enabled = false; + handleFreeText = true; + cm.freeTextItem.rawText.erase(); // clear the previous freetext message + freeTextMode = true; // render input field instead of normal menu + // Open the on-screen keyboard only for full joystick devices + if (settings->joystick.enabled && !inkhud->twoWayRocker) + inkhud->openKeyboard(); + break; + case STORE_CANNEDMESSAGE_SELECTION: - cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + if (!settings->joystick.enabled || inkhud->twoWayRocker) + cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + else + cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry break; case SEND_CANNEDMESSAGE: cm.selectedRecipientItem = &cm.recipientItems.at(cursor); + // send selected message sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here break; @@ -868,6 +866,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) switch (page) { case ROOT: + previousPage = MenuPage::EXIT; // Optional: next applet if (settings->optionalMenuItems.nextTile && settings->userTiles.count > 1) items.push_back(MenuItem("Next Tile", MenuAction::NEXT_TILE, MenuPage::ROOT)); // Only if multiple applets shown @@ -878,7 +877,6 @@ void InkHUD::MenuApplet::showPage(MenuPage page) items.push_back(MenuItem("Node Config", MenuPage::NODE_CONFIG)); items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::EXIT; break; case SEND: @@ -888,11 +886,12 @@ void InkHUD::MenuApplet::showPage(MenuPage page) case CANNEDMESSAGE_RECIPIENT: populateRecipientPage(); - previousPage = MenuPage::OPTIONS; + previousPage = MenuPage::SEND; break; case OPTIONS: - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT)); + previousPage = MenuPage::ROOT; + items.push_back(MenuItem("Back", previousPage)); // Optional: backlight if (settings->optionalMenuItems.backlight) items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label @@ -907,7 +906,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) if (settings->userTiles.maxCount > 1) items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); - if (settings->joystick.enabled) + if (settings->joystick.enabled && !inkhud->twoWayRocker) items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, &settings->optionalFeatures.notifications)); @@ -916,31 +915,32 @@ void InkHUD::MenuApplet::showPage(MenuPage page) invertedColors = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); items.push_back(MenuItem("Invert Color", MenuAction::TOGGLE_INVERT_COLOR, MenuPage::OPTIONS, &invertedColors)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::ROOT; break; case APPLETS: - populateAppletPage(); // must be first - items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS)); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); previousPage = MenuPage::OPTIONS; + populateAppletPage(); // must be first + items.insert(items.begin(), MenuItem("Back", previousPage)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case AUTOSHOW: - populateAutoshowPage(); // must be first - items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS)); - items.push_back(MenuItem("Exit", MenuPage::EXIT)); previousPage = MenuPage::OPTIONS; + populateAutoshowPage(); // must be first + items.insert(items.begin(), MenuItem("Back", previousPage)); + items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case RECENTS: + previousPage = MenuPage::OPTIONS; populateRecentsPage(); // builds only the options - items.insert(items.begin(), MenuItem("Back", MenuAction::BACK, MenuPage::OPTIONS)); + items.insert(items.begin(), MenuItem("Back", previousPage)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); break; case NODE_CONFIG: - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::ROOT)); + previousPage = MenuPage::ROOT; + items.push_back(MenuItem("Back", previousPage)); // Radio Config Section items.push_back(MenuItem::Header("Radio Config")); items.push_back(MenuItem("LoRa", MenuPage::NODE_CONFIG_LORA)); @@ -965,8 +965,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; case NODE_CONFIG_DEVICE: { - - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); const char *role = DisplayFormatters::getDeviceRole(config.device.role); nodeConfigLabels.emplace_back("Role: " + std::string(role)); @@ -981,7 +981,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_POSITION: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); #if !MESHTASTIC_EXCLUDE_GPS && HAS_GPS const auto mode = config.position.gps_mode; if (mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { @@ -996,7 +997,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_POWER: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); #if defined(ARCH_ESP32) items.push_back(MenuItem("Powersave", MenuAction::TOGGLE_POWER_SAVE, MenuPage::EXIT, &config.power.is_power_saving)); #endif @@ -1029,7 +1031,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_POWER_ADC_CAL: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_POWER)); + previousPage = MenuPage::NODE_CONFIG_POWER; + items.push_back(MenuItem("Back", previousPage)); // Instruction text (header-style, non-selectable) items.push_back(MenuItem::Header("Run on full charge Only")); @@ -1042,7 +1045,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_NETWORK: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); const char *wifiLabel = config.network.wifi_enabled ? "WiFi: On" : "WiFi: Off"; @@ -1099,7 +1103,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_DISPLAY: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY, &config.display.use_12h_clock)); @@ -1114,7 +1119,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_BLUETOOTH: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); const char *btLabel = config.bluetooth.enabled ? "Bluetooth: On" : "Bluetooth: Off"; items.push_back(MenuItem(btLabel, MenuAction::TOGGLE_BLUETOOTH, MenuPage::EXIT)); @@ -1127,8 +1133,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_LORA: { - - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); const char *region = myRegion ? myRegion->name : "Unset"; nodeConfigLabels.emplace_back("Region: " + std::string(region)); @@ -1150,10 +1156,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_CHANNELS: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { - meshtastic_Channel &ch = channels.getByIndex(i); + const meshtastic_Channel &ch = channels.getByIndex(i); if (!ch.has_settings) continue; @@ -1181,7 +1188,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_CHANNEL_DETAIL: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNELS)); + previousPage = MenuPage::NODE_CONFIG_CHANNELS; + items.push_back(MenuItem("Back", previousPage)); meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); @@ -1226,8 +1234,9 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_CHANNEL_PRECISION: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); - meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); + previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL; + items.push_back(MenuItem("Back", previousPage)); + const meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) { items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); break; @@ -1247,7 +1256,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case NODE_CONFIG_DEVICE_ROLE: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE)); + previousPage = MenuPage::NODE_CONFIG_DEVICE; + items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("Client", MenuAction::SET_ROLE_CLIENT, MenuPage::EXIT)); items.push_back(MenuItem("Client Mute", MenuAction::SET_ROLE_CLIENT_MUTE, MenuPage::EXIT)); items.push_back(MenuItem("Router", MenuAction::SET_ROLE_ROUTER, MenuPage::EXIT)); @@ -1257,7 +1267,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } case TIMEZONE: - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_DEVICE)); + previousPage = MenuPage::NODE_CONFIG_DEVICE; + items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("US/Hawaii", SET_TZ_US_HAWAII, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("US/Alaska", SET_TZ_US_ALASKA, MenuPage::NODE_CONFIG_DEVICE)); items.push_back(MenuItem("US/Pacific", SET_TZ_US_PACIFIC, MenuPage::NODE_CONFIG_DEVICE)); @@ -1279,7 +1290,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; case REGION: - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA)); + previousPage = MenuPage::NODE_CONFIG_LORA; + items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT)); items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT)); items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT)); @@ -1310,7 +1322,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; case NODE_CONFIG_PRESET: { - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG_LORA)); + previousPage = MenuPage::NODE_CONFIG_LORA; + items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT)); items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT)); items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT)); @@ -1323,7 +1336,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) } // Administration Section case NODE_CONFIG_ADMIN_RESET: - items.push_back(MenuItem("Back", MenuAction::BACK, MenuPage::NODE_CONFIG)); + previousPage = MenuPage::NODE_CONFIG; + items.push_back(MenuItem("Back", previousPage)); items.push_back(MenuItem("Reset All", MenuAction::RESET_NODEDB_ALL, MenuPage::EXIT)); items.push_back(MenuItem("Keep Favorites Only", MenuAction::RESET_NODEDB_KEEP_FAVORITES, MenuPage::EXIT)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); @@ -1361,8 +1375,14 @@ void InkHUD::MenuApplet::showPage(MenuPage page) currentPage = page; } -void InkHUD::MenuApplet::onRender() +void InkHUD::MenuApplet::onRender(bool full) { + // Free text mode draws a text input field and skips the normal rendering + if (freeTextMode) { + drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText); + return; + } + if (items.size() == 0) LOG_ERROR("Empty Menu"); @@ -1481,44 +1501,56 @@ void InkHUD::MenuApplet::onRender() void InkHUD::MenuApplet::onButtonShortPress() { - // Push the auto-close timer back - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!freeTextMode) { + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (!settings->joystick.enabled) { - if (!cursorShown) { - cursorShown = true; - cursor = 0; - } else { - do { - cursor = (cursor + 1) % items.size(); - } while (items.at(cursor).isHeader); - } - requestUpdate(Drivers::EInk::UpdateTypes::FAST); - } else { - if (cursorShown) - execute(items.at(cursor)); - else - showPage(MenuPage::EXIT); - if (!wantsToRender()) + if (!settings->joystick.enabled) { + if (!cursorShown) { + cursorShown = true; + // Select the first item that isn't a header + cursor = 0; + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) { + cursorShown = false; + cursor = 0; + } + } else { + do { + cursor = (cursor + 1) % items.size(); + } while (items.at(cursor).isHeader); + } requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } else { + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } } } void InkHUD::MenuApplet::onButtonLongPress() { - // Push the auto-close timer back - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!freeTextMode) { + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (cursorShown) - execute(items.at(cursor)); - else - showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close - // If we didn't already request a specialized update, when handling a menu action, - // then perform the usual fast update. - // FAST keeps things responsive: important because we're dealing with user input - if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + // If we didn't already request a specialized update, when handling a menu action, + // then perform the usual fast update. + // FAST keeps things responsive: important because we're dealing with user input + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } } void InkHUD::MenuApplet::onExitShort() @@ -1531,56 +1563,123 @@ void InkHUD::MenuApplet::onExitShort() void InkHUD::MenuApplet::onNavUp() { - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!freeTextMode) { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (!cursorShown) { - cursorShown = true; - cursor = 0; - } else { - do { - if (cursor == 0) - cursor = items.size() - 1; - else + if (!cursorShown) { + cursorShown = true; + // Select the last item that isn't a header + cursor = items.size() - 1; + while (items.at(cursor).isHeader) { + if (cursor == 0) { + cursorShown = false; + break; + } cursor--; - } while (items.at(cursor).isHeader); - } + } + } else { + do { + if (cursor == 0) + cursor = items.size() - 1; + else + cursor--; + } while (items.at(cursor).isHeader); + } - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } } void InkHUD::MenuApplet::onNavDown() { - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!freeTextMode) { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (!cursorShown) { - cursorShown = true; - cursor = 0; - } else { - do { - cursor = (cursor + 1) % items.size(); - } while (items.at(cursor).isHeader); + if (!cursorShown) { + cursorShown = true; + // Select the first item that isn't a header + cursor = 0; + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) { + cursorShown = false; + cursor = 0; + } + } else { + do { + cursor = (cursor + 1) % items.size(); + } while (items.at(cursor).isHeader); + } + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); } - - requestUpdate(Drivers::EInk::UpdateTypes::FAST); } void InkHUD::MenuApplet::onNavLeft() { - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!freeTextMode) { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - // Go to the previous menu page - showPage(previousPage); - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + // Go to the previous menu page + showPage(previousPage); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } } void InkHUD::MenuApplet::onNavRight() { - OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (!freeTextMode) { + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + if (cursorShown) + execute(items.at(cursor)); + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} - if (cursorShown) - execute(items.at(cursor)); - if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); +void InkHUD::MenuApplet::onFreeText(char c) +{ + if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b') + return; + if (c == '\b') { + if (!cm.freeTextItem.rawText.empty()) + cm.freeTextItem.rawText.pop_back(); + } else { + cm.freeTextItem.rawText += c; + } + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onFreeTextDone() +{ + // Restart the auto-close timeout + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + OSThread::enabled = true; + + handleFreeText = false; + freeTextMode = false; + + if (!cm.freeTextItem.rawText.empty()) { + cm.selectedMessageItem = &cm.freeTextItem; + showPage(MenuPage::CANNEDMESSAGE_RECIPIENT); + } + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onFreeTextCancel() +{ + // Restart the auto-close timeout + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + OSThread::enabled = true; + + handleFreeText = false; + freeTextMode = false; + + // Clear the free text message + cm.freeTextItem.rawText.erase(); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); } // Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu @@ -1635,6 +1734,10 @@ void InkHUD::MenuApplet::populateSendPage() // Position / NodeInfo packet items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); + // If joystick is available, include the Free Text option + if (settings->joystick.enabled && !inkhud->twoWayRocker) + items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND)); + // One menu item for each canned message uint8_t count = cm.store->size(); for (uint8_t i = 0; i < count; i++) { @@ -1664,7 +1767,7 @@ void InkHUD::MenuApplet::populateRecipientPage() for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { // Get the channel, and check if it's enabled - meshtastic_Channel &channel = channels.getByIndex(i); + const meshtastic_Channel &channel = channels.getByIndex(i); if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) continue; @@ -1734,6 +1837,48 @@ void InkHUD::MenuApplet::populateRecipientPage() items.push_back(MenuItem("Exit", MenuPage::EXIT)); } +void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text) +{ + setFont(fontSmall); + uint16_t wrapMaxH = 0; + + // Draw the text, input box, and cursor + // Adjusting the box for screen height + while (wrapMaxH < height - fontSmall.lineHeight()) { + wrapMaxH += fontSmall.lineHeight(); + } + + // If the text is so long that it goes outside of the input box, the text is actually rendered off screen. + uint32_t textHeight = getWrappedTextHeight(0, width - 5, text); + if (!text.empty()) { + uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1; + if (textHeight > wrapMaxH) + printWrapped(2, textPadding, width - 5, text); + else + printWrapped(2, top + 2, width - 5, text); + } + + uint16_t textCursorX = text.empty() ? 1 : getCursorX(); + uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3; + + if (textCursorX + 1 > width - 5) { + textCursorX = getCursorX() - width + 5; + textCursorY += fontSmall.lineHeight(); + } + + fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK); + + // A white rectangle clears the top part of the screen for any text that's printed beyond the input box + fillRect(0, 0, X(1.0), top, WHITE); + + // Draw character limit + std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit); + uint16_t textLen = getTextWidth(ftlen); + printAt(X(1.0) - textLen - 2, 0, ftlen); + + // Draw the border + drawRect(0, top, width, wrapMaxH + 5, BLACK); +} // Renders the panel shown at the top of the root menu. // Displays the clock, and several other pieces of instantaneous system info, // which we'd prefer not to have displayed in a normal applet, as they update too frequently. @@ -1867,7 +2012,7 @@ void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone } -// Free up any heap mmemory we'd used while selecting / sending canned messages +// Free up any heap memory we'd used while selecting / sending canned messages void InkHUD::MenuApplet::freeCannedMessageResources() { cm.selectedMessageItem = nullptr; @@ -1875,4 +2020,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources() cm.messageItems.clear(); cm.recipientItems.clear(); } -#endif // MESHTASTIC_INCLUDE_INKHUD \ No newline at end of file +#endif // MESHTASTIC_INCLUDE_INKHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index 82ccc8f45..b5c1c86e4 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -32,7 +32,10 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void onNavDown() override; void onNavLeft() override; void onNavRight() override; - void onRender() override; + void onFreeText(char c) override; + void onFreeTextDone() override; + void onFreeTextCancel() override; + void onRender(bool full) override; void show(Tile *t); // Open the menu, onto a user tile void setStartPage(MenuPage page); @@ -51,6 +54,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, + const std::string &text); // Draw input field for free text uint16_t getSystemInfoPanelHeight(); void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *height = nullptr); // Info panel at top of root menu @@ -62,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread MenuPage previousPage = MenuPage::EXIT; uint8_t cursor = 0; // Which menu item is currently highlighted bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) - + bool freeTextMode = false; uint16_t systemInfoPanelHeight = 0; // Need to know before we render + uint16_t menuTextLimit = 200; std::vector items; // MenuItems for the current page. Filled by ShowPage std::vector nodeConfigLabels; // Persistent labels for Node Config pages @@ -104,6 +110,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread // Cleared onBackground (when MenuApplet closes) std::vector messageItems; std::vector recipientItems; + + MessageItem freeTextItem; } cm; Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index 2ea9c7fe0..6c8069c8b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -65,7 +65,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket return 0; } -void InkHUD::NotificationApplet::onRender() +void InkHUD::NotificationApplet::onRender(bool full) { // Clear the region beneath the tile // Most applets are drawing onto an empty frame buffer and don't need to do this @@ -139,54 +139,47 @@ void InkHUD::NotificationApplet::onForeground() void InkHUD::NotificationApplet::onBackground() { handleInput = false; + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } void InkHUD::NotificationApplet::onButtonShortPress() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::NotificationApplet::onButtonLongPress() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::NotificationApplet::onExitShort() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::NotificationApplet::onExitLong() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::NotificationApplet::onNavUp() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::NotificationApplet::onNavDown() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::NotificationApplet::onNavLeft() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } void InkHUD::NotificationApplet::onNavRight() { dismiss(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } // Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification @@ -235,17 +228,17 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) { // Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently - bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; + bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; // Pick source of message - MessageStore::Message *message = - isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; + const MessageStore::Message *message = + msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; // Find info about the sender meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender); // Leading tag (channel vs. DM) - text += isBroadcast ? "From:" : "DM: "; + text += msgIsBroadcast ? "From:" : "DM: "; // Sender id if (node && node->has_user) @@ -259,7 +252,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila text.clear(); // Leading tag (channel vs. DM) - text += isBroadcast ? "Msg from " : "DM from "; + text += msgIsBroadcast ? "Msg from " : "DM from "; // Sender id if (node && node->has_user) diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h index 16ea13407..d398a36f3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h @@ -26,7 +26,7 @@ class NotificationApplet : public SystemApplet public: NotificationApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonShortPress() override; diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp index 09931f109..54515b296 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -9,7 +9,7 @@ InkHUD::PairingApplet::PairingApplet() bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); } -void InkHUD::PairingApplet::onRender() +void InkHUD::PairingApplet::onRender(bool full) { // Header setFont(fontMedium); @@ -45,7 +45,7 @@ void InkHUD::PairingApplet::onBackground() // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) @@ -55,12 +55,12 @@ int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *sta // We'll mimic that behavior, just to keep in line with the other Statuses, // even though I'm not sure what the original reason for jumping through these extra hoops was. assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH); - meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status; + const auto *btStatus = static_cast(status); // When pairing begins - if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { + if (btStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { // Store the passkey for rendering - passkey = bluetoothStatus->getPasskey(); + passkey = btStatus->getPasskey(); // Show pairing screen bringToForeground(); diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h index b89783a25..4c2e95321 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.h @@ -22,7 +22,7 @@ class PairingApplet : public SystemApplet public: PairingApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp index 99cdeb0ac..228c8b2ca 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.cpp @@ -4,7 +4,7 @@ using namespace NicheGraphics; -void InkHUD::PlaceholderApplet::onRender() +void InkHUD::PlaceholderApplet::onRender(bool full) { // This placeholder applet fills its area with sparse diagonal lines hatchRegion(0, 0, width(), height(), 8, BLACK); diff --git a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h index 78ba5cd89..fa40913e0 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Placeholder/PlaceholderApplet.h @@ -17,7 +17,7 @@ namespace NicheGraphics::InkHUD class PlaceholderApplet : public SystemApplet { public: - void onRender() override; + void onRender(bool full) override; // Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet. // The window manager decides when and where it should be rendered diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index 7869319fe..a45e8d9b5 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -45,7 +45,7 @@ InkHUD::TipsApplet::TipsApplet() bringToForeground(); } -void InkHUD::TipsApplet::onRender() +void InkHUD::TipsApplet::onRender(bool full) { switch (tipQueue.front()) { case Tip::WELCOME: @@ -152,6 +152,11 @@ void InkHUD::TipsApplet::onRender() drawBullet("User Button"); drawBullet("- short press: next"); drawBullet("- long press: select or open menu"); + } else if (inkhud->twoWayRocker) { + drawBullet("Rocker + Button"); + drawBullet("- center press: open menu or select"); + drawBullet("- left/right: applet nav"); + drawBullet("- in menu: up/down"); } else { drawBullet("Joystick"); drawBullet("- press: open menu or select"); @@ -261,7 +266,7 @@ void InkHUD::TipsApplet::onBackground() // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case - inkhud->forceUpdate(EInk::UpdateTypes::FULL); + inkhud->forceUpdate(EInk::UpdateTypes::FULL, true); } // While our SystemApplet::handleInput flag is true @@ -292,9 +297,8 @@ void InkHUD::TipsApplet::onButtonShortPress() inkhud->persistence->saveSettings(); } - // Close applet and clean the screen + // Close applet sendToBackground(); - inkhud->forceUpdate(EInk::UpdateTypes::FULL); } else { requestUpdate(); } @@ -306,4 +310,4 @@ void InkHUD::TipsApplet::onExitShort() onButtonShortPress(); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index ff7eea046..2e81d678b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -33,7 +33,7 @@ class TipsApplet : public SystemApplet public: TipsApplet(); - void onRender() override; + void onRender(bool full) override; void onForeground() override; void onBackground() override; void onButtonShortPress() override; diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index 7c6232f3b..96c519599 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -34,7 +34,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket * return 0; } -void InkHUD::AllMessageApplet::onRender() +void InkHUD::AllMessageApplet::onRender(bool full) { // Find newest message, regardless of whether DM or broadcast MessageStore::Message *message; diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h index c74e16196..4aa97e4f1 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h @@ -30,7 +30,7 @@ class Applet; class AllMessageApplet : public Applet { public: - void onRender() override; + void onRender(bool full) override; void onActivate() override; void onDeactivate() override; diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index a3b9615a5..189a56cab 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -37,7 +37,7 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) return 0; } -void InkHUD::DMApplet::onRender() +void InkHUD::DMApplet::onRender(bool full) { // Abort if no text message if (!latestMessage->dm.sender) { diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h index b3dc36e66..4eb0ec704 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.h @@ -30,7 +30,7 @@ class Applet; class DMApplet : public Applet { public: - void onRender() override; + void onRender(bool full) override; void onActivate() override; void onDeactivate() override; diff --git a/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp new file mode 100644 index 000000000..520070d72 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp @@ -0,0 +1,111 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./FavoritesMapApplet.h" +#include "NodeDB.h" + +using namespace NicheGraphics; + +bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node) +{ + // Keep our own node available as map anchor/center; all others must be favorited. + return node && (node->num == nodeDB->getNodeNum() || node->is_favorite); +} + +void InkHUD::FavoritesMapApplet::onRender(bool full) +{ + // Custom empty state text for favorites-only map. + if (!enoughMarkers()) { + printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Favorite node position", CENTER, MIDDLE); + printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); + return; + } + + // Draw the usual map applet first. + MapApplet::onRender(full); + + // Draw our latest "node of interest" as a special marker. + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); + if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers()) + drawLabeledMarker(node); +} + +// Determine if we need to redraw the map, when we receive a new position packet. +ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ + // If applet is not active, we shouldn't be handling any data. + if (!isActive()) + return ProcessMessage::CONTINUE; + + // Try decode a position from the packet. + bool hasPosition = false; + float lat; + float lng; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { + meshtastic_Position position = meshtastic_Position_init_default; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { + if (position.has_latitude_i && position.has_longitude_i // Actually has position + && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" + { + hasPosition = true; + lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format + lng = position.longitude_i * 1e-7; + } + } + } + + // Skip if we didn't get a valid position. + if (!hasPosition) + return ProcessMessage::CONTINUE; + + const int8_t hopsAway = getHopsAway(mp); + const bool hasHopsAway = hopsAway >= 0; + + // Determine if the position packet would change anything on-screen. + bool somethingChanged = false; + + // If our own position. + if (isFromUs(&mp)) { + // Ignore tiny local movement to reduce update spam. + if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { + somethingChanged = true; + ourLastLat = lat; + ourLastLng = lng; + } + } else { + // For non-local packets, this applet only reacts to favorited nodes. + const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); + if (!sender || !sender->is_favorite) + return ProcessMessage::CONTINUE; + + // Check if this position is from someone different than our previous position packet. + if (mp.from != lastFrom) { + somethingChanged = true; + lastFrom = mp.from; + lastLat = lat; + lastLng = lng; + lastHopsAway = hopsAway; + } + + // Same sender: check if position changed. + else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { + somethingChanged = true; + lastLat = lat; + lastLng = lng; + } + + // Same sender, same position: check if hops changed. + else if (hasHopsAway && (hopsAway != lastHopsAway)) { + somethingChanged = true; + lastHopsAway = hopsAway; + } + } + + if (somethingChanged) { + requestAutoshow(); + requestUpdate(); + } + + return ProcessMessage::CONTINUE; +} + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h new file mode 100644 index 000000000..da5fb0dc3 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h @@ -0,0 +1,44 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Plots position of favorited nodes from DB, with North facing up. +Scaled to fit the most distant node. +Size of marker represents hops away. +The favorite node which most recently sent a position will be labeled. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h" + +#include "SinglePortModule.h" + +namespace NicheGraphics::InkHUD +{ + +class FavoritesMapApplet : public MapApplet, public SinglePortModule +{ + public: + FavoritesMapApplet() : SinglePortModule("FavoritesMapApplet", meshtastic_PortNum_POSITION_APP) {} + void onRender(bool full) override; + + protected: + bool shouldDrawNode(meshtastic_NodeInfoLite *node) override; + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + NodeNum lastFrom = 0; // Sender of most recent favorited (non-local) position packet + float lastLat = 0.0; + float lastLng = 0.0; + float lastHopsAway = 0; + + float ourLastLat = 0.0; // Info about most recent local position + float ourLastLng = 0.0; +}; + +} // namespace NicheGraphics::InkHUD + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp index 5a659c606..a7fd094e6 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp @@ -69,9 +69,10 @@ void InkHUD::HeardApplet::populateFromNodeDB() } // Sort the collection by age - std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool { - return (top->last_heard > bottom->last_heard); - }); + std::sort(ordered.begin(), ordered.end(), + [](const meshtastic_NodeInfoLite *top, const meshtastic_NodeInfoLite *bottom) -> bool { + return (top->last_heard > bottom->last_heard); + }); // Keep the most recent entries only // Just enough to fill the screen diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp index ad0f9fc47..ae7679962 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp @@ -5,10 +5,10 @@ using namespace NicheGraphics; -void InkHUD::PositionsApplet::onRender() +void InkHUD::PositionsApplet::onRender(bool full) { // Draw the usual map applet first - MapApplet::onRender(); + MapApplet::onRender(full); // Draw our latest "node of interest" as a special marker // ------------------------------------------------------- diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h index 28a53cb0f..d0d3e5f07 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h @@ -24,7 +24,7 @@ class PositionsApplet : public MapApplet, public SinglePortModule { public: PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {} - void onRender() override; + void onRender(bool full) override; protected: ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index fdb5a168d..01bdc2224 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -22,7 +22,7 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) store = new MessageStore("ch" + to_string(channelIndex)); } -void InkHUD::ThreadedMessageApplet::onRender() +void InkHUD::ThreadedMessageApplet::onRender(bool full) { // ============= // Draw a header @@ -69,7 +69,7 @@ void InkHUD::ThreadedMessageApplet::onRender() while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { // Grab data for message - MessageStore::Message &m = store->messages.at(i); + const MessageStore::Message &m = store->messages.at(i); bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index c986539b3..2cd2c4163 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -7,7 +7,7 @@ Displays a thread-view of incoming and outgoing message for a specific channel The channel for this applet is set in the constructor, when the applet is added to WindowManager in the setupNicheGraphics method. -Several messages are saved to flash at shutdown, to preseve applet between reboots. +Several messages are saved to flash at shutdown, to preserve applet between reboots. This class has its own internal method for saving and loading to fs, which interacts directly with the FSCommon layer. If the amount of flash usage is unacceptable, we could keep these in RAM only. @@ -36,7 +36,7 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule explicit ThreadedMessageApplet(uint8_t channelIndex); ThreadedMessageApplet() = delete; - void onRender() override; + void onRender(bool full) override; void onActivate() override; void onDeactivate() override; @@ -55,4 +55,4 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index fa45a49ed..577a773bb 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -59,10 +59,16 @@ void InkHUD::Events::onButtonShort() if (consumer) { consumer->onButtonShortPress(); } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module - if (!settings->joystick.enabled) - inkhud->nextApplet(); - else - inkhud->openMenu(); + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_SHORT)) + userConsumer->onButtonShortPress(); + else { + if (!settings->joystick.enabled) + inkhud->nextApplet(); + else + inkhud->openMenu(); + } } } @@ -84,8 +90,14 @@ void InkHUD::Events::onButtonLong() // If no system applet is handling input, default behavior instead is to open the menu if (consumer) consumer->onButtonLongPress(); - else - inkhud->openMenu(); + else { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_LONG)) + userConsumer->onButtonLongPress(); + else + inkhud->openMenu(); + } } void InkHUD::Events::onExitShort() @@ -110,8 +122,14 @@ void InkHUD::Events::onExitShort() // If no system applet is handling input, default behavior instead is change tiles if (consumer) consumer->onExitShort(); - else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module - inkhud->nextTile(); + else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT)) + userConsumer->onExitShort(); + else + inkhud->nextTile(); + } } } @@ -133,6 +151,13 @@ void InkHUD::Events::onExitLong() if (consumer) consumer->onExitLong(); + else { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG)) + userConsumer->onExitLong(); + // Nothing uses exit long yet + } } } @@ -157,6 +182,12 @@ void InkHUD::Events::onNavUp() if (consumer) consumer->onNavUp(); + else if (!dismissedExt) { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP)) + userConsumer->onNavUp(); + } } } @@ -181,6 +212,12 @@ void InkHUD::Events::onNavDown() if (consumer) consumer->onNavDown(); + else if (!dismissedExt) { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN)) + userConsumer->onNavDown(); + } } } @@ -206,8 +243,14 @@ void InkHUD::Events::onNavLeft() // If no system applet is handling input, default behavior instead is to cycle applets if (consumer) consumer->onNavLeft(); - else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module - inkhud->prevApplet(); + else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT)) + userConsumer->onNavLeft(); + else + inkhud->prevApplet(); + } } } @@ -233,8 +276,47 @@ void InkHUD::Events::onNavRight() // If no system applet is handling input, default behavior instead is to cycle applets if (consumer) consumer->onNavRight(); - else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module - inkhud->nextApplet(); + else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT)) + userConsumer->onNavRight(); + else + inkhud->nextApplet(); + } + } +} + +void InkHUD::Events::onFreeText(char c) +{ + // Trigger the first system applet that wants to handle the new character + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleFreeText) { + sa->onFreeText(c); + break; + } + } +} + +void InkHUD::Events::onFreeTextDone() +{ + // Trigger the first system applet that wants to handle it + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleFreeText) { + sa->onFreeTextDone(); + break; + } + } +} + +void InkHUD::Events::onFreeTextCancel() +{ + // Trigger the first system applet that wants to handle it + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleFreeText) { + sa->onFreeTextCancel(); + break; + } } } @@ -266,7 +348,7 @@ int InkHUD::Events::beforeDeepSleep(void *unused) // then prepared a final powered-off screen for us, which shows device shortname. // We're updating to show that one now. - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); delay(1000); // Cooldown, before potentially yanking display power // InkHUD shutdown complete diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index 1916cf78e..873f53fd5 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -37,6 +37,11 @@ class Events void onNavLeft(); // Navigate left void onNavRight(); // Navigate right + // Free text typing events + void onFreeText(char c); // New freetext character input + void onFreeTextDone(); + void onFreeTextCancel(); + int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 13b15b7e8..edffda6b7 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -175,6 +175,25 @@ void InkHUD::InkHUD::navRight() } } +// Call this for keyboard input +// The Keyboard Applet also calls this +void InkHUD::InkHUD::freeText(char c) +{ + events->onFreeText(c); +} + +// Call this to complete a freetext input +void InkHUD::InkHUD::freeTextDone() +{ + events->onFreeTextDone(); +} + +// Call this to cancel a freetext input +void InkHUD::InkHUD::freeTextCancel() +{ + events->onFreeTextCancel(); +} + // Cycle the next user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" @@ -191,6 +210,12 @@ void InkHUD::InkHUD::prevApplet() windowManager->prevApplet(); } +// Returns the currently active applet +InkHUD::Applet *InkHUD::InkHUD::getActiveApplet() +{ + return windowManager->getActiveApplet(); +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::InkHUD::openMenu() @@ -204,6 +229,18 @@ void InkHUD::InkHUD::openAlignStick() windowManager->openAlignStick(); } +// Open the on-screen keyboard +void InkHUD::InkHUD::openKeyboard() +{ + windowManager->openKeyboard(); +} + +// Close the on-screen keyboard +void InkHUD::InkHUD::closeKeyboard() +{ + windowManager->closeKeyboard(); +} + // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press void InkHUD::InkHUD::nextTile() @@ -252,10 +289,11 @@ void InkHUD::InkHUD::requestUpdate() // Ignores all diplomacy: // - the display *will* update // - the specified update type *will* be used +// If the all parameter is true, the whole screen buffer is cleared and re-rendered // If the async parameter is false, code flow is blocked while the update takes place -void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async) +void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async) { - renderer->forceUpdate(type, async); + renderer->forceUpdate(type, all, async); } // Wait for any in-progress display update to complete before continuing diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 5280d9ac7..abd53951a 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -63,14 +63,22 @@ class InkHUD void navLeft(); void navRight(); + // Freetext handlers + void freeText(char c); + void freeTextDone(); + void freeTextCancel(); + // Trigger UI changes // - called by various InkHUD components // - suitable(?) for use by aux button, connected in variant nicheGraphics.h void nextApplet(); void prevApplet(); + NicheGraphics::InkHUD::Applet *getActiveApplet(); void openMenu(); void openAlignStick(); + void openKeyboard(); + void closeKeyboard(); void nextTile(); void prevTile(); void rotate(); @@ -80,11 +88,15 @@ class InkHUD // Used by TipsApplet to force menu to start on Region selection bool forceRegionMenu = false; + // Input mode hint for devices that use a left/right rocker plus center button + bool twoWayRocker = false; + // Updating the display // - called by various InkHUD components void requestUpdate(); - void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true); + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false, + bool async = true); void awaitUpdate(); // (Re)configuring WindowManager @@ -121,4 +133,4 @@ class InkHUD } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/MessageStore.cpp b/src/graphics/niche/InkHUD/MessageStore.cpp index 94e0aa661..44a1ef633 100644 --- a/src/graphics/niche/InkHUD/MessageStore.cpp +++ b/src/graphics/niche/InkHUD/MessageStore.cpp @@ -12,7 +12,7 @@ using namespace NicheGraphics; constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; -InkHUD::MessageStore::MessageStore(std::string label) +InkHUD::MessageStore::MessageStore(const std::string &label) { filename = ""; filename += "/NicheGraphics"; @@ -50,12 +50,13 @@ void InkHUD::MessageStore::saveToFlash() // For each message for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { Message &m = messages.at(i); - f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes - f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes - f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte - f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length - f.write('\0'); // Append null term - LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); + f.write(reinterpret_cast(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes + f.write(reinterpret_cast(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes + f.write(reinterpret_cast(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte + f.write(reinterpret_cast(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text + f.write('\0'); // Append null term + LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast(i), min(MAX_MESSAGE_SIZE, m.text.size()), + m.text.c_str()); } // Release firmware's SPI lock, because SafeFile::close needs it @@ -111,17 +112,17 @@ void InkHUD::MessageStore::loadFromFlash() // First byte: how many messages are in the flash store uint8_t flashMessageCount = 0; - f.readBytes((char *)&flashMessageCount, 1); - LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount); + f.readBytes(reinterpret_cast(&flashMessageCount), 1); + LOG_DEBUG("Messages available: %u", static_cast(flashMessageCount)); // For each message for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { Message m; // Read meta data (fixed width) - f.readBytes((char *)&m.timestamp, sizeof(m.timestamp)); - f.readBytes((char *)&m.sender, sizeof(m.sender)); - f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); + f.readBytes(reinterpret_cast(&m.timestamp), sizeof(m.timestamp)); + f.readBytes(reinterpret_cast(&m.sender), sizeof(m.sender)); + f.readBytes(reinterpret_cast(&m.channelIndex), sizeof(m.channelIndex)); // Read characters until we find a null term char c; @@ -136,7 +137,8 @@ void InkHUD::MessageStore::loadFromFlash() // Store in RAM messages.push_back(m); - LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str()); + LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast(i), m.timestamp, m.sender, + m.text.c_str()); } f.close(); diff --git a/src/graphics/niche/InkHUD/MessageStore.h b/src/graphics/niche/InkHUD/MessageStore.h index 745c3b2eb..55fb9b8cc 100644 --- a/src/graphics/niche/InkHUD/MessageStore.h +++ b/src/graphics/niche/InkHUD/MessageStore.h @@ -31,7 +31,7 @@ class MessageStore }; MessageStore() = delete; - explicit MessageStore(std::string label); // Label determines filename in flash + explicit MessageStore(const std::string &label); // Label determines filename in flash void saveToFlash(); void loadFromFlash(); diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini index b985f9f77..67ad5098f 100644 --- a/src/graphics/niche/InkHUD/PlatformioConfig.ini +++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -8,5 +8,5 @@ build_flags = -D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling -D HAS_BUTTON=0 ; Suppress default ButtonThread lib_deps = - # TODO renovate - https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX + # renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root + https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX diff --git a/src/graphics/niche/InkHUD/Renderer.cpp b/src/graphics/niche/InkHUD/Renderer.cpp index 072e9dbd6..a73e209ff 100644 --- a/src/graphics/niche/InkHUD/Renderer.cpp +++ b/src/graphics/niche/InkHUD/Renderer.cpp @@ -56,15 +56,16 @@ void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMul void InkHUD::Renderer::begin() { - forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); + forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false); } // Set a flag, which will be picked up by runOnce, ASAP. // Quite likely, multiple applets will all want to respond to one event (Observable, etc) // Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce -void InkHUD::Renderer::requestUpdate() +void InkHUD::Renderer::requestUpdate(bool all) { requested = true; + renderAll |= all; // We will run the thread as soon as we loop(), // after all Applets have had a chance to observe whatever event set this off @@ -79,10 +80,11 @@ void InkHUD::Renderer::requestUpdate() // Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event // Display health, for example. // In these situations, we use forceUpdate -void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async) +void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async) { requested = true; forced = true; + renderAll |= all; displayHealth.forceUpdateType(type); // Normally, we need to start the timer, in case the display is busy and we briefly defer the update @@ -219,7 +221,8 @@ void InkHUD::Renderer::render(bool async) Drivers::EInk::UpdateTypes updateType = decideUpdateType(); // Render the new image - clearBuffer(); + if (renderAll) + clearBuffer(); renderUserApplets(); renderPlaceholders(); renderSystemApplets(); @@ -247,6 +250,7 @@ void InkHUD::Renderer::render(bool async) // Tidy up, ready for a new request requested = false; forced = false; + renderAll = false; } // Manually fill the image buffer with WHITE @@ -259,6 +263,76 @@ void InkHUD::Renderer::clearBuffer() memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); } +// Manually clear the pixels below a tile +void InkHUD::Renderer::clearTile(Tile *t) +{ + // Rotate the tile dimensions + int16_t left = 0; + int16_t top = 0; + uint16_t tileW = 0; + uint16_t tileH = 0; + switch (settings->rotation) { + case 0: + left = t->getLeft(); + top = t->getTop(); + tileW = t->getWidth(); + tileH = t->getHeight(); + break; + case 1: + left = driver->width - (t->getTop() + t->getHeight()); + top = t->getLeft(); + tileW = t->getHeight(); + tileH = t->getWidth(); + break; + case 2: + left = driver->width - (t->getLeft() + t->getWidth()); + top = driver->height - (t->getTop() + t->getHeight()); + tileW = t->getWidth(); + tileH = t->getHeight(); + break; + case 3: + left = t->getTop(); + top = driver->height - (t->getLeft() + t->getWidth()); + tileW = t->getHeight(); + tileH = t->getWidth(); + break; + } + + // Calculate the bounds to clear + uint16_t xStart = (left < 0) ? 0 : left; + uint16_t yStart = (top < 0) ? 0 : top; + if (xStart >= driver->width || yStart >= driver->height || left + tileW < 0 || top + tileH < 0) + return; // the box is completely off the screen + uint16_t xEnd = left + tileW; + uint16_t yEnd = top + tileH; + if (xEnd > driver->width) + xEnd = driver->width; + if (yEnd > driver->height) + yEnd = driver->height; + + // Clear the pixels + if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear + memset(imageBuffer + (yStart * imageBufferWidth), 0xFF, (yEnd - yStart) * imageBufferWidth); + } else { + const uint16_t byteStart = (xStart / 8) + 1; + const uint16_t byteEnd = xEnd / 8; + const uint8_t leadingByte = 0xFF >> (xStart - ((byteStart - 1) * 8)); + const uint8_t trailingByte = (0xFF00 >> (xEnd - (byteEnd * 8))) & 0xFF; + for (uint16_t i = yStart * imageBufferWidth; i < yEnd * imageBufferWidth; i += imageBufferWidth) { + // Set the leading byte + imageBuffer[i + byteStart - 1] |= leadingByte; + + // Set the continuous bytes + if (byteStart < byteEnd) + memset(imageBuffer + i + byteStart, 0xFF, byteEnd - byteStart); + + // Set the trailing byte + if (byteEnd != imageBufferWidth) + imageBuffer[i + byteEnd] |= trailingByte; + } + } +} + void InkHUD::Renderer::checkLocks() { lockRendering = nullptr; @@ -323,12 +397,12 @@ Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType() if (!forced) { // User applets for (Applet *ua : inkhud->userApplets) { - if (ua && ua->isForeground()) + if (ua && ua->isForeground() && (ua->wantsToRender() || renderAll)) displayHealth.requestUpdateType(ua->wantsUpdateType()); } // System Applets for (SystemApplet *sa : inkhud->systemApplets) { - if (sa && sa->isForeground()) + if (sa && sa->isForeground() && (sa->wantsToRender() || sa->alwaysRender || renderAll)) displayHealth.requestUpdateType(sa->wantsUpdateType()); } } @@ -346,9 +420,16 @@ void InkHUD::Renderer::renderUserApplets() // Render any user applets which are currently visible for (Applet *ua : inkhud->userApplets) { - if (ua && ua->isActive() && ua->isForeground()) { + if (ua && ua->isActive() && ua->isForeground() && (ua->wantsToRender() || renderAll)) { + + // Clear the tile unless the applet wants to draw over its previous render + // or everything is getting re-rendered anyways + if (ua->wantsFullRender() && !renderAll) + clearTile(ua->getTile()); + uint32_t start = millis(); - ua->render(); // Draw! + bool full = ua->wantsFullRender() || renderAll; + ua->render(full); // Draw! uint32_t stop = millis(); LOG_DEBUG("%s took %dms to render", ua->name, stop - start); } @@ -370,6 +451,9 @@ void InkHUD::Renderer::renderSystemApplets() if (!sa->isForeground()) continue; + if (!sa->wantsToRender() && !sa->alwaysRender && !renderAll) + continue; + // Skip if locked by another applet if (lockRendering && lockRendering != sa) continue; @@ -381,8 +465,14 @@ void InkHUD::Renderer::renderSystemApplets() assert(sa->getTile()); + // Clear the tile unless the applet wants to draw over its previous render + // or everything is getting re-rendered anyways + if (sa->wantsFullRender() && !renderAll) + clearTile(sa->getTile()); + // uint32_t start = millis(); - sa->render(); // Draw! + bool full = sa->wantsFullRender() || renderAll; + sa->render(full); // Draw! // uint32_t stop = millis(); // LOG_DEBUG("%s took %dms to render", sa->name, stop - start); } @@ -409,7 +499,10 @@ void InkHUD::Renderer::renderPlaceholders() // uint32_t start = millis(); for (Tile *t : emptyTiles) { t->assignApplet(placeholder); - placeholder->render(); + // Clear the tile unless everything is getting re-rendered + if (!renderAll) + clearTile(t); + placeholder->render(true); // full render t->assignApplet(nullptr); } // uint32_t stop = millis(); diff --git a/src/graphics/niche/InkHUD/Renderer.h b/src/graphics/niche/InkHUD/Renderer.h index b6cf9e215..1ab94b70b 100644 --- a/src/graphics/niche/InkHUD/Renderer.h +++ b/src/graphics/niche/InkHUD/Renderer.h @@ -37,8 +37,8 @@ class Renderer : protected concurrency::OSThread // Call these to make the image change - void requestUpdate(); // Update display, if a foreground applet has info it wants to show - void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, + void requestUpdate(bool all = false); // Update display, if a foreground applet has info it wants to show + void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false, bool async = true); // Update display, regardless of whether any applets requested this // Wait for an update to complete @@ -53,7 +53,7 @@ class Renderer : protected concurrency::OSThread uint16_t height(); private: - // Make attemps to render / update, once triggered by requestUpdate or forceUpdate + // Make attempts to render / update, once triggered by requestUpdate or forceUpdate int32_t runOnce() override; // Apply the display rotation to handled pixels @@ -65,6 +65,7 @@ class Renderer : protected concurrency::OSThread // Steps of the rendering process void clearBuffer(); + void clearTile(Tile *t); void checkLocks(); bool shouldUpdate(); Drivers::EInk::UpdateTypes decideUpdateType(); @@ -85,6 +86,7 @@ class Renderer : protected concurrency::OSThread bool requested = false; bool forced = false; + bool renderAll = false; // For convenience InkHUD *inkhud = nullptr; @@ -93,4 +95,4 @@ class Renderer : protected concurrency::OSThread } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/SystemApplet.h b/src/graphics/niche/InkHUD/SystemApplet.h index fb5b06e51..32e0e58bb 100644 --- a/src/graphics/niche/InkHUD/SystemApplet.h +++ b/src/graphics/niche/InkHUD/SystemApplet.h @@ -22,9 +22,11 @@ class SystemApplet : public Applet public: // System applets have the right to: - bool handleInput = false; // - respond to input from the user button - bool lockRendering = false; // - prevent other applets from being rendered during an update - bool lockRequests = false; // - prevent other applets from triggering display updates + bool handleInput = false; // - respond to input from the user button + bool handleFreeText = false; // - respond to free text input + bool lockRendering = false; // - prevent other applets from being rendered during an update + bool lockRequests = false; // - prevent other applets from triggering display updates + bool alwaysRender = false; // - render every time the screen is updated virtual void onReboot() { onShutdown(); } // - handle reboot specially virtual void onApplyingChanges() {} @@ -41,4 +43,4 @@ class SystemApplet : public Applet }; // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Tile.cpp b/src/graphics/niche/InkHUD/Tile.cpp index 5e548de74..8beb25f39 100644 --- a/src/graphics/niche/InkHUD/Tile.cpp +++ b/src/graphics/niche/InkHUD/Tile.cpp @@ -18,7 +18,7 @@ static int32_t runtaskHighlight() LOG_DEBUG("Dismissing Highlight"); InkHUD::Tile::highlightShown = false; InkHUD::Tile::highlightTarget = nullptr; - InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting + InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); // Re-render, clearing the highlighting return taskHighlight->disable(); } static void inittaskHighlight() @@ -190,6 +190,18 @@ void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) } } +// Used in Renderer for clearing the tile +int16_t InkHUD::Tile::getLeft() +{ + return left; +} + +// Used in Renderer for clearing the tile +int16_t InkHUD::Tile::getTop() +{ + return top; +} + // Called by Applet base class, when setting applet dimensions, immediately before render uint16_t InkHUD::Tile::getWidth() { @@ -220,7 +232,7 @@ void InkHUD::Tile::requestHighlight() { Tile::highlightTarget = this; Tile::highlightShown = false; - inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); } // Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first diff --git a/src/graphics/niche/InkHUD/Tile.h b/src/graphics/niche/InkHUD/Tile.h index 0f5444f17..0c09e4704 100644 --- a/src/graphics/niche/InkHUD/Tile.h +++ b/src/graphics/niche/InkHUD/Tile.h @@ -29,6 +29,8 @@ class Tile void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet + int16_t getLeft(); + int16_t getTop(); uint16_t getWidth(); uint16_t getHeight(); static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index 0548de1eb..c4a0813d8 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -4,6 +4,7 @@ #include "./Applets/System/AlignStick/AlignStickApplet.h" #include "./Applets/System/BatteryIcon/BatteryIconApplet.h" +#include "./Applets/System/Keyboard/KeyboardApplet.h" #include "./Applets/System/Logo/LogoApplet.h" #include "./Applets/System/Menu/MenuApplet.h" #include "./Applets/System/Notification/NotificationApplet.h" @@ -142,12 +143,40 @@ void InkHUD::WindowManager::openMenu() // Bring the AlignStick applet to the foreground void InkHUD::WindowManager::openAlignStick() { - if (settings->joystick.enabled) { + if (settings->joystick.enabled && !inkhud->twoWayRocker) { AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); alignStick->bringToForeground(); } } +void InkHUD::WindowManager::openKeyboard() +{ + if (!settings->joystick.enabled || inkhud->twoWayRocker) + return; + + KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); + + if (keyboard) { + keyboard->bringToForeground(); + keyboardOpen = true; + changeLayout(); + } +} + +void InkHUD::WindowManager::closeKeyboard() +{ + if (!settings->joystick.enabled || inkhud->twoWayRocker) + return; + + KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); + + if (keyboard) { + keyboard->sendToBackground(); + keyboardOpen = false; + changeLayout(); + } +} + // On the currently focussed tile: cycle to the next available user applet // Applets available for this must be activated, and not already displayed on another tile void InkHUD::WindowManager::nextApplet() @@ -250,6 +279,12 @@ void InkHUD::WindowManager::prevApplet() inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } +// Returns active applet +NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet() +{ + return userTiles.at(settings->userTiles.focused)->getAssignedApplet(); +} + // Rotate the display image by 90 degrees void InkHUD::WindowManager::rotate() { @@ -272,7 +307,6 @@ void InkHUD::WindowManager::toggleBatteryIcon() batteryIcon->sendToBackground(); // Force-render - // - redraw all applets inkhud->forceUpdate(EInk::UpdateTypes::FAST); } @@ -311,9 +345,25 @@ void InkHUD::WindowManager::changeLayout() menu->show(ft); } + // Resize for the on-screen keyboard + if (keyboardOpen) { + // Send all user applets to the background + // User applets currently don't handle free text input + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) + inkhud->userApplets.at(i)->sendToBackground(); + // Find the first system applet that can handle freetext and resize it + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleFreeText) { + const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); + sa->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height() - keyboardHeight - 1); + break; + } + } + } + // Force-render // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + inkhud->forceUpdate(EInk::UpdateTypes::FAST, true); } // Perform necessary reconfiguration when user activates or deactivates applets at run-time @@ -347,7 +397,7 @@ void InkHUD::WindowManager::changeActivatedApplets() // Force-render // - redraw all applets - inkhud->forceUpdate(EInk::UpdateTypes::FAST); + inkhud->forceUpdate(EInk::UpdateTypes::FAST, true); } // Some applets may be permitted to bring themselves to foreground, to show new data @@ -358,7 +408,7 @@ void InkHUD::WindowManager::autoshow() { // Don't perform autoshow if a system applet has exclusive use of the display right now // Note: lockRequests prevents autoshow attempting to hide menuApplet - for (SystemApplet *sa : inkhud->systemApplets) { + for (const SystemApplet *sa : inkhud->systemApplets) { if (sa->lockRendering || sa->lockRequests) return; } @@ -433,8 +483,10 @@ void InkHUD::WindowManager::createSystemApplets() addSystemApplet("Logo", new LogoApplet, new Tile); addSystemApplet("Pairing", new PairingApplet, new Tile); addSystemApplet("Tips", new TipsApplet, new Tile); - if (settings->joystick.enabled) + if (settings->joystick.enabled && !inkhud->twoWayRocker) { addSystemApplet("AlignStick", new AlignStickApplet, new Tile); + addSystemApplet("Keyboard", new KeyboardApplet, new Tile); + } addSystemApplet("Menu", new MenuApplet, nullptr); @@ -457,19 +509,23 @@ void InkHUD::WindowManager::placeSystemTiles() inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - if (settings->joystick.enabled) + if (settings->joystick.enabled && !inkhud->twoWayRocker) { inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - + const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); + inkhud->getSystemApplet("Keyboard") + ->getTile() + ->setRegion(0, inkhud->height() - keyboardHeight, inkhud->width(), keyboardHeight); + } inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2; const uint16_t batteryIconWidth = batteryIconHeight * 1.8; inkhud->getSystemApplet("BatteryIcon") ->getTile() - ->setRegion(inkhud->width() - batteryIconWidth, // x - 2, // y - batteryIconWidth, // width - batteryIconHeight); // height + ->setRegion(inkhud->width() - batteryIconWidth - 1, // x + 1, // y + batteryIconWidth + 1, // width + batteryIconHeight + 2); // height // Note: the tiles of placeholder and menu applets are manipulated specially // - menuApplet borrows user tiles @@ -599,7 +655,7 @@ void InkHUD::WindowManager::refocusTile() } } -// Seach for any applets which believe they are foreground, but no longer have a valid tile +// Search for any applets which believe they are foreground, but no longer have a valid tile // Tidies up after layout changes at runtime void InkHUD::WindowManager::findOrphanApplets() { @@ -629,4 +685,4 @@ void InkHUD::WindowManager::findOrphanApplets() } } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index 5def48f8c..a11688cf5 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -29,8 +29,11 @@ class WindowManager void nextTile(); void prevTile(); + Applet *getActiveApplet(); void openMenu(); void openAlignStick(); + void openKeyboard(); + void closeKeyboard(); void nextApplet(); void prevApplet(); void rotate(); @@ -64,6 +67,7 @@ class WindowManager void findOrphanApplets(); // Find any applets left-behind when layout changes std::vector userTiles; // Tiles which can host user applets + bool keyboardOpen = false; // For convenience InkHUD *inkhud = nullptr; diff --git a/src/graphics/niche/InkHUD/docs/README.md b/src/graphics/niche/InkHUD/docs/README.md index aa23f065f..7cd468d73 100644 --- a/src/graphics/niche/InkHUD/docs/README.md +++ b/src/graphics/niche/InkHUD/docs/README.md @@ -174,7 +174,7 @@ class BasicExampleApplet : public Applet // You must have an onRender() method // All drawing happens here - void onRender() override; + void onRender(bool full) override; }; ``` @@ -183,7 +183,7 @@ The `onRender` method is called when the display image is redrawn. This can happ ```cpp // All drawing happens here // Our basic example doesn't do anything useful. It just passively prints some text. -void InkHUD::BasicExampleApplet::onRender() +void InkHUD::BasicExampleApplet::onRender(bool full) { printAt(0, 0, "Hello, world!"); } @@ -733,7 +733,7 @@ To add support for additional encodings, add to the `AppletFont::Encodings` enum #### Custom Line Height -Some fonts may have a handful of especially tall characters, especially extended-ASCII fonts with diacritcs. Ideally, the font should be modified to help resolve this, but if the problem remains, manual offsets to the automatically determined line height can be specified in the constructor. +Some fonts may have a handful of especially tall characters, especially extended-ASCII fonts with diacritics. Ideally, the font should be modified to help resolve this, but if the problem remains, manual offsets to the automatically determined line height can be specified in the constructor. ```cpp // -2 px of padding above, +1 px of padding below diff --git a/src/graphics/niche/Inputs/TwoButton.cpp b/src/graphics/niche/Inputs/TwoButton.cpp index bd29f981d..1a27e039b 100644 --- a/src/graphics/niche/Inputs/TwoButton.cpp +++ b/src/graphics/niche/Inputs/TwoButton.cpp @@ -59,7 +59,7 @@ void TwoButton::stop() } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings -// This helper method isn't used by the TweButton class itself, it could be moved elsewhere. +// This helper method isn't used by the TwoButton class itself, it could be moved elsewhere. // Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method. uint8_t TwoButton::getUserButtonPin() { @@ -308,4 +308,4 @@ int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) #endif -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp index 287fb943f..f979faca9 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.cpp +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -156,6 +156,24 @@ void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lP pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); } +// Configures only left/right joystick directions for a two-way rocker +void TwoButtonExtended::setTwoWayRockerWiring(uint8_t leftPin, uint8_t rightPin, bool internalPullup) +{ + if (leftPin == rightPin) { + LOG_WARN("Attempted reuse of TwoWayRocker GPIO. Ignoring assignment"); + return; + } + + joystick[Direction::UP].pin = 0xFF; + joystick[Direction::DOWN].pin = 0xFF; + joystick[Direction::LEFT].pin = leftPin; + joystick[Direction::RIGHT].pin = rightPin; + joystickActiveLogic = LOW; + + pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); +} + void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) { assert(whichButton < 2); @@ -229,6 +247,13 @@ void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPres joystick[Direction::RIGHT].onPress = rPress; } +// Set press handlers for a two-way rocker mapped to left/right directions +void TwoButtonExtended::setTwoWayRockerPressHandlers(Callback lPress, Callback rPress) +{ + joystick[Direction::LEFT].onPress = lPress; + joystick[Direction::RIGHT].onPress = rPress; +} + // Handle the start of a press to the primary button // Wakes our button thread void TwoButtonExtended::isrPrimary() diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h index 23fd78a2a..eb536907d 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.h +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -45,6 +45,7 @@ class TwoButtonExtended : protected concurrency::OSThread void stop(); // Stop handling button input (disconnect ISRs for sleep) void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); + void setTwoWayRockerWiring(uint8_t leftPin, uint8_t rightPin, bool internalPullup = false); void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); void setJoystickDebounce(uint32_t debounceMs); void setHandlerDown(uint8_t whichButton, Callback onDown); @@ -54,6 +55,7 @@ class TwoButtonExtended : protected concurrency::OSThread void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); + void setTwoWayRockerPressHandlers(Callback lPress, Callback rPress); // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 diff --git a/src/graphics/niche/Utils/CannedMessageStore.cpp b/src/graphics/niche/Utils/CannedMessageStore.cpp index 50998930d..182b7e1f8 100644 --- a/src/graphics/niche/Utils/CannedMessageStore.cpp +++ b/src/graphics/niche/Utils/CannedMessageStore.cpp @@ -146,7 +146,7 @@ void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) std::string merged; if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 merged.reserve(201); - for (std::string &s : messages) { + for (const std::string &s : messages) { merged += s; merged += '|'; } diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 5654fa02a..708cd8967 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -16,6 +16,10 @@ DeviceScreen *deviceScreen = nullptr; +#ifndef TFT_TASK_STACK_SIZE +#define TFT_TASK_STACK_SIZE 16384 +#endif + #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver tftSleepObserver = @@ -127,7 +131,7 @@ void tftSetup(void) #ifdef ARCH_ESP32 tftSleepObserver.observe(¬ifyLightSleep); endSleepObserver.observe(¬ifyLightSleepEnd); - xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); + xTaskCreatePinnedToCore(tft_task_handler, "tft", TFT_TASK_STACK_SIZE, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 0d835a3a9..df8de4905 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -271,12 +271,13 @@ int32_t ButtonThread::runOnce() break; } // end multipress event - // Do actual shutdown when button released, otherwise the button release - // may wake the board immediatedly. + // Do actual shutdown when button released, otherwise the button release + // may wake the board immediately. case BUTTON_EVENT_LONG_RELEASED: { LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); - if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && + // Require press started after boot holdoff to avoid phantom shutdown from floating pins + if (millis() > 30000 && buttonPressStartTime > 30000 && _longLongPress != INPUT_BROKER_NONE && (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) { evt.inputEvent = _longLongPress; this->notifyObservers(&evt); @@ -346,4 +347,4 @@ int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) void ButtonThread::storeClickCount() { multipressClickCount = userButton.getNumberClicks(); -} \ No newline at end of file +} diff --git a/src/input/CardputerKeyboard.cpp b/src/input/CardputerKeyboard.cpp new file mode 100644 index 000000000..1bd695461 --- /dev/null +++ b/src/input/CardputerKeyboard.cpp @@ -0,0 +1,199 @@ +#if defined(M5STACK_CARDPUTER_ADV) + +#include "CardputerKeyboard.h" +#include "main.h" + +#define _TCA8418_COLS 8 +#define _TCA8418_ROWS 7 +#define _TCA8418_NUM_KEYS 56 + +#define _TCA8418_MULTI_TAP_THRESHOLD 1500 + +using Key = TCA8418KeyboardBase::TCA8418Key; + +constexpr uint8_t modifierFnKey = 2; +constexpr uint8_t modifierFn = 0b0010; +constexpr uint8_t modifierCtrlKey = 3; +constexpr uint8_t modifierShiftKey = 6; +constexpr uint8_t modifierShift = 0b0001; +constexpr uint8_t modifierOptKey = 7; +constexpr uint8_t modifierAltKey = 11; + +// Num chars per key, Modulus for rotating through characters +static uint8_t CardputerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; + +static unsigned char CardputerTapMap[_TCA8418_NUM_KEYS][3] = {{'`', '~', Key::ESC}, + {Key::TAB, 0x00, 0x00}, + {0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00}, + {'1', '!', 0x00}, + {'q', 'Q', Key::REBOOT}, + {0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00}, + {'2', '@', 0x00}, + {'w', 'W', 0x00}, + {'a', 'A', 0x00}, + {0x00, 0x00, 0x00}, + {'3', '#', 0x00}, + {'e', 'E', 0x00}, + {'s', 'S', 0x00}, + {'z', 'Z', 0x00}, + {'4', '$', 0x00}, + {'r', 'R', 0x00}, + {'d', 'D', 0x00}, + {'x', 'X', 0x00}, + {'5', '%', 0x00}, + {'t', 'T', 0x00}, + {'f', 'F', 0x00}, + {'c', 'C', 0x00}, + {'6', '^', 0x00}, + {'y', 'Y', 0x00}, + {'g', 'G', Key::GPS_TOGGLE}, + {'v', 'V', 0x00}, + {'7', '&', 0x00}, + {'u', 'U', 0x00}, + {'h', 'H', 0x00}, + {'b', 'B', Key::BT_TOGGLE}, + {'8', '*', 0x00}, + {'i', 'I', 0x00}, + {'j', 'J', 0x00}, + {'n', 'N', 0x00}, + {'9', '(', 0x00}, + {'o', 'O', 0x00}, + {'k', 'K', 0x00}, + {'m', 'M', Key::MUTE_TOGGLE}, + {'0', ')', 0x00}, + {'p', 'P', Key::SEND_PING}, + {'l', 'L', 0x00}, + {',', '<', Key::LEFT}, + {'_', '-', 0x00}, + {'[', '{', 0x00}, + {';', ':', Key::UP}, + {'.', '>', Key::DOWN}, + {'=', '+', 0x00}, + {']', '}', 0x00}, + {'\'', '"', 0x00}, + {'/', '?', Key::RIGHT}, + {Key::BSP, 0x00, 0x00}, + {'\\', '|', 0x00}, + {Key::SELECT, 0x00, 0x00}, + {' ', ' ', ' '}}; + +CardputerKeyboard::CardputerKeyboard() + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ + reset(); +} + +void CardputerKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); +} + +// handle multi-key presses (shift and alt) +void CardputerKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } + } +} + +void CardputerKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } + + int row = (key - 1) / 10; + int col = (key - 1) % 10; + + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + next_key = row * _TCA8418_COLS + col; + state = Held; + + uint32_t now = millis(); + tap_interval = now - last_tap; + + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } + + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; +} + +void CardputerKeyboard::released() +{ + if (state != Held) { + return; + } + + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } + + uint32_t now = millis(); + last_tap = now; + + if (inputBroker->menuMode && modifierFlag == 0) { + if (last_key == 0 || last_key == 43 || last_key == 46 || last_key == 47 || + last_key == 51) { // esc, left, up, down, right key + modifierFlag = modifierFn; + } + } + + queueEvent(CardputerTapMap[last_key][modifierFlag % CardputerTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; +} + +void CardputerKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierShiftKey) { + modifierFlag ^= modifierShift; + } else if (key == modifierFnKey) { + modifierFlag ^= modifierFn; + } +} + +bool CardputerKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierShiftKey || key == modifierFnKey); +} + +#endif \ No newline at end of file diff --git a/src/input/CardputerKeyboard.h b/src/input/CardputerKeyboard.h new file mode 100644 index 000000000..c9de1f36b --- /dev/null +++ b/src/input/CardputerKeyboard.h @@ -0,0 +1,26 @@ +#include "TCA8418KeyboardBase.h" + +class CardputerKeyboard : public TCA8418KeyboardBase +{ + public: + CardputerKeyboard(); + void reset(void); + void trigger(void) override; + virtual ~CardputerKeyboard() {} + + protected: + void pressed(uint8_t key) override; + void released(void) override; + + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + + private: + uint8_t modifierFlag; + uint32_t last_modifier_time; + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; +}; diff --git a/src/input/HackadayCommunicatorKeyboard.cpp b/src/input/HackadayCommunicatorKeyboard.cpp index c6a9e0ae8..b096c74d2 100644 --- a/src/input/HackadayCommunicatorKeyboard.cpp +++ b/src/input/HackadayCommunicatorKeyboard.cpp @@ -106,8 +106,8 @@ static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, {}}; HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), - last_tap(0L), char_idx(0), tap_interval(0) + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), + next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { reset(); } @@ -147,7 +147,6 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key) modifierFlag = 0; } - uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { @@ -187,8 +186,8 @@ void HackadayCommunicatorKeyboard::released() return; } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; + if (last_key >= _TCA8418_NUM_KEYS) { + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/HackadayCommunicatorKeyboard.h b/src/input/HackadayCommunicatorKeyboard.h index 8316bed72..cbba5c12f 100644 --- a/src/input/HackadayCommunicatorKeyboard.h +++ b/src/input/HackadayCommunicatorKeyboard.h @@ -18,8 +18,8 @@ class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase private: uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; + uint8_t last_key; + uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index 0aa78e2b8..b7c9b27a9 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -1,8 +1,59 @@ #include "InputBroker.h" #include "PowerFSM.h" // needed for event trigger #include "configuration.h" +#include "graphics/Screen.h" #include "modules/ExternalNotificationModule.h" +#if ARCH_PORTDUINO +#include "input/LinuxInputImpl.h" +#include "input/SeesawRotary.h" +#include "platform/portduino/PortduinoGlue.h" +#endif + +#if !MESHTASTIC_EXCLUDE_INPUTBROKER +#include "input/ExpressLRSFiveWay.h" +#include "input/RotaryEncoderImpl.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/SerialKeyboardImpl.h" +#include "input/UpDownInterruptImpl1.h" +#include "input/i2cButton.h" +#if HAS_TRACKBALL +#include "input/TrackballInterruptImpl1.h" +#endif + +#include "modules/StatusLEDModule.h" + +#if !MESHTASTIC_EXCLUDE_I2C +#include "input/cardKbI2cImpl.h" +#endif +#include "input/kbMatrixImpl.h" +#endif + +#if HAS_BUTTON || defined(ARCH_PORTDUINO) +#include "input/ButtonThread.h" + +#if defined(BUTTON_PIN_TOUCH) +ButtonThread *TouchButtonThread = nullptr; +#if defined(PIN_EINK_EN) +static bool touchBacklightWasOn = false; +static bool touchBacklightActive = false; +#endif +#endif + +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) +ButtonThread *UserButtonThread = nullptr; +#endif + +#if defined(ALT_BUTTON_PIN) +ButtonThread *BackButtonThread = nullptr; +#endif + +#if defined(CANCEL_BUTTON_PIN) +ButtonThread *CancelButtonThread = nullptr; +#endif + +#endif + InputBroker *inputBroker = nullptr; InputBroker::InputBroker() @@ -49,13 +100,28 @@ void InputBroker::processInputEventQueue() int InputBroker::handleInputEvent(const InputEvent *event) { - powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release +#if HAS_SCREEN + bool screenWasOff = false; + if (screen) { + screenWasOff = !screen->isScreenOn(); + } +#endif + powerFSM.trigger(EVENT_INPUT); if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule && moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) { externalNotificationModule->stopNow(); + // If this turns off a notification, don't further process the event + return 0; } +#if HAS_SCREEN + if (screen && screenWasOff) { + // If the screen was off, it is in the process of turning on, and we just drop the event + return 0; + } +#endif + this->notifyObservers(event); return 0; } @@ -74,3 +140,267 @@ void InputBroker::pollSoonWorker(void *p) vTaskDelete(NULL); } #endif + +void InputBroker::Init() +{ + +#ifdef BUTTON_PIN +#ifdef ARCH_ESP32 + +#if ESP_ARDUINO_VERSION_MAJOR >= 3 +#ifdef BUTTON_NEED_PULLUP + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); +#else + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN +#endif +#else + pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN +#ifdef BUTTON_NEED_PULLUP + gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); + delay(10); +#endif +#ifdef BUTTON_NEED_PULLUP2 + gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); + delay(10); +#endif +#endif +#endif +#endif + +// buttons are now inputBroker, so have to come after setupModules +#if HAS_BUTTON + int pullup_sense = 0; +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did +#ifdef BUTTON_SENSE_TYPE + pullup_sense = BUTTON_SENSE_TYPE; +#else + pullup_sense = INPUT_PULLUP_SENSE; +#endif +#endif +#if defined(ARCH_PORTDUINO) + + if (portduino_config.userButtonPin.enabled) { + + LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); + UserButtonThread = new ButtonThread("UserButton"); + if (screen) { + ButtonConfig config; + config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; + config.activeLow = true; + config.activePullup = true; + config.pullupSense = INPUT_PULLUP; + config.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + config.singlePress = INPUT_BROKER_USER_PRESS; + config.longPress = INPUT_BROKER_SELECT; + UserButtonThread->initButton(config); + } + } +#endif + +#ifdef BUTTON_PIN_TOUCH + TouchButtonThread = new ButtonThread("BackButton"); + ButtonConfig touchConfig; + touchConfig.pinNumber = BUTTON_PIN_TOUCH; + touchConfig.activeLow = true; + touchConfig.activePullup = true; + touchConfig.pullupSense = pullup_sense; + touchConfig.intRoutine = []() { + TouchButtonThread->userButton.tick(); + TouchButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + touchConfig.singlePress = INPUT_BROKER_NONE; + touchConfig.longPress = INPUT_BROKER_BACK; +#if defined(PIN_EINK_EN) + // Touch pad drives the backlight on devices with e-ink backlight pin + touchConfig.longPress = INPUT_BROKER_NONE; + touchConfig.suppressLeadUpSound = true; + touchConfig.onPress = []() { + touchBacklightWasOn = uiconfig.screen_brightness == 1; + if (!touchBacklightWasOn) { + digitalWrite(PIN_EINK_EN, HIGH); + } + touchBacklightActive = true; + }; + touchConfig.onRelease = []() { + if (touchBacklightActive && !touchBacklightWasOn) { + digitalWrite(PIN_EINK_EN, LOW); + } + touchBacklightActive = false; + }; +#endif + TouchButtonThread->initButton(touchConfig); +#endif + +#if defined(CANCEL_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + CancelButtonThread = new ButtonThread("CancelButton"); + ButtonConfig cancelConfig; + cancelConfig.pinNumber = CANCEL_BUTTON_PIN; + cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; + cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; + cancelConfig.pullupSense = pullup_sense; + cancelConfig.intRoutine = []() { + CancelButtonThread->userButton.tick(); + CancelButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + cancelConfig.singlePress = INPUT_BROKER_CANCEL; + cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; + cancelConfig.longPressTime = 4000; + CancelButtonThread->initButton(cancelConfig); +#endif + +#if defined(ALT_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + BackButtonThread = new ButtonThread("BackButton"); + ButtonConfig backConfig; + backConfig.pinNumber = ALT_BUTTON_PIN; + backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; + backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; + backConfig.pullupSense = pullup_sense; + backConfig.intRoutine = []() { + BackButtonThread->userButton.tick(); + BackButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + backConfig.singlePress = INPUT_BROKER_ALT_PRESS; + backConfig.longPress = INPUT_BROKER_ALT_LONG; + backConfig.longPressTime = 500; + BackButtonThread->initButton(backConfig); +#endif + +#if defined(BUTTON_PIN) +#if defined(USERPREFS_BUTTON_PIN) + int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; +#else + int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; +#endif +#ifndef BUTTON_ACTIVE_LOW +#define BUTTON_ACTIVE_LOW true +#endif +#ifndef BUTTON_ACTIVE_PULLUP +#define BUTTON_ACTIVE_PULLUP true +#endif + + // Buttons. Moved here cause we need NodeDB to be initialized + // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP + UserButtonThread = new ButtonThread("UserButton"); +#if !MESHTASTIC_EXCLUDE_SCREEN + if (screen) { + ButtonConfig userConfig; + userConfig.pinNumber = (uint8_t)_pinNum; + userConfig.activeLow = BUTTON_ACTIVE_LOW; + userConfig.activePullup = BUTTON_ACTIVE_PULLUP; + userConfig.pullupSense = pullup_sense; + userConfig.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + userConfig.singlePress = INPUT_BROKER_USER_PRESS; + userConfig.longPress = INPUT_BROKER_SELECT; + userConfig.longPressTime = 500; + userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; + UserButtonThread->initButton(userConfig); + } else +#endif + { + ButtonConfig userConfigNoScreen; + userConfigNoScreen.pinNumber = (uint8_t)_pinNum; + userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; + userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; + userConfigNoScreen.pullupSense = pullup_sense; + userConfigNoScreen.intRoutine = []() { + UserButtonThread->userButton.tick(); + UserButtonThread->setIntervalFromNow(0); + runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + }; + userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; + userConfigNoScreen.longPress = INPUT_BROKER_NONE; + userConfigNoScreen.longPressTime = 500; + userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; + userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; + userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; + UserButtonThread->initButton(userConfigNoScreen); + } +#endif +#endif + +#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { +#if defined(T_LORA_PAGER) + // use a special FSM based rotary encoder version for T-LoRa Pager + rotaryEncoderImpl = new RotaryEncoderImpl(); + if (!rotaryEncoderImpl->init()) { + delete rotaryEncoderImpl; + rotaryEncoderImpl = nullptr; + } +#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } +#else + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } +#endif + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); +#if defined(M5STACK_UNITC6L) + i2cButton = new i2cButtonThread("i2cButtonThread"); +#endif +#ifdef INPUTBROKER_MATRIX_TYPE + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); +#endif // INPUTBROKER_MATRIX_TYPE +#ifdef INPUTBROKER_SERIAL_TYPE + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); +#endif // INPUTBROKER_MATRIX_TYPE + } +#endif // HAS_BUTTON +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (portduino_config.i2cdev != "") { + seesawRotary = new SeesawRotary("SeesawRotary"); + if (!seesawRotary->init()) { + delete seesawRotary; + seesawRotary = nullptr; + } + } + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); + } +#endif +#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); + } +#endif +#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE + expressLRSFiveWayInput = new ExpressLRSFiveWay(); +#endif +} diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 970e9969a..847604011 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -1,6 +1,7 @@ #pragma once #include "Observer.h" +#include "concurrency/OSThread.h" #include "freertosinc.h" #ifdef InputBrokerDebug @@ -69,6 +70,7 @@ class InputBroker : public Observable public: InputBroker(); + bool menuMode = true; void registerSource(Observable *source); void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) @@ -76,6 +78,7 @@ class InputBroker : public Observable void queueInputEvent(const InputEvent *event); void processInputEventQueue(); #endif + void Init(); protected: int handleInputEvent(const InputEvent *event); @@ -89,4 +92,5 @@ class InputBroker : public Observable #endif }; -extern InputBroker *inputBroker; \ No newline at end of file +extern InputBroker *inputBroker; +extern bool runASAP; \ No newline at end of file diff --git a/src/input/MPR121Keyboard.cpp b/src/input/MPR121Keyboard.cpp index 9bca6801d..80a272d3b 100644 --- a/src/input/MPR121Keyboard.cpp +++ b/src/input/MPR121Keyboard.cpp @@ -91,7 +91,7 @@ MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(null { // LOG_DEBUG("MPR121 @ %02x", m_addr); state = Init; - last_key = -1; + last_key = UINT8_MAX; last_tap = 0L; char_idx = 0; queue = ""; @@ -177,7 +177,7 @@ void MPR121Keyboard::reset() delay(20); writeRegister(_MPR121_REG_CONFIG2, 0x21); delay(20); - // Enter run mode by Seting partial filter calibration tracking, disable proximity detection, enable 12 channels + // Enter run mode by setting partial filter calibration tracking, disable proximity detection, enable 12 channels writeRegister(_MPR121_REG_ELECTRODE_CONFIG, ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); delay(100); @@ -359,8 +359,8 @@ void MPR121Keyboard::released() return; } // would clear longpress callback... later. - if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds - last_key = -1; + if (last_key >= _NUM_KEYS) { // reset to idle if last_key out of bounds + last_key = UINT8_MAX; state = Idle; return; } @@ -430,4 +430,4 @@ void MPR121Keyboard::writeRegister(uint8_t reg, uint8_t value) if (writeCallback) { writeCallback(m_addr, data[0], &(data[1]), 1); } -} \ No newline at end of file +} diff --git a/src/input/MPR121Keyboard.h b/src/input/MPR121Keyboard.h index 6349750ce..ec3f56e87 100644 --- a/src/input/MPR121Keyboard.h +++ b/src/input/MPR121Keyboard.h @@ -14,7 +14,7 @@ class MPR121Keyboard MPR121States state; - int8_t last_key; + uint8_t last_key; uint32_t last_tap; uint8_t char_idx; diff --git a/src/input/SeesawRotary.cpp b/src/input/SeesawRotary.cpp index 0a6e6e974..dc57b296b 100644 --- a/src/input/SeesawRotary.cpp +++ b/src/input/SeesawRotary.cpp @@ -59,7 +59,7 @@ int32_t SeesawRotary::runOnce() wasPressed = currentlyPressed; int32_t new_position = ss.getEncoderPosition(); - // did we move arounde? + // did we move around? if (encoder_position != new_position) { if (encoder_position == 0 && new_position != 1) { e.inputEvent = INPUT_BROKER_ALT_PRESS; @@ -80,4 +80,4 @@ int32_t SeesawRotary::runOnce() return 50; } -#endif \ No newline at end of file +#endif diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index a5d2c614f..8037b0d57 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -5,7 +5,6 @@ SerialKeyboard *globalSerialKeyboard = nullptr; #ifdef INPUTBROKER_SERIAL_TYPE -#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file #if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter // 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp index bd8338acf..238b9bb51 100644 --- a/src/input/TCA8418Keyboard.cpp +++ b/src/input/TCA8418Keyboard.cpp @@ -43,8 +43,8 @@ static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = { }; TCA8418Keyboard::TCA8418Keyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0), - should_backspace(false) + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0), + tap_interval(0), should_backspace(false) { } @@ -63,7 +63,6 @@ void TCA8418Keyboard::pressed(uint8_t key) if (state == Init || state == Busy) { return; } - uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; @@ -72,7 +71,7 @@ void TCA8418Keyboard::pressed(uint8_t key) } // Compute key index based on dynamic row/column - next_key = row * _TCA8418_COLS + col; + next_key = (uint8_t)(row * _TCA8418_COLS + col); // LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key); @@ -89,7 +88,7 @@ void TCA8418Keyboard::pressed(uint8_t key) // Check if the key is the same as the last one or if the time interval has passed if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { char_idx = 0; // Reset char index if new key or long press - should_backspace = false; // dont backspace on new key + should_backspace = false; // don't backspace on new key } else { char_idx += 1; // Cycle through characters if same key pressed should_backspace = true; // allow backspace on same key @@ -106,8 +105,8 @@ void TCA8418Keyboard::released() return; } - if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds - last_key = -1; + if (last_key >= _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/TCA8418Keyboard.h b/src/input/TCA8418Keyboard.h index b76916643..0e8821260 100644 --- a/src/input/TCA8418Keyboard.h +++ b/src/input/TCA8418Keyboard.h @@ -14,8 +14,8 @@ class TCA8418Keyboard : public TCA8418KeyboardBase void pressed(uint8_t key) override; void released(void) override; - int8_t last_key; - int8_t next_key; + uint8_t last_key; + uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; diff --git a/src/input/TDeckProKeyboard.cpp b/src/input/TDeckProKeyboard.cpp index eeafe4949..b83f0c6ae 100644 --- a/src/input/TDeckProKeyboard.cpp +++ b/src/input/TDeckProKeyboard.cpp @@ -62,8 +62,8 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = { }; TDeckProKeyboard::TDeckProKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), - last_tap(0L), char_idx(0), tap_interval(0) + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), + next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { } @@ -101,7 +101,6 @@ void TDeckProKeyboard::pressed(uint8_t key) modifierFlag = 0; } - uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; @@ -142,8 +141,8 @@ void TDeckProKeyboard::released() return; } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; + if (last_key >= _TCA8418_NUM_KEYS) { + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/TDeckProKeyboard.h b/src/input/TDeckProKeyboard.h index 617f3f20b..3ef97fc3d 100644 --- a/src/input/TDeckProKeyboard.h +++ b/src/input/TDeckProKeyboard.h @@ -19,8 +19,8 @@ class TDeckProKeyboard : public TCA8418KeyboardBase private: uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; + uint8_t last_key; + uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp index 9a4fd8679..4efa5d6e2 100644 --- a/src/input/TLoraPagerKeyboard.cpp +++ b/src/input/TLoraPagerKeyboard.cpp @@ -65,8 +65,8 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, {' ', 0x00, Key::BL_TOGGLE}}; TLoraPagerKeyboard::TLoraPagerKeyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), - last_tap(0L), char_idx(0), tap_interval(0) + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), + next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); @@ -129,7 +129,6 @@ void TLoraPagerKeyboard::pressed(uint8_t key) modifierFlag = 0; } - uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; @@ -170,8 +169,8 @@ void TLoraPagerKeyboard::released() return; } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; + if (last_key >= _TCA8418_NUM_KEYS) { + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h index f04d2ce6a..06a4c7b63 100644 --- a/src/input/TLoraPagerKeyboard.h +++ b/src/input/TLoraPagerKeyboard.h @@ -21,8 +21,8 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase private: uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed uint32_t last_modifier_time; // Timestamp of the last modifier key press - int8_t last_key; - int8_t next_key; + uint8_t last_key; + uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp index 8f842bc9b..4fd275a40 100644 --- a/src/input/TouchScreenBase.cpp +++ b/src/input/TouchScreenBase.cpp @@ -43,7 +43,7 @@ int32_t TouchScreenBase::runOnce() // process touch events int16_t x, y; bool touched = getTouch(x, y); - if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen + if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turning off the screen touched = false; if (touched) { this->setInterval(20); @@ -123,7 +123,7 @@ int32_t TouchScreenBase::runOnce() } } #else - // fire TAP event when no 2nd tap occured within time + // fire TAP event when no 2nd tap occurred within time if (_tapped) { _tapped = false; e.touchEvent = static_cast(TOUCH_ACTION_TAP); diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 906dcd2a8..4f62fd5fa 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -8,6 +8,14 @@ UpDownInterruptImpl1::UpDownInterruptImpl1() : UpDownInterruptBase("upDown1") {} bool UpDownInterruptImpl1::init() { +#if defined(INPUTDRIVER_TWO_WAY_ROCKER) && defined(INPUTDRIVER_TWO_WAY_ROCKER_LEFT) && defined(INPUTDRIVER_TWO_WAY_ROCKER_RIGHT) + moduleConfig.canned_message.updown1_enabled = true; + moduleConfig.canned_message.inputbroker_pin_a = INPUTDRIVER_TWO_WAY_ROCKER_LEFT; + moduleConfig.canned_message.inputbroker_pin_b = INPUTDRIVER_TWO_WAY_ROCKER_RIGHT; +#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) + moduleConfig.canned_message.inputbroker_pin_press = INPUTDRIVER_TWO_WAY_ROCKER_BTN; +#endif +#endif if (!moduleConfig.canned_message.updown1_enabled) { // Input device is disabled. @@ -46,4 +54,4 @@ void UpDownInterruptImpl1::handleIntUp() void UpDownInterruptImpl1::handleIntPressed() { upDownInterruptImpl1->intPressHandler(); -} \ No newline at end of file +} diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index d744ee2ca..510fb1e31 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -7,6 +7,8 @@ #include "TDeckProKeyboard.h" #elif defined(T_LORA_PAGER) #include "TLoraPagerKeyboard.h" +#elif defined(M5STACK_CARDPUTER_ADV) +#include "CardputerKeyboard.h" #elif defined(HACKADAY_COMMUNICATOR) #include "HackadayCommunicatorKeyboard.h" #else @@ -22,6 +24,8 @@ KbI2cBase::KbI2cBase(const char *name) TCAKeyboard(*(new TDeckProKeyboard())) #elif defined(T_LORA_PAGER) TCAKeyboard(*(new TLoraPagerKeyboard())) +#elif defined(M5STACK_CARDPUTER_ADV) + TCAKeyboard(*(new CardputerKeyboard())) #elif defined(HACKADAY_COMMUNICATOR) TCAKeyboard(*(new HackadayCommunicatorKeyboard())) #else @@ -487,7 +491,7 @@ int32_t KbI2cBase::runOnce() e.kbchar = 0; break; case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) - // toggle moddifiers button. + // toggle modifiers button. is_sym = !is_sym; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the diff --git a/src/main.cpp b/src/main.cpp index 9b26b7799..9b83602d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,13 +7,14 @@ #include "NodeDB.h" #include "PowerFSM.h" #include "PowerMon.h" +#include "RadioLibInterface.h" #include "ReliableRouter.h" +#include "TransmitHistory.h" #include "airtime.h" #include "buzz.h" #include "power/PowerHAL.h" #include "FSCommon.h" -#include "Led.h" #include "RTC.h" #include "SPILock.h" #include "Throttle.h" @@ -29,7 +30,6 @@ #include #endif #include "detect/einkScan.h" -#include "graphics/RAKled.h" #include "graphics/Screen.h" #include "main.h" #include "mesh/generated/meshtastic/config.pb.h" @@ -120,35 +120,10 @@ void printPartitionTable() #endif // DEBUG_PARTITION_TABLE #endif // ARCH_ESP32 -#if HAS_BUTTON || defined(ARCH_PORTDUINO) -#include "input/ButtonThread.h" - -#if defined(BUTTON_PIN_TOUCH) -ButtonThread *TouchButtonThread = nullptr; -#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) -static bool touchBacklightWasOn = false; -static bool touchBacklightActive = false; -#endif -#endif - -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) -ButtonThread *UserButtonThread = nullptr; -#endif - -#if defined(ALT_BUTTON_PIN) -ButtonThread *BackButtonThread = nullptr; -#endif - -#if defined(CANCEL_BUTTON_PIN) -ButtonThread *CancelButtonThread = nullptr; -#endif - -#endif - #include "AmbientLightingThread.h" #include "PowerFSMThread.h" -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" AccelerometerThread *accelerometerThread = nullptr; #endif @@ -267,33 +242,12 @@ const char *getDeviceName() return name; } -// TODO remove from main.cpp -static int32_t ledBlinker() -{ - // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if - // config.device.led_heartbeat_disabled is changed - if (config.device.led_heartbeat_disabled) - return 1000; - - static bool ledOn; - ledOn ^= 1; - - ledBlink.set(ledOn); - - // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that - return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); -} - uint32_t timeLastPowered = 0; -static Periodic *ledPeriodic; static OSThread *powerFSMthread; -static OSThread *ambientLightingThread; +OSThread *ambientLightingThread; -RadioInterface *rIf = NULL; -#ifdef ARCH_PORTDUINO RadioLibHal *RadioLibHAL = NULL; -#endif /** * Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep. @@ -324,21 +278,16 @@ void earlyInitVariant() {} // blink user led in 3 flashes sequence to indicate what is happening void waitUntilPowerLevelSafe() { - -#ifdef LED_PIN - pinMode(LED_PIN, OUTPUT); -#endif - while (powerHAL_isPowerLevelSafe() == false) { -#ifdef LED_PIN +#ifdef LED_POWER // 3x: blink for 300 ms, pause for 300 ms for (int i = 0; i < 3; i++) { - digitalWrite(LED_PIN, LED_STATE_ON); + digitalWrite(LED_POWER, LED_STATE_ON); delay(300); - digitalWrite(LED_PIN, LED_STATE_OFF); + digitalWrite(LED_POWER, LED_STATE_OFF); delay(300); } #endif @@ -362,6 +311,11 @@ void setup() // initialize power HAL layer as early as possible powerHAL_init(); +#ifdef LED_POWER + pinMode(LED_POWER, OUTPUT); + digitalWrite(LED_POWER, LED_STATE_ON); +#endif + // prevent booting if device is in power failure mode // boot sequence will follow when battery level raises to safe mode waitUntilPowerLevelSafe(); @@ -374,14 +328,9 @@ void setup() digitalWrite(PIN_POWER_EN, HIGH); #endif -#ifdef LED_POWER - pinMode(LED_POWER, OUTPUT); - digitalWrite(LED_POWER, LED_STATE_ON); -#endif - -#ifdef USER_LED - pinMode(USER_LED, OUTPUT); - digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); +#ifdef LED_NOTIFICATION + pinMode(LED_NOTIFICATION, OUTPUT); + digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON); #endif #ifdef WIFI_LED @@ -391,17 +340,14 @@ void setup() #ifdef BLE_LED pinMode(BLE_LED, OUTPUT); -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif + digitalWrite(BLE_LED, LED_STATE_OFF); #endif concurrency::hasBeenSetup = true; - +#if HAS_SCREEN meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; +#endif OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; #ifdef USE_SEGGER @@ -443,8 +389,8 @@ void setup() #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #ifndef SENSECAP_INDICATOR - // use PSRAM for malloc calls > 256 bytes - heap_caps_malloc_extmem_enable(256); + // use PSRAM for malloc calls > 2048 bytes + heap_caps_malloc_extmem_enable(2048); #endif #endif @@ -509,42 +455,10 @@ void setup() LOG_INFO("Wait for peripherals to stabilize"); delay(PERIPHERAL_WARMUP_MS); #endif - -#ifdef BUTTON_PIN -#ifdef ARCH_ESP32 - -#if ESP_ARDUINO_VERSION_MAJOR >= 3 -#ifdef BUTTON_NEED_PULLUP - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT_PULLUP); -#else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN -#endif -#else - pinMode(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, INPUT); // default to BUTTON_PIN -#ifdef BUTTON_NEED_PULLUP - gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); - delay(10); -#endif -#ifdef BUTTON_NEED_PULLUP2 - gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); - delay(10); -#endif -#endif -#endif -#endif - initSPI(); OSThread::setup(); - // TODO make this ifdef based on defined pins and move from main.cpp -#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) - // The ThinkNodes have their own blink logic - // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); -#else - ledPeriodic = new Periodic("Blink", ledBlinker); -#endif - fsInit(); #if !MESHTASTIC_EXCLUDE_I2C @@ -650,6 +564,7 @@ void setup() } #endif +#if HAS_SCREEN auto screenInfo = i2cScanner->firstScreen(); screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; @@ -667,6 +582,7 @@ void setup() screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; } } +#endif #define UPDATE_FROM_SCANNER(FIND_FN) #if defined(USE_VIRTUAL_KEYBOARD) @@ -741,7 +657,7 @@ void setup() } #endif -#if !defined(ARCH_STM32WL) +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER auto acc_info = i2cScanner->firstAccelerometer(); accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; LOG_DEBUG("acc_info = %i", acc_info.type); @@ -761,21 +677,12 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); - #endif #ifdef HAS_SDCARD setupSDCard(); #endif - // LED init - -#ifdef LED_PIN - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now -#endif - // Hello printInfo(); #ifdef BUILD_EPOCH @@ -797,6 +704,9 @@ void setup() // We do this as early as possible because this loads preferences from flash // but we need to do this after main cpu init (esp32setup), because we need the random seed set nodeDB = new NodeDB; + + // Initialize transmit history to persist broadcast throttle timers across reboots + TransmitHistory::getInstance()->loadFromDisk(); #if HAS_TFT if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { tftSetup(); @@ -813,21 +723,21 @@ void setup() else playStartMelody(); - // fixed screen override? - if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) - screen_model = config.display.oled; - +#if HAS_SCREEN + // fixed screen override? #if defined(USE_SH1107) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 screen_geometry = GEOMETRY_128_128; -#endif - -#if defined(USE_SH1107_128_64) +#elif defined(USE_SH1107_128_64) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64 +#else + if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) + screen_model = config.display.oled; +#endif #endif #if !MESHTASTIC_EXCLUDE_I2C -#if !defined(ARCH_STM32WL) +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER if (acc_info.type != ScanI2C::DeviceType::NONE) { accelerometerThread = new AccelerometerThread(acc_info.type); } @@ -883,7 +793,7 @@ void setup() SPI.begin(); #endif #else -// ESP32 + // ESP32 #if defined(HW_SPI1_DEVICE) SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); @@ -978,6 +888,13 @@ void setup() service = new MeshService(); service->init(); + // Set osk_found for trackball/encoder devices BEFORE setupModules so CannedMessageModule can detect it +#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) +#ifndef HAS_PHYSICAL_KEYBOARD + osk_found = true; +#endif +#endif + // Now that the mesh service is created, create any modules setupModules(); @@ -999,180 +916,9 @@ void setup() nodeDB->hasWarned = true; } #endif - -// buttons are now inputBroker, so have to come after setupModules -#if HAS_BUTTON - int pullup_sense = 0; -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did -#ifdef BUTTON_SENSE_TYPE - pullup_sense = BUTTON_SENSE_TYPE; -#else - pullup_sense = INPUT_PULLUP_SENSE; -#endif -#endif -#if defined(ARCH_PORTDUINO) - - if (portduino_config.userButtonPin.enabled) { - - LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig config; - config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; - config.activeLow = true; - config.activePullup = true; - config.pullupSense = INPUT_PULLUP; - config.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - config.singlePress = INPUT_BROKER_USER_PRESS; - config.longPress = INPUT_BROKER_SELECT; - UserButtonThread->initButton(config); - } - } -#endif - -#ifdef BUTTON_PIN_TOUCH - TouchButtonThread = new ButtonThread("BackButton"); - ButtonConfig touchConfig; - touchConfig.pinNumber = BUTTON_PIN_TOUCH; - touchConfig.activeLow = true; - touchConfig.activePullup = true; - touchConfig.pullupSense = pullup_sense; - touchConfig.intRoutine = []() { - TouchButtonThread->userButton.tick(); - TouchButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - touchConfig.singlePress = INPUT_BROKER_NONE; - touchConfig.longPress = INPUT_BROKER_BACK; -#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) - // On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds - touchConfig.longPress = INPUT_BROKER_NONE; - touchConfig.suppressLeadUpSound = true; - touchConfig.onPress = []() { - touchBacklightWasOn = uiconfig.screen_brightness == 1; - if (!touchBacklightWasOn) { - digitalWrite(PIN_EINK_EN, HIGH); - } - touchBacklightActive = true; - }; - touchConfig.onRelease = []() { - if (touchBacklightActive && !touchBacklightWasOn) { - digitalWrite(PIN_EINK_EN, LOW); - } - touchBacklightActive = false; - }; -#endif - TouchButtonThread->initButton(touchConfig); -#endif - -#if defined(CANCEL_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - CancelButtonThread = new ButtonThread("CancelButton"); - ButtonConfig cancelConfig; - cancelConfig.pinNumber = CANCEL_BUTTON_PIN; - cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; - cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; - cancelConfig.pullupSense = pullup_sense; - cancelConfig.intRoutine = []() { - CancelButtonThread->userButton.tick(); - CancelButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - cancelConfig.singlePress = INPUT_BROKER_CANCEL; - cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; - cancelConfig.longPressTime = 4000; - CancelButtonThread->initButton(cancelConfig); -#endif - -#if defined(ALT_BUTTON_PIN) - // Buttons. Moved here cause we need NodeDB to be initialized - BackButtonThread = new ButtonThread("BackButton"); - ButtonConfig backConfig; - backConfig.pinNumber = ALT_BUTTON_PIN; - backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; - backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; - backConfig.pullupSense = pullup_sense; - backConfig.intRoutine = []() { - BackButtonThread->userButton.tick(); - BackButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - backConfig.singlePress = INPUT_BROKER_ALT_PRESS; - backConfig.longPress = INPUT_BROKER_ALT_LONG; - backConfig.longPressTime = 500; - BackButtonThread->initButton(backConfig); -#endif - -#if defined(BUTTON_PIN) -#if defined(USERPREFS_BUTTON_PIN) - int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; -#else - int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; -#endif -#ifndef BUTTON_ACTIVE_LOW -#define BUTTON_ACTIVE_LOW true -#endif -#ifndef BUTTON_ACTIVE_PULLUP -#define BUTTON_ACTIVE_PULLUP true -#endif - - // Buttons. Moved here cause we need NodeDB to be initialized - // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP - UserButtonThread = new ButtonThread("UserButton"); - if (screen) { - ButtonConfig userConfig; - userConfig.pinNumber = (uint8_t)_pinNum; - userConfig.activeLow = BUTTON_ACTIVE_LOW; - userConfig.activePullup = BUTTON_ACTIVE_PULLUP; - userConfig.pullupSense = pullup_sense; - userConfig.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfig.singlePress = INPUT_BROKER_USER_PRESS; - userConfig.longPress = INPUT_BROKER_SELECT; - userConfig.longPressTime = 500; - userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; - UserButtonThread->initButton(userConfig); - } else { - ButtonConfig userConfigNoScreen; - userConfigNoScreen.pinNumber = (uint8_t)_pinNum; - userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; - userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; - userConfigNoScreen.pullupSense = pullup_sense; - userConfigNoScreen.intRoutine = []() { - UserButtonThread->userButton.tick(); - UserButtonThread->setIntervalFromNow(0); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }; - userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; - userConfigNoScreen.longPress = INPUT_BROKER_NONE; - userConfigNoScreen.longPressTime = 500; - userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN; - userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; - userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; - UserButtonThread->initButton(userConfigNoScreen); - } -#endif - +#if !MESHTASTIC_EXCLUDE_INPUTBROKER + if (inputBroker) + inputBroker->Init(); #endif #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS @@ -1180,12 +926,6 @@ void setup() setupNicheGraphics(); #endif -#ifdef LED_PIN - // Turn LED off after boot, if heartbeat by config - if (config.device.led_heartbeat_disabled) - digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); -#endif - // Do this after service.init (because that clears error_code) #ifdef HAS_PMU if (!pmu_found) @@ -1211,7 +951,7 @@ void setup() #endif #endif - initLoRa(); + auto rIf = initLoRa(); lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) @@ -1239,12 +979,6 @@ void setup() #endif #endif -#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) -#ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; -#endif -#endif - #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. webServerThread = new WebServerThread(); @@ -1266,12 +1000,12 @@ void setup() if (!rIf) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO); else { - router->addInterface(rIf); - // Log bit rate to debug output LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) / (float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) * 1000); + + router->addInterface(std::move(rIf)); } // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values @@ -1388,6 +1122,21 @@ void loop() #endif power->powerCommandsCheck(); + if (RadioLibInterface::instance != nullptr) { + static uint32_t lastRadioMissedIrqPoll; + if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) { + lastRadioMissedIrqPoll = millis(); + RadioLibInterface::instance->pollMissedIrqs(); + } + + // Periodic AGC reset — warm sleep + recalibrate to prevent stuck AGC gain + static uint32_t lastAgcReset; + if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) { + lastAgcReset = millis(); + RadioLibInterface::instance->resetAGC(); + } + } + #ifdef DEBUG_STACK static uint32_t lastPrint = 0; if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { @@ -1407,10 +1156,7 @@ void loop() } if (portduino_status.LoRa_in_error && rebootAtMsec == 0) { LOG_ERROR("LoRa in error detected, attempting to recover"); - if (rIf != nullptr) { - delete rIf; - rIf = nullptr; - } + router->addInterface(nullptr); if (portduino_config.lora_spi_dev == "ch341") { if (ch341Hal != nullptr) { delete ch341Hal; @@ -1426,8 +1172,9 @@ void loop() exit(EXIT_FAILURE); } } - if (initLoRa()) { - router->addInterface(rIf); + auto rIf = initLoRa(); + if (rIf) { + router->addInterface(std::move(rIf)); portduino_status.LoRa_in_error = false; } else { LOG_WARN("Reconfigure failed, rebooting"); diff --git a/src/main.h b/src/main.h index 91e27951f..56f048134 100644 --- a/src/main.h +++ b/src/main.h @@ -65,7 +65,7 @@ extern UdpMulticastHandler *udpHandler; // Global Screen singleton. extern graphics::Screen *screen; -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" extern AccelerometerThread *accelerometerThread; #endif diff --git a/src/memGet.cpp b/src/memGet.cpp index 14e614014..42a3430f6 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -10,8 +10,26 @@ #include "memGet.h" #include "configuration.h" -#ifdef ARCH_STM32WL +#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) #include +#include // 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; diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4dcd94e3b..1583567fe 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -22,6 +22,8 @@ const char *Channels::serialChannel = "serial"; const char *Channels::mqttChannel = "mqtt"; #endif +meshtastic_Channel dummyChannel = {.index = -1}; + uint8_t xorHash(const uint8_t *p, size_t len) { uint8_t code = 0; @@ -309,13 +311,7 @@ meshtastic_Channel &Channels::getByIndex(ChannelIndex chIndex) return *ch; } else { LOG_ERROR("Invalid channel index %d > %d, malformed packet received?", chIndex, channelFile.channels_count); - - static meshtastic_Channel *ch = (meshtastic_Channel *)malloc(sizeof(meshtastic_Channel)); - memset(ch, 0, sizeof(meshtastic_Channel)); - // ch.index -1 means we don't know the channel locally and need to look it up by settings.name - // not sure this is handled right everywhere - ch->index = -1; - return *ch; + return dummyChannel; } } diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index 0f4d64113..daa7a3a75 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -1,8 +1,10 @@ #include "CryptoEngine.h" // #include "NodeDB.h" #include "architecture.h" +#include #if !(MESHTASTIC_EXCLUDE_PKI) +#include "HardwareRNG.h" #include "NodeDB.h" #include "aes-ccm.h" #include "meshUtils.h" @@ -25,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); } @@ -60,6 +71,33 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey) } return true; } + +bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user) +{ + if (user.is_licensed) { + return false; + } + + bool keygenSuccess = false; + if (security.private_key.size == 32) { + if (regeneratePublicKey(security.public_key.bytes, security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + LOG_INFO("Generate new PKI keys"); + generateKeyPair(security.public_key.bytes, security.private_key.bytes); + keygenSuccess = true; + } + + if (keygenSuccess) { + security.public_key.size = 32; + security.private_key.size = 32; + user.public_key.size = 32; + memcpy(user.public_key.bytes, security.public_key.bytes, 32); + } + + return keygenSuccess; +} #endif /** @@ -169,10 +207,9 @@ void CryptoEngine::hash(uint8_t *bytes, size_t numBytes) void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len) { - delete aes; aes = nullptr; if (key_len != 0) { - aes = new AESSmall256(); + aes = std::unique_ptr(new AESSmall256()); aes->setKey(key_bytes, key_len); } } @@ -231,12 +268,11 @@ void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes // Generic implementation of AES-CTR encryption. void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) { - delete ctr; - ctr = nullptr; + std::unique_ptr ctr; if (_key.length == 16) - ctr = new CTR(); + ctr = std::unique_ptr(new CTR()); else - ctr = new CTR(); + ctr = std::unique_ptr(new CTR()); ctr->setKey(_key.bytes, _key.length); static uint8_t scratch[MAX_BLOCKSIZE]; memcpy(scratch, bytes, numBytes); diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h index 7689006ab..f40400331 100644 --- a/src/mesh/CryptoEngine.h +++ b/src/mesh/CryptoEngine.h @@ -5,6 +5,7 @@ #include "configuration.h" #include "mesh-pb-constants.h" #include +#include extern concurrency::Lock *cryptLock; @@ -35,6 +36,7 @@ class CryptoEngine #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey); virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey); + virtual bool ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user); #endif void setDHPrivateKey(uint8_t *_private_key); @@ -48,7 +50,7 @@ class CryptoEngine virtual void aesSetKey(const uint8_t *key, size_t key_len); virtual void aesEncrypt(uint8_t *in, uint8_t *out); - AESSmall256 *aes = NULL; + std::unique_ptr aes = nullptr; #endif @@ -77,7 +79,6 @@ class CryptoEngine /** Our per packet nonce */ uint8_t nonce[16] = {0}; CryptoKey key = {}; - CTRCommon *ctr = NULL; #if !(MESHTASTIC_EXCLUDE_PKI) uint8_t shared_key[32] = {0}; uint8_t private_key[32] = {0}; diff --git a/src/mesh/Default.cpp b/src/mesh/Default.cpp index 1bd0340f8..3ecd766f1 100644 --- a/src/mesh/Default.cpp +++ b/src/mesh/Default.cpp @@ -38,11 +38,13 @@ uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultVa uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes) { // If we are a router, we don't scale the value. It's already significantly higher. - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) return getConfiguredOrDefaultMs(configured, defaultValue); // Additionally if we're a tracker or sensor, we want priority to send position and telemetry - if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER)) + if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) return getConfiguredOrDefaultMs(configured, defaultValue); return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes); diff --git a/src/mesh/Default.h b/src/mesh/Default.h index e206d8277..59425042e 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -1,5 +1,8 @@ #pragma once +#include #include +#include +#include #include #include #define ONE_DAY 24 * 60 * 60 @@ -10,12 +13,12 @@ #define TEN_SECONDS_MS 10 * 1000 #define MAX_INTERVAL INT32_MAX // FIXME: INT32_MAX to avoid overflow issues with Apple clients but should be UINT32_MAX -#define min_default_telemetry_interval_secs 30 * 60 +#define min_default_telemetry_interval_secs IF_ROUTER(ONE_DAY / 2, 30 * 60) #define default_gps_update_interval IF_ROUTER(ONE_DAY, 2 * 60) #define default_telemetry_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) #define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) #define default_broadcast_smart_minimum_interval_secs 5 * 60 -#define min_default_broadcast_interval_secs 60 * 60 +#define min_default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) #define min_default_broadcast_smart_minimum_interval_secs 5 * 60 #define default_wait_bluetooth_secs IF_ROUTER(1, 60) #define default_sds_secs IF_ROUTER(ONE_DAY, UINT32_MAX) // Default to forever super deep sleep @@ -27,6 +30,11 @@ #define min_node_info_broadcast_secs 60 * 60 // No regular broadcasts of more than once an hour #define min_neighbor_info_broadcast_secs 4 * 60 * 60 #define default_map_publish_interval_secs 60 * 60 + +// Traffic management defaults +#define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells +#define default_traffic_mgmt_position_min_interval_secs (ONE_DAY / 2) // 12 hours between identical positions + #ifdef USERPREFS_RINGTONE_NAG_SECS #define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS #else @@ -42,7 +50,10 @@ #define default_mqtt_tls_enabled false #define IF_ROUTER(routerVal, normalVal) \ - ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) ? (routerVal) : (normalVal)) + ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || \ + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) \ + ? (routerVal) \ + : (normalVal)) class Default { @@ -63,25 +74,39 @@ class Default if (numOnlineNodes <= 40) { return 1.0; } else { - float throttlingFactor = 0.075; - if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW) - throttlingFactor = 0.04; - else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST) - throttlingFactor = 0.02; - else if (config.lora.use_preset && - IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)) - throttlingFactor = 0.01; + // Resolve SF and BW from preset or manual config + // When use_preset is true, config.lora.spread_factor and bandwidth may be 0 + // because applyModemConfig() sets them on RadioInterface, not on config.lora + float bwKHz; + uint8_t sf; + uint8_t cr; + if (config.lora.use_preset) { + modemPresetToParams(config.lora.modem_preset, false, bwKHz, sf, cr); + } else { + sf = config.lora.spread_factor; + bwKHz = bwCodeToKHz(config.lora.bandwidth); + } + + // Guard against invalid values + sf = clampSpreadFactor(sf); + bwKHz = clampBandwidthKHz(bwKHz); + + // throttlingFactor = 2^SF / (BW_in_kHz * scaling_divisor) + // With scaling_divisor=100: + // In SF11 and BW=250khz (longfast), this gives 0.08192 rather than the original 0.075 + // In SF10 and BW=250khz (mediumslow), this gives 0.04096 rather than the original 0.04 + // In SF9 and BW=250khz (mediumfast), this gives 0.02048 rather than the original 0.02 + // In SF7 and BW=250khz (shortfast), this gives 0.00512 rather than the original 0.01 + float throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 100.0f); #if USERPREFS_EVENT_MODE - // If we are in event mode, scale down the throttling factor - throttlingFactor = 0.04; + // If we are in event mode, scale down the throttling factor by 4 + throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 25.0f); #endif // Scaling up traffic based on number of nodes over 40 int nodesOverForty = (numOnlineNodes - 40); - return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default) + return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by throttle factor } } -}; +}; \ No newline at end of file diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 78602a9ec..13f98299f 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -91,10 +91,27 @@ void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p) { if (nodeDB) nodeDB->updateFrom(*p); + #if !MESHTASTIC_EXCLUDE_TRACEROUTE + if (traceRouteModule && p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) { + // If we got a packet that is not decoded, try to decode it so we can check for traceroute. + auto decodedState = perhapsDecode(const_cast(p)); + if (decodedState == DecodeState::DECODE_SUCCESS) { + // parsing was successful, print for debugging + printPacket("reprocessPacket(DUP)", p); + } else { + // Fatal decoding error, we can't do anything with this packet + LOG_WARN( + "FloodingRouter::reprocessPacket: Fatal decode error (state=%d, id=0x%08x, from=%u), can't check for traceroute", + static_cast(decodedState), p->id, getFrom(p)); + return; + } + } + if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) + p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) { traceRouteModule->processUpgradedPacket(*p); + } #endif } diff --git a/src/mesh/HardwareRNG.cpp b/src/mesh/HardwareRNG.cpp new file mode 100644 index 000000000..f5a805487 --- /dev/null +++ b/src/mesh/HardwareRNG.cpp @@ -0,0 +1,160 @@ +#include "HardwareRNG.h" + +#include +#include +#include + +#include "configuration.h" + +#if HAS_RADIO +#include "RadioLibInterface.h" +#endif + +#if defined(ARCH_NRF52) +#include +extern Adafruit_nRFCrypto nRFCrypto; +#elif defined(ARCH_ESP32) +#include +#elif defined(ARCH_RP2040) +#include +#elif defined(ARCH_PORTDUINO) +#include +#include +#include +#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) { + // Intentionally silent: this path runs during portduinoSetup() before the + // console/SerialConsole is initialized, so LOG_* here would dereference a null pointer. + 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(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(&candidate), sizeof(candidate), true)) { + return false; + } + seedOut = candidate; + return true; +} + +} // namespace HardwareRNG diff --git a/src/mesh/HardwareRNG.h b/src/mesh/HardwareRNG.h new file mode 100644 index 000000000..2dacb6c23 --- /dev/null +++ b/src/mesh/HardwareRNG.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +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 diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index a8a2780ed..7a193f7f3 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -71,12 +71,10 @@ template bool LR11x0Interface::init() RadioLibInterface::init(); - limitPower(LR1110_MAX_POWER); - - if ((power > LR1120_MAX_POWER) && - (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range - power = LR1120_MAX_POWER; - preambleLength = 12; // 12 is the default for operation above 2GHz + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + limitPower(LR1120_MAX_POWER); + } else { + limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range } #ifdef LR11X0_RF_SWITCH_SUBGHZ @@ -170,13 +168,19 @@ template bool LR11x0Interface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); + err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); err = lora.setSyncWord(syncWord); assert(err == RADIOLIB_ERR_NONE); + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + limitPower(LR1120_MAX_POWER); + } else { + limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range + } + err = lora.setPreambleLength(preambleLength); assert(err == RADIOLIB_ERR_NONE); @@ -184,14 +188,14 @@ template bool LR11x0Interface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > LR1110_MAX_POWER) // This chip has lower power limits than some - power = LR1110_MAX_POWER; - if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit - power = LR1120_MAX_POWER; - err = lora.setOutputPower(power); assert(err == RADIOLIB_ERR_NONE); + // Apply RX gain mode — valid in STDBY, matches resetAGC() pattern + err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + if (err != RADIOLIB_ERR_NONE) + LOG_WARN("LR11x0 setRxBoostedGainMode %s%d", radioLibErr, err); + startReceive(); // restart receiving return RADIOLIB_ERR_NONE; @@ -263,6 +267,7 @@ template void LR11x0Interface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } @@ -298,6 +303,38 @@ template bool LR11x0Interface::isActivelyReceiving() RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); } +#ifdef LR11X0_AGC_RESET +template void LR11x0Interface::resetAGC() +{ + // Safety: don't reset mid-packet + if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) + return; + + LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)"); + + // 1. Warm sleep — powers down the analog frontend, resetting AGC state + lora.sleep(true, 0); + + // 2. Wake to RC standby for stable calibration + lora.standby(RADIOLIB_LR11X0_STANDBY_RC, true); + + // 3. Calibrate all blocks (PLL, ADC, image, RC oscillators) + // calibrate() is protected on LR11x0, so use raw SPI (same as internal implementation) + uint8_t calData = RADIOLIB_LR11X0_CALIBRATE_ALL; + module.SPIwriteStream(RADIOLIB_LR11X0_CMD_CALIBRATE, &calData, 1, true, true); + + // 4. Re-calibrate image rejection for actual operating frequency + // Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions. + lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f); + + // 5. Re-apply RX boosted gain mode + lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + + // 6. Resume receiving + startReceive(); +} +#endif + template bool LR11x0Interface::sleep() { // \todo Display actual typename of the adapter, not just `LR11x0` diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index 840184bbf..1a6b92520 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -27,6 +27,10 @@ template class LR11x0Interface : public RadioLibInterface bool isIRQPending() override { return lora.getIrqFlags() != 0; } +#ifdef LR11X0_AGC_RESET + void resetAGC() override; +#endif + protected: /** * Specific module instance diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp new file mode 100644 index 000000000..b44c7539b --- /dev/null +++ b/src/mesh/LoRaFEMInterface.cpp @@ -0,0 +1,230 @@ +#if HAS_LORA_FEM +#include "LoRaFEMInterface.h" + +#if defined(ARCH_ESP32) +#include +#include +#endif + +LoRaFEMInterface loraFEMInterface; +void LoRaFEMInterface::init(void) +{ + setLnaCanControl(false); // Default is uncontrollable +#ifdef HELTEC_V4 + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + delay(1); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); + pinMode(LORA_KCT8103L_PA_CSD, INPUT); // detect which FEM is used + delay(1); + if (digitalRead(LORA_KCT8103L_PA_CSD) == HIGH) { + // FEM is KCT8103L + fem_type = KCT8103L_PA; + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); + pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); // LNA enabled by default + setLnaCanControl(true); + } else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) { + // FEM is GC1109 + fem_type = GC1109_PA; + // LORA_GC1109_PA_EN and LORA_KCT8103L_PA_CSD are the same pin and do not need to be repeatedly turned off and held. + // rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); + pinMode(LORA_GC1109_PA_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_EN, HIGH); + pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else { + fem_type = OTHER_FEM_TYPES; + } +#elif defined(USE_GC1109_PA) + fem_type = GC1109_PA; + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); +#if defined(ARCH_ESP32) + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); + rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_TX_EN); +#endif + delay(1); + pinMode(LORA_GC1109_PA_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_EN, HIGH); + pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); +#elif defined(USE_KCT8103L_PA) + fem_type = KCT8103L_PA; + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); +#if defined(ARCH_ESP32) + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); +#endif + delay(1); + pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); // LNA enabled by default + setLnaCanControl(true); +#endif +} + +void LoRaFEMInterface::setSleepModeEnable(void) +{ +#ifdef HELTEC_V4 + if (fem_type == GC1109_PA) { + /* + * Do not switch the power on and off frequently. + * After turning off LORA_GC1109_PA_EN, the power consumption has dropped to the uA level. + */ + digitalWrite(LORA_GC1109_PA_EN, LOW); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else if (fem_type == KCT8103L_PA) { + // shutdown the PA + digitalWrite(LORA_KCT8103L_PA_CSD, LOW); + } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, LOW); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); +#elif defined(USE_KCT8103L_PA) + // shutdown the PA + digitalWrite(LORA_KCT8103L_PA_CSD, LOW); +#endif +} + +void LoRaFEMInterface::setTxModeEnable(void) +{ +#ifdef HELTEC_V4 + if (fem_type == GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) + } else if (fem_type == KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) +#elif defined(USE_KCT8103L_PA) + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); +#endif +} + +void LoRaFEMInterface::setRxModeEnable(void) +{ +#ifdef HELTEC_V4 + if (fem_type == GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else if (fem_type == KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + if (lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } + } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); +#elif defined(USE_KCT8103L_PA) + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + if (lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } +#endif +} + +void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void) +{ + +#ifdef HELTEC_V4 + // Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake. + // Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown), + // then latch with RTC hold so the state survives deep sleep. + digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); + if (fem_type == GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); + } else if (fem_type == KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); + if (lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); + } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_PA_POWER, HIGH); + digitalWrite(LORA_GC1109_PA_EN, HIGH); +#if defined(ARCH_ESP32) + rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); +#endif +#elif defined(USE_KCT8103L_PA) + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + if (lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } +#if defined(ARCH_ESP32) + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); +#endif +#endif +} + +void LoRaFEMInterface::setLNAEnable(bool enabled) +{ + lna_enabled = enabled; +} + +int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower) +{ +#ifdef HELTEC_V4 + const uint16_t gc1109_tx_gain[] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7}; + const uint16_t kct8103l_tx_gain[] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7}; + const uint16_t *tx_gain; + uint16_t tx_gain_num; + if (fem_type == GC1109_PA) { + tx_gain = gc1109_tx_gain; + tx_gain_num = sizeof(gc1109_tx_gain) / sizeof(gc1109_tx_gain[0]); + } else if (fem_type == KCT8103L_PA) { + tx_gain = kct8103l_tx_gain; + tx_gain_num = sizeof(kct8103l_tx_gain) / sizeof(kct8103l_tx_gain[0]); + } else { + return loraOutputPower; + } +#else +#ifdef ARCH_PORTDUINO + const uint16_t *tx_gain = portduino_config.tx_gain_lora; + uint16_t tx_gain_num = portduino_config.num_pa_points; +#else + const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; + uint16_t tx_gain_num = NUM_PA_POINTS; +#endif +#endif + for (int radio_dbm = 0; radio_dbm < tx_gain_num; radio_dbm++) { + if (((radio_dbm + tx_gain[radio_dbm]) > loraOutputPower) || + ((radio_dbm == (tx_gain_num - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= loraOutputPower))) { + // we've exceeded the power limit, or hit the max we can do + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", loraOutputPower, tx_gain[radio_dbm]); + loraOutputPower -= tx_gain[radio_dbm]; + break; + } + } + return loraOutputPower; +} + +#endif \ No newline at end of file diff --git a/src/mesh/LoRaFEMInterface.h b/src/mesh/LoRaFEMInterface.h new file mode 100644 index 000000000..14220c6e3 --- /dev/null +++ b/src/mesh/LoRaFEMInterface.h @@ -0,0 +1,30 @@ +#pragma once +#if HAS_LORA_FEM +#include "configuration.h" +#include + +typedef enum { GC1109_PA, KCT8103L_PA, OTHER_FEM_TYPES } LoRaFEMType; + +class LoRaFEMInterface +{ + public: + LoRaFEMInterface() : fem_type(OTHER_FEM_TYPES) {} + virtual ~LoRaFEMInterface() {} + void init(void); + void setSleepModeEnable(void); + void setTxModeEnable(void); + void setRxModeEnable(void); + void setRxModeEnableWhenMCUSleep(void); + void setLNAEnable(bool enabled); + int8_t powerConversion(int8_t loraOutputPower); + bool isLnaCanControl(void) { return lna_can_control; } + void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + + private: + LoRaFEMType fem_type; + bool lna_enabled = true; + bool lna_can_control = false; +}; +extern LoRaFEMInterface loraFEMInterface; + +#endif \ No newline at end of file diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index 63f401d18..9d579d4f1 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -106,6 +106,18 @@ class MeshModule /* We allow modules to ignore a request without sending an error if they have a specific reason for it. */ bool ignoreRequest = false; + /** + * Check if the current request is a multi-hop broadcast. Modules should call this in allocReply() + * and return NULL to prevent reply storms from broadcast requests that have already been relayed. + */ + bool isMultiHopBroadcastRequest() + { + if (currentRequest && isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { + return true; + } + return false; + } + /** If a bound channel name is set, we will only accept received packets that come in on that channel. * A special exception (FIXME, not sure if this is a good idea) - packets that arrive on the local interface * are allowed on any channel (this lets the local user do anything). diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp index cbea85c62..4aad40c69 100644 --- a/src/mesh/MeshPacketQueue.cpp +++ b/src/mesh/MeshPacketQueue.cpp @@ -184,6 +184,29 @@ bool MeshPacketQueue::replaceLowerPriorityPacket(meshtastic_MeshPacket *p) } } + if (backPacket->tx_after) { + // Check if there's a late packet at the queue end + auto now = millis(); + if (backPacket->tx_after < now && (!p->tx_after || backPacket->tx_after > p->tx_after)) { + int32_t dt = (int32_t)(backPacket->tx_after - now); + if (p->tx_after) { + LOG_WARN("Dropping late packet 0x%08x with TX delay %dms to make room in the TX queue for packet 0x%08x with " + "TX delay %ums", + backPacket->id, dt, p->id, p->tx_after - now); + + } else { + LOG_WARN("Dropping late packet 0x%08x with TX delay %dms to make room in the TX queue for packet 0x%08x " + "with no TX delay", + backPacket->id, dt, p->id); + } + queue.pop_back(); + packetPool.release(backPacket); + // Insert the new packet in the correct order + enqueue(p); + return true; + } + } + // If the back packet's priority is not lower, no replacement occurs return false; } \ No newline at end of file diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index bbb0ee00f..089b4b189 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -4,19 +4,54 @@ #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 = + static_cast(0xFF); + +// Region profile: bundles the preset list with regulatory parameters shared across regions +struct RegionProfile { + const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated; first entry is the default + float spacing; // gaps between radio channels + float padding; // padding at each side of the "operating channel" + bool audioPermitted; + bool licensedOnly; // a region profile for licensed operators only + int8_t textThrottle; // throttle for text - future expansion + int8_t positionThrottle; // throttle for location data - future expansion + int8_t telemetryThrottle; // throttle for telemetry - future expansion + uint8_t overrideSlot; // a per-region override slot for if we need to fix it in place +}; + +extern const RegionProfile PROFILE_STD; +extern const RegionProfile PROFILE_EU868; +extern const RegionProfile PROFILE_UNDEF; +// extern const RegionProfile PROFILE_LITE; +// extern const RegionProfile PROFILE_NARROW; +// extern const RegionProfile PROFILE_HAM; // Map from old region names to new region enums struct RegionInfo { meshtastic_Config_LoRaConfig_RegionCode code; float freqStart; float freqEnd; - float dutyCycle; - float spacing; + float dutyCycle; // modified by getEffectiveDutyCycle uint8_t powerLimit; // Or zero for not set - bool audioPermitted; bool freqSwitching; bool wideLora; + const RegionProfile *profile; const char *name; // EU433 etc + + // Preset accessors (delegate through profile) + meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return profile->presets[0]; } + const meshtastic_Config_LoRaConfig_ModemPreset *getAvailablePresets() const { return profile->presets; } + size_t getNumPresets() const + { + size_t n = 0; + while (profile->presets[n] != MODEM_PRESET_END) + n++; + return n; + } }; extern const RegionInfo regions[]; @@ -24,6 +59,49 @@ extern const RegionInfo *myRegion; extern void initRegion(); +// Valid LoRa spread factor range and defaults +constexpr uint8_t LORA_SF_MIN = 5; +constexpr uint8_t LORA_SF_MAX = 12; +constexpr uint8_t LORA_SF_DEFAULT = 11; // LONG_FAST default + +// Valid LoRa coding rate range and default +constexpr uint8_t LORA_CR_MIN = 4; +constexpr uint8_t LORA_CR_MAX = 8; +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 [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; +} + +/// Clamp coding rate to the valid LoRa range [4, 8]. +/// Out-of-range values return LORA_CR_DEFAULT. +static inline uint8_t clampCodingRate(uint8_t cr) +{ + if (cr < LORA_CR_MIN || cr > LORA_CR_MAX) + return LORA_CR_DEFAULT; + return cr; +} + +/// Ensure bandwidth is positive. Non-positive values return LORA_BW_DEFAULT_KHZ. +static inline float clampBandwidthKHz(float bwKHz) +{ + if (bwKHz <= 0.0f) + return LORA_BW_DEFAULT_KHZ; + return bwKHz; +} + static inline float bwCodeToKHz(uint16_t bwCode) { if (bwCode == 31) diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index c1b3839bb..952a6d2be 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -87,7 +87,9 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio - bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; + bool isPreferredRebroadcaster = + IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 5230e5b85..13f948a7b 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -4,6 +4,9 @@ #if !MESHTASTIC_EXCLUDE_TRACEROUTE #include "modules/TraceRouteModule.h" #endif +#if HAS_TRAFFIC_MANAGEMENT +#include "modules/TrafficManagementModule.h" +#endif #include "NodeDB.h" NextHopRouter::NextHopRouter() {} @@ -23,7 +26,7 @@ ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us wasSeenRecently(p); // FIXME, move this to a sniffSent method - p->next_hop = getNextHop(p->to, p->relay_node); // set the next hop + p->next_hop = getNextHop(p->to, p->relay_node).value_or(NO_NEXT_HOP_PREFERENCE); // set the next hop LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is @@ -126,15 +129,28 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast /* Check if we should be rebroadcasting this packet if so, do so. */ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) { - if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) { + // Check if traffic management wants to exhaust this packet's hops + bool exhaustHops = false; +#if HAS_TRAFFIC_MANAGEMENT + if (trafficManagementModule && trafficManagementModule->shouldExhaustHops(*p)) { + exhaustHops = true; + } +#endif + + // Allow rebroadcast if hop_limit > 0 OR if we're exhausting hops (which sets hop_limit = 0 but still needs one relay) + if (!isToUs(p) && !isFromUs(p) && (p->hop_limit > 0 || exhaustHops)) { if (p->id != 0) { if (isRebroadcaster()) { if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it LOG_INFO("Rebroadcast received message coming from %x", p->relay_node); - // Use shared logic to determine if hop_limit should be decremented - if (shouldDecrementHopLimit(p)) { + // If exhausting hops, force hop_limit = 0 regardless of other logic + if (exhaustHops) { + tosend->hop_limit = 0; + LOG_INFO("Traffic management: exhausting hops for 0x%08x, setting hop_limit=0", getFrom(p)); + } else if (shouldDecrementHopLimit(p)) { + // Use shared logic to determine if hop_limit should be decremented tosend->hop_limit--; // bump down the hop count } else { LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit"); @@ -170,10 +186,10 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) * Get the next hop for a destination, given the relay node * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) */ -uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) +std::optional NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) { if (isBroadcast(to)) - return NO_NEXT_HOP_PREFERENCE; + return std::nullopt; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to); if (node && node->next_hop) { @@ -184,7 +200,7 @@ uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) } else LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop); } - return NO_NEXT_HOP_PREFERENCE; + return std::nullopt; } PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key) diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h index c1df3596b..42ef13cd9 100644 --- a/src/mesh/NextHopRouter.h +++ b/src/mesh/NextHopRouter.h @@ -1,6 +1,7 @@ #pragma once #include "FloodingRouter.h" +#include #include /** @@ -146,7 +147,7 @@ class NextHopRouter : public FloodingRouter * Get the next hop for a destination, given the relay node * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) */ - uint8_t getNextHop(NodeNum to, uint8_t relay_node); + std::optional getNextHop(NodeNum to, uint8_t relay_node); /** Check if we should be rebroadcasting this packet if so, do so. * @return true if we did rebroadcast */ diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 1ba67fe93..f0ea3b630 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -143,9 +143,8 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_ digitalWrite(rst, HIGH); delay(10); - uint32_t ID = 0; - ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); - ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice + readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); + uint32_t ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice return ID; } @@ -322,10 +321,10 @@ NodeDB::NodeDB() // Uncomment below to always enable UDP broadcasts // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; - // If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum value - // of 30 minutes or more - if (channels.isDefaultChannel(channels.getPrimaryIndex())) { - LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults"); + // If we are setup to broadcast on any default channel slot (with default frequency slot semantics), + // ensure that the telemetry intervals are coerced to the role-aware minimum value. + if (channels.hasDefaultChannel()) { + LOG_DEBUG("Coerce telemetry to role-aware minimum on defaults"); moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue( moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs); moduleConfig.telemetry.environment_update_interval = Default::getConfiguredOrMinimumValue( @@ -348,7 +347,7 @@ NodeDB::NodeDB() } } if (positionUsesDefaultChannel) { - LOG_DEBUG("Coerce position broadcasts to min of 1 hour and smart broadcast min of 5 minutes on defaults"); + LOG_DEBUG("Coerce position broadcasts to role-aware minimum and smart broadcast min of 5 minutes on defaults"); config.position.position_broadcast_secs = Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, min_default_broadcast_interval_secs); config.position.broadcast_smart_minimum_interval_secs = Default::getConfiguredOrMinimumValue( @@ -569,11 +568,20 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) config.lora.override_duty_cycle = false; config.lora.config_ok_to_mqtt = false; +#if HAS_LORA_FEM + config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED; +#else + config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT; +#endif #if HAS_TFT // For the devices that support MUI, default to that config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; #endif +#if defined(TFT_WIDTH) && defined(TFT_HEIGHT) && (TFT_WIDTH >= 200 || TFT_HEIGHT >= 200) + config.display.enable_message_bubbles = true; +#endif + #ifdef USERPREFS_CONFIG_DEVICE_ROLE // Restrict ROUTER*, LOST AND FOUND roles for security reasons if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, @@ -664,7 +672,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) #endif config.position.broadcast_smart_minimum_distance = 100; config.position.broadcast_smart_minimum_interval_secs = default_broadcast_smart_minimum_interval_secs; - if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && + config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; config.security.serial_enabled = true; config.security.admin_channel_enabled = false; @@ -810,32 +819,28 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.has_store_forward = true; moduleConfig.has_telemetry = true; moduleConfig.has_external_notification = true; -#if defined(PIN_BUZZER) +#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) || defined(PCA_LED_NOTIFICATION) moduleConfig.external_notification.enabled = true; +#endif +#if defined(PIN_BUZZER) moduleConfig.external_notification.output_buzzer = PIN_BUZZER; moduleConfig.external_notification.use_pwm = true; moduleConfig.external_notification.alert_message_buzzer = true; - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #if defined(PIN_VIBRATION) - moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.output_vibra = PIN_VIBRATION; moduleConfig.external_notification.alert_message_vibra = true; moduleConfig.external_notification.output_ms = 500; - moduleConfig.external_notification.nag_timeout = 2; -#endif -#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \ - defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6) - // Default to PIN_LED2 for external notification output (LED color depends on device variant) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output = PIN_LED2; -#if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) - moduleConfig.external_notification.active = false; -#else - moduleConfig.external_notification.active = true; #endif +#if defined(LED_NOTIFICATION) + moduleConfig.external_notification.output = LED_NOTIFICATION; + moduleConfig.external_notification.active = LED_STATE_ON; moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; +#endif +#if defined(PIN_VIBRATION) + moduleConfig.external_notification.nag_timeout = 2; +#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION) moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif @@ -857,15 +862,6 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.output_ms = 100; moduleConfig.external_notification.active = true; #endif -#ifdef ELECROW_ThinkNode_M1 - // Default to Elecrow USER_LED (blue) - moduleConfig.external_notification.enabled = true; - moduleConfig.external_notification.output = USER_LED; - moduleConfig.external_notification.active = true; - moduleConfig.external_notification.alert_message = true; - moduleConfig.external_notification.output_ms = 1000; - moduleConfig.external_notification.nag_timeout = 60; -#endif #ifdef T_LORA_PAGER moduleConfig.canned_message.updown1_enabled = true; moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; @@ -1303,9 +1299,13 @@ void NodeDB::loadFromDisk() // Coerce LoRa config fields derived from presets while bootstrapping. // Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode. if (config.has_lora && config.lora.use_preset) { - RadioInterface::bootstrapLoRaConfigFromPreset(config.lora); + RadioInterface::clampConfigLora(config.lora); } +#if defined(USERPREFS_LORA_TX_DISABLED) && USERPREFS_LORA_TX_DISABLED + config.lora.tx_enabled = false; +#endif + if (backupSecurity.private_key.size > 0) { LOG_DEBUG("Restoring backup of security config"); config.security = backupSecurity; @@ -1419,6 +1419,15 @@ void NodeDB::loadFromDisk() if (portduino_config.has_configDisplayMode) { config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; } + if (portduino_config.has_statusMessage) { + moduleConfig.has_statusmessage = true; + strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(), + sizeof(moduleConfig.statusmessage.node_status)); + moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0'; + } + if (portduino_config.enable_UDP) { + config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; + } #endif } @@ -1559,6 +1568,7 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat) moduleConfig.has_ambient_lighting = true; moduleConfig.has_audio = true; moduleConfig.has_paxcounter = true; + moduleConfig.has_statusmessage = true; success &= saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); @@ -1640,6 +1650,25 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p) return delta; } +HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p) +{ + // Guard against invalid values. + if (p.hop_start < p.hop_limit) + return HopStartStatus::INVALID; + + if (p.hop_start == 0) { + // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a + // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware + // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as + // the bitfield is encrypted under the channel encryption key. + if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield) + return HopStartStatus::VALID; + return HopStartStatus::MISSING_OR_UNKNOWN; + } + + return HopStartStatus::VALID; +} + int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) { // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a @@ -1677,6 +1706,22 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) #include "MeshModule.h" #include "Throttle.h" +static constexpr uint32_t HOPSTART_DROP_LOG_INTERVAL_MS = 15000; + +void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context) +{ + static uint32_t lastLogMs = 0; + if (Throttle::isWithinTimespanMs(lastLogMs, HOPSTART_DROP_LOG_INTERVAL_MS)) { + return; + } + lastLogMs = millis(); + const bool decoded = (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag); + const bool hasBitfield = decoded && p.decoded.has_bitfield; + LOG_DEBUG( + "Drop packet (%s): hop_start invalid/missing (from=0x%x id=%u hop_start=%u hop_limit=%u decoded=%d has_bitfield=%d)", + context ? context : "unknown", p.from, p.id, p.hop_start, p.hop_limit, decoded, hasBitfield); +} + /** Update position info for this node based on received position data */ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src) @@ -1773,7 +1818,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) info->has_device_metrics = false; info->has_position = false; info->user.public_key.size = 0; - info->user.public_key.bytes[0] = 0; + memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes)); } else { /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with * public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM! @@ -2228,8 +2273,8 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { // TODO: After more mainline SD card support } - return success; #endif + return success; } /// Record an error that should be reported via analytics diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index adf2b42ea..f6be963c1 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -114,6 +114,27 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p); /// Returns defaultIfUnknown if the number of hops couldn't be determined. int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1); +enum class HopStartStatus : uint8_t { VALID = 0, MISSING_OR_UNKNOWN, INVALID }; + +/// Classify hop_start validity for forwarding decisions. +HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p); + +inline bool shouldDropPacketForPreHop(const meshtastic_MeshPacket &p) +{ +#if !MESHTASTIC_PREHOP_DROP + (void)p; + return false; +#else + if (isFromUs(&p)) { + return false; // local-originated packets should never be dropped by pre-hop drop policy + } + return classifyHopStart(p) != HopStartStatus::VALID; +#endif +} + +/// Rate-limited debug log when hop_start is invalid/missing and packet is dropped. +void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context); + enum LoadFileResult { // Successfully opened the file LOAD_SUCCESS = 1, diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index b4af707ae..845a936d4 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -90,9 +90,9 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd bool seenRecently = (found != NULL); // If found -> the packet was seen recently // Check for hop_limit upgrade scenario - if (seenRecently && wasUpgraded && found->hop_limit < p->hop_limit) { - LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit, - p->hop_limit); + if (seenRecently && wasUpgraded && getHighestHopLimit(*found) < p->hop_limit) { + LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, + getHighestHopLimit(*found), p->hop_limit); *wasUpgraded = true; } else if (wasUpgraded) { *wasUpgraded = false; // Initialize to false if not an upgrade @@ -439,7 +439,7 @@ void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, cons } // Getters and setters for hop limit fields packed in hop_limit -inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r) +inline uint8_t PacketHistory::getHighestHopLimit(const PacketRecord &r) { return r.hop_limit & HOP_LIMIT_HIGHEST_MASK; } @@ -449,7 +449,7 @@ inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit) r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK); } -inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r) +inline uint8_t PacketHistory::getOurTxHopLimit(const PacketRecord &r) { return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT; } diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 5fbad2dc9..9b6a93280 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -43,9 +43,9 @@ class PacketHistory * @return true if node was indeed a relayer, false if not */ bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); - uint8_t getHighestHopLimit(PacketRecord &r); + uint8_t getHighestHopLimit(const PacketRecord &r); void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit); - uint8_t getOurTxHopLimit(PacketRecord &r); + uint8_t getOurTxHopLimit(const PacketRecord &r); void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit); PacketHistory(const PacketHistory &); // non construction-copyable diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 9050ee89d..7df6720a2 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -122,6 +122,8 @@ void PhoneAPI::close() } packetForPhone = NULL; filesManifest.clear(); + filesManifest.shrink_to_fit(); + lastPortNumToRadio.clear(); fromRadioNum = 0; config_nonce = 0; config_state = 0; @@ -447,6 +449,11 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; break; + case meshtastic_ModuleConfig_traffic_management_tag: + LOG_DEBUG("Send module config: traffic management"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag; + fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management; + break; default: LOG_ERROR("Unknown module config type %d", config_state); } diff --git a/src/mesh/PointerQueue.h b/src/mesh/PointerQueue.h index b45245eb8..972eb65fd 100644 --- a/src/mesh/PointerQueue.h +++ b/src/mesh/PointerQueue.h @@ -17,14 +17,4 @@ template class PointerQueue : public TypedQueue return this->dequeue(&p, maxWait) ? p : nullptr; } - -#ifdef HAS_FREE_RTOS - // returns a ptr or null if the queue was empty - T *dequeuePtrFromISR(BaseType_t *higherPriWoken) - { - T *p; - - return this->dequeueFromISR(&p, higherPriWoken) ? p : nullptr; - } -#endif }; diff --git a/src/mesh/ProtobufModule.h b/src/mesh/ProtobufModule.h index 725477eae..27e653efe 100644 --- a/src/mesh/ProtobufModule.h +++ b/src/mesh/ProtobufModule.h @@ -82,7 +82,6 @@ template class ProtobufModule : protected SinglePortModule // it would be better to update even if the message was destined to others. auto &p = mp.decoded; - LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size); T scratch; T *decoded = NULL; @@ -90,6 +89,8 @@ template class ProtobufModule : protected SinglePortModule memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { decoded = &scratch; + LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, + p.payload.size); } else { LOG_ERROR("Error decoding proto module!"); // if we can't decode it, nobody can process it! diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 0c12401ca..32c92de93 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -240,8 +240,7 @@ bool RF95Interface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > RF95_MAX_POWER) // This chip has lower power limits than some - power = RF95_MAX_POWER; + limitPower(RF95_MAX_POWER); #ifdef USE_RF95_RFO err = lora->setOutputPower(power, true); @@ -301,6 +300,7 @@ void RF95Interface::startReceive() // Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); } bool RF95Interface::isChannelActive() diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 2a9b4e8f3..b7b672d8b 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -16,10 +16,12 @@ #include "configuration.h" #include "detect/LoRaRadioType.h" #include "main.h" +#include "meshUtils.h" // for pow_of_2 #include "sleep.h" #include #include #include +#include #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" @@ -27,20 +29,36 @@ #include "platform/portduino/USBHal.h" #endif -#ifdef ARCH_STM32WL> +#ifdef ARCH_STM32WL #include "STM32WLE5JCInterface.h" #endif -// Calculate 2^n without calling pow() -uint32_t pow_of_2(uint32_t n) -{ - return 1 << n; -} +static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, MODEM_PRESET_END}; -#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \ +static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, MODEM_PRESET_END}; + +static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, + MODEM_PRESET_END}; + +// Region profiles: bundle preset list + regulatory parameters shared across regions +// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle, override slot +const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 0, 0, 0}; +const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 0, 0, 0}; +const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 0, 0, 0}; + +#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr) \ { \ - meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \ - frequency_switching, wide_lora, #name \ + meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \ + wide_lora, &profile_ptr, #name \ } const RegionInfo regions[] = { @@ -48,7 +66,7 @@ const RegionInfo regions[] = { https://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/ */ - RDEF(US, 902.0f, 928.0f, 100, 0, 30, true, false, false), + RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD), /* EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21] @@ -56,8 +74,7 @@ const RegionInfo regions[] = { https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf FIXME: https://github.com/meshtastic/firmware/issues/3371 */ - RDEF(EU_433, 433.0f, 434.0f, 10, 0, 10, true, false, false), - + RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD), /* https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/ https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/ @@ -72,33 +89,33 @@ const RegionInfo regions[] = { AFA) to avoid a duty cycle. (Please refer to line P page 22 of this document.) https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.01.01_60/en_30022002v030101p.pdf */ - RDEF(EU_868, 869.4f, 869.65f, 10, 0, 27, false, false, false), + RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf */ - RDEF(CN, 470.0f, 510.0f, 100, 0, 19, true, false, false), + RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf https://qiita.com/ammo0613/items/d952154f1195b64dc29f */ - RDEF(JP, 920.5f, 923.5f, 100, 0, 13, true, false, false), + RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD), /* https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf Also used in Brazil. */ - RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false), + RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD), /* 433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100 */ - RDEF(ANZ_433, 433.05f, 434.79f, 100, 0, 14, true, false, false), + RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD), /* https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf @@ -106,13 +123,13 @@ const RegionInfo regions[] = { Note: - We do LBT, so 100% is allowed. */ - RDEF(RU, 868.7f, 869.2f, 100, 0, 20, true, false, false), + RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD), /* https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0 https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters */ - RDEF(KR, 920.0f, 923.0f, 100, 0, 23, true, false, false), + RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD), /* Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor. @@ -120,42 +137,40 @@ const RegionInfo regions[] = { https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF https://gazette.nat.gov.tw/egFront/e_detail.do?metaid=147283 */ - RDEF(TW, 920.0f, 925.0f, 100, 0, 27, true, false, false), + RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf */ - RDEF(IN, 865.0f, 867.0f, 100, 0, 30, true, false, false), + RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD), /* https://rrf.rsm.govt.nz/smart-web/smart/page/-smart/domain/licence/LicenceSummary.wdk?id=219752 https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf */ - RDEF(NZ_865, 864.0f, 868.0f, 100, 0, 36, true, false, false), + RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf + https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH + Thailand 920–925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations. */ - RDEF(TH, 920.0f, 925.0f, 100, 0, 16, true, false, false), + RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD), /* 433,05-434,7 Mhz 10 mW - https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf - */ - RDEF(UA_433, 433.0f, 434.7f, 10, 0, 10, true, false, false), - - /* 868,0-868,6 Mhz 25 mW https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf */ - RDEF(UA_868, 868.0f, 868.6f, 1, 0, 14, true, false, false), + RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD), + RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD), /* Malaysia 433 - 435 MHz at 100mW, no restrictions. https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf */ - RDEF(MY_433, 433.0f, 435.0f, 100, 0, 20, true, false, false), + RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD), /* Malaysia @@ -164,14 +179,14 @@ const RegionInfo regions[] = { Frequency hopping is used for 919 - 923 MHz. https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf */ - RDEF(MY_919, 919.0f, 924.0f, 100, 0, 27, true, true, false), + RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD), /* Singapore SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions. https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf */ - RDEF(SG_923, 917.0f, 925.0f, 100, 0, 20, true, false, false), + RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD), /* Philippines @@ -181,8 +196,9 @@ const RegionInfo regions[] = { https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135 */ - RDEF(PH_433, 433.0f, 434.7f, 100, 0, 10, true, false, false), RDEF(PH_868, 868.0f, 869.4f, 100, 0, 14, true, false, false), - RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false), + RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD), + RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD), + RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD), /* Kazakhstan @@ -190,60 +206,57 @@ const RegionInfo regions[] = { 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields https://github.com/meshtastic/firmware/issues/7204 */ - RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), - RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false), + RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD), + RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD), /* Nepal 865 MHz to 868 MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use, specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf */ - RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false), + RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD), /* Brazil 902 - 907.5 MHz , 1W power limit, no duty cycle restrictions https://github.com/meshtastic/firmware/issues/3741 */ - RDEF(BR_902, 902.0f, 907.5f, 100, 0, 30, true, false, false), + RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD), /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ - RDEF(LORA_24, 2400.0f, 2483.5f, 100, 0, 10, true, false, true), + RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD), /* This needs to be last. Same as US. */ - RDEF(UNSET, 902.0f, 928.0f, 100, 0, 30, true, false, false) + RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF) }; const RegionInfo *myRegion; bool RadioInterface::uses_default_frequency_slot = true; +bool RadioInterface::uses_custom_channel_name = false; static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; // Global LoRa radio type LoRaRadioType radioType = NO_RADIO; -extern RadioInterface *rIf; extern RadioLibHal *RadioLibHAL; #if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32) extern SPIClass SPI1; #endif -bool initLoRa() +std::unique_ptr initLoRa() { - if (rIf != nullptr) { - delete rIf; - rIf = nullptr; - } + std::unique_ptr rIf = nullptr; #if ARCH_PORTDUINO - SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); + SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); #else - SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); + SPISettings loraSpiSettings(4000000, MSBFIRST, SPI_MODE0); #endif #ifdef ARCH_PORTDUINO @@ -252,26 +265,26 @@ bool initLoRa() RADIOLIB_PIN_TYPE busy) { switch (portduino_config.lora_module) { case use_rf95: - return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new RF95Interface(hal, cs, irq, rst, busy)); case use_sx1262: - return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new SX1262Interface(hal, cs, irq, rst, busy)); case use_sx1268: - return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new SX1268Interface(hal, cs, irq, rst, busy)); case use_sx1280: - return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new SX1280Interface(hal, cs, irq, rst, busy)); case use_lr1110: - return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new LR1110Interface(hal, cs, irq, rst, busy)); case use_lr1120: - return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new LR1120Interface(hal, cs, irq, rst, busy)); case use_lr1121: - return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new LR1121Interface(hal, cs, irq, rst, busy)); case use_llcc68: - return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new LLCC68Interface(hal, cs, irq, rst, busy)); case use_simradio: - return (RadioInterface *)new SimRadio; + return std::unique_ptr(new SimRadio); default: assert(0); // shouldn't happen - return (RadioInterface *)nullptr; + return std::unique_ptr(nullptr); } }; @@ -284,7 +297,7 @@ bool initLoRa() delete RadioLibHAL; RadioLibHAL = nullptr; } - RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + RadioLibHAL = new LockingArduinoHal(SPI, loraSpiSettings); } rIf = loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, @@ -292,27 +305,28 @@ bool initLoRa() if (!rIf->init()) { LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); - delete rIf; - rIf = NULL; + rIf = nullptr; exit(EXIT_FAILURE); } else { LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); } #elif defined(HW_SPI1_DEVICE) - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); + LockingArduinoHal *loraHal = new LockingArduinoHal(SPI1, loraSpiSettings); + RadioLibHAL = loraHal; #else // HW_SPI1_DEVICE - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + LockingArduinoHal *loraHal = new LockingArduinoHal(SPI, loraSpiSettings); + RadioLibHAL = loraHal; #endif // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) #if defined(USE_STM32WLx) if (!rIf) { - rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + rIf = std::unique_ptr( + new STM32WLE5JCInterface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No STM32WL radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("STM32WL init success"); radioType = STM32WLx_RADIO; @@ -322,11 +336,10 @@ bool initLoRa() #if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); + rIf = std::unique_ptr(new RF95Interface(loraHal, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1)); if (!rIf->init()) { LOG_WARN("No RF95 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("RF95 init success"); radioType = RF95_RADIO; @@ -336,17 +349,17 @@ bool initLoRa() #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + auto sxIf = + std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); #ifdef SX126X_DIO3_TCXO_VOLTAGE sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); #endif if (!sxIf->init()) { LOG_WARN("No SX1262 radio"); - delete sxIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1262 init success"); - rIf = sxIf; + rIf = std::move(sxIf); radioType = SX1262_RADIO; } } @@ -355,26 +368,25 @@ bool initLoRa() #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // try using the specified TCXO voltage - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + auto sxIf = + std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); if (!sxIf->init()) { LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; + rIf = std::move(sxIf); radioType = SX1262_RADIO; } } if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead - rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + rIf = std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); radioType = SX1262_RADIO; @@ -386,25 +398,24 @@ bool initLoRa() #if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // try using the specified TCXO voltage - auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + auto sxIf = + std::unique_ptr(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); if (!sxIf->init()) { LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; + rIf = std::move(sxIf); radioType = SX1268_RADIO; } } #endif if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + rIf = std::unique_ptr(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1268 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1268 init success"); radioType = SX1268_RADIO; @@ -414,11 +425,10 @@ bool initLoRa() #if defined(USE_LLCC68) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + rIf = std::unique_ptr(new LLCC68Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No LLCC68 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("LLCC68 init success"); radioType = LLCC68_RADIO; @@ -428,11 +438,11 @@ bool initLoRa() #if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); + rIf = std::unique_ptr( + new LR1110Interface(loraHal, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1110 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("LR1110 init success"); radioType = LR1110_RADIO; @@ -442,11 +452,11 @@ bool initLoRa() #if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 if (!rIf) { - rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); + rIf = std::unique_ptr( + new LR1120Interface(loraHal, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1120 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("LR1120 init success"); radioType = LR1120_RADIO; @@ -456,11 +466,11 @@ bool initLoRa() #if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 if (!rIf) { - rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); + rIf = std::unique_ptr( + new LR1121Interface(loraHal, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1121 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("LR1121 init success"); radioType = LR1121_RADIO; @@ -470,11 +480,10 @@ bool initLoRa() #if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 if (!rIf) { - rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); + rIf = std::unique_ptr(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1280 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1280 init success"); radioType = SX1280_RADIO; @@ -496,7 +505,7 @@ bool initLoRa() rebootAtMsec = millis() + 5000; } } - return rIf != nullptr; + return rIf; } void initRegion() @@ -514,45 +523,14 @@ void initRegion() myRegion = r; } -void RadioInterface::bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig) +const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code) { - if (!loraConfig.use_preset) { - return; - } - - // Find region info to determine whether "wide" LoRa is permitted (2.4 GHz uses wider bandwidth codes). const RegionInfo *r = regions; - for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != loraConfig.region; r++) + for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != code; r++) ; - - const bool regionWideLora = r->wideLora; - - float bwKHz = 0; - uint8_t sf = 0; - uint8_t cr = 0; - modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr); - - // If selected preset requests a bandwidth larger than the region span, fall back to LONG_FAST. - if (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && (r->freqEnd - r->freqStart) < (bwKHz / 1000.0f)) { - loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; - modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr); - } - - loraConfig.bandwidth = bwKHzToCode(bwKHz); - loraConfig.spread_factor = sf; + return r; } -/** - * ## LoRaWAN for North America - -LoRaWAN defines 64, 125 kHz channels from 902.3 to 914.9 MHz increments. - -The maximum output power for North America is +30 dBM. - -The band is from 902 to 928 MHz. It mentions channel number and its respective channel frequency. All the 13 channels are -separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency. -*/ - uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received) { uint32_t pl = 0; @@ -762,7 +740,7 @@ void RadioInterface::saveFreq(float freq) } /** - * Save our channel for later reuse. + * Save our frequency slot (aka channel) for later reuse. */ void RadioInterface::saveChannelNum(uint32_t channel_num) { @@ -785,113 +763,304 @@ uint32_t RadioInterface::getChannelNum() return savedChannelNum; } +/** + * Send an error-level client notification. Safe to call when service is null (e.g. in tests). + */ +static void sendErrorNotification(const char *msg) +{ + if (!service) + return; + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + if (!cn) + return; + cn->level = meshtastic_LogRecord_Level_ERROR; + snprintf(cn->message, sizeof(cn->message), "%s", msg); + service->sendClientNotification(cn); +} + +/** + * Checks if a region is valid for the current settings. + * Returns false if not compatible. + */ +bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig) +{ + const RegionInfo *newRegion = getRegion(loraConfig.region); + + // If you are not licensed, you can't use ham regions. + if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) { + char err_string[160]; + snprintf(err_string, sizeof(err_string), "Region %s requires licensed mode", newRegion->name); + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + return false; + } + + return true; +} + +/** + * Internal helper: validate or clamp a LoRa config against its region. + * When clamp==false, returns false on first error (pure validation). + * When clamp==true, fixes invalid settings in-place and returns true. + */ +bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp) +{ + char err_string[160]; + float check_bw; + + const RegionInfo *newRegion = getRegion(loraConfig.region); + + const char *presetName = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); + + // Check preset validity (only when use_preset is true) + if (loraConfig.use_preset) { + check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); + + bool preset_valid = false; + for (size_t i = 0; i < newRegion->getNumPresets(); i++) { + if (loraConfig.modem_preset == newRegion->getAvailablePresets()[i]) { + preset_valid = true; + break; + } + } + if (!preset_valid) { + const char *defaultName = DisplayFormatters::getModemPresetDisplayName(newRegion->getDefaultPreset(), false, true); + if (clamp) { + snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s, using %s", presetName, newRegion->name, + defaultName); + } else { + snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s", presetName, newRegion->name); + } + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + + if (clamp) { + loraConfig.modem_preset = newRegion->getDefaultPreset(); + check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); + } else { + return false; + } + } + } else { + check_bw = bwCodeToKHz(loraConfig.bandwidth); + } + + // Calculate width of slots (aka channels) based on bandwidth and any spacing or padding required by the region: + // spacing = gap between slots (0 for continuous spectrum) and at the beginning of the band + // padding = gap at the beginning and end of the slots (0 for no padding) + float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz + uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); + + // Check if the region supports the requested bandwidth + if ((newRegion->freqEnd - newRegion->freqStart) < freqSlotWidth) { + const float regionSpanKHz = (newRegion->freqEnd - newRegion->freqStart) * 1000.0f; + snprintf(err_string, sizeof(err_string), "%s span %.0fkHz < requested %.0fkHz", newRegion->name, regionSpanKHz, check_bw); + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + + if (clamp) { + loraConfig.bandwidth = bwKHzToCode(modemPresetToBwKHz(newRegion->getDefaultPreset(), newRegion->wideLora)); + check_bw = bwCodeToKHz(loraConfig.bandwidth); + + // Recompute slot width and number of slots based on the new bandwidth + freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz + numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); + } else { + return false; + } + } + + const char *channelName = channels.getName(channels.getPrimaryIndex()); + const char *presetNameDisplay = + DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); + uint32_t channelNameHashSlot = hash(channelName) % numFreqSlots; + uint32_t presetNameHashSlot = hash(presetNameDisplay) % numFreqSlots; + + if (loraConfig.override_frequency == 0) { + + // Check if we use the default frequency slot + uses_default_frequency_slot = + (loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default + (newRegion->profile->overrideSlot != 0 && + loraConfig.channel_num == newRegion->profile->overrideSlot) || // user setting matches override + ((newRegion->profile->overrideSlot == 0) && + ((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset hash, no override + + // check if user setting different to preset name + uses_custom_channel_name = (strcmp(channelName, presetNameDisplay) != 0); + + if (loraConfig.channel_num > numFreqSlots) { + snprintf(err_string, sizeof(err_string), "Channel number %u invalid for %s, max is %u", loraConfig.channel_num, + newRegion->name, numFreqSlots); + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + + if (clamp) { + if (uses_custom_channel_name) { // clamp to channel name hash + loraConfig.channel_num = + channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1 + } else if ((loraConfig.use_preset) && (newRegion->profile->overrideSlot != 0)) { // clamp to preset override slot + loraConfig.channel_num = + newRegion->profile->overrideSlot; // use the override slot specified by the region profile + uses_default_frequency_slot = true; + } else if (loraConfig.use_preset) { // clamp to preset slot + loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1 + uses_default_frequency_slot = true; + } else { // if not using preset, and no custom channel name, just clamp to default anyway + uses_default_frequency_slot = true; + }; + } else { + return false; + } + } // end of channel number check + } else { + // if we have a frequency override, we ignore the channel number and just use the override frequency + snprintf(err_string, sizeof(err_string), "Frequency override in place, using %.3f", loraConfig.override_frequency); + } + return true; +} + +bool RadioInterface::validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig) +{ + auto copy = loraConfig; + return checkOrClampConfigLora(copy, false); +} + +void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig) +{ + checkOrClampConfigLora(loraConfig, true); +} + /** * Pull our channel settings etc... from protobufs to the dumb interface settings + * Note: this must be given only settings which have been validated or clamped! */ void RadioInterface::applyModemConfig() { // Set up default configuration // No Sync Words in LORA mode meshtastic_Config_LoRaConfig &loraConfig = config.lora; - bool validConfig = false; // We need to check for a valid configuration - while (!validConfig) { - if (loraConfig.use_preset) { - modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr); - if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { - cr = loraConfig.coding_rate; - LOG_INFO("Using custom Coding Rate %u", cr); - } - } else { - sf = loraConfig.spread_factor; + const RegionInfo *newRegion = getRegion(loraConfig.region); + myRegion = newRegion; + + if (loraConfig.use_preset) { + if (!validateConfigLora(loraConfig)) { + loraConfig.modem_preset = newRegion->getDefaultPreset(); + } + uint8_t newcr; + modemPresetToParams(loraConfig.modem_preset, newRegion->wideLora, bw, sf, newcr); + // If custom CR is being used already, check if the new preset is higher + if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate < newcr) { + cr = newcr; + LOG_INFO("Default Coding Rate is higher than custom setting, using %u", cr); + } + // If the custom CR is higher than the preset, use it + else if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate > newcr) { cr = loraConfig.coding_rate; - bw = bwCodeToKHz(loraConfig.bandwidth); - } - - if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { - const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f; - const float requestedBwKHz = bw; - const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset - const char *presetName = - DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); - - char err_string[160]; - if (isWideRequest) { - snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.", - myRegion->name, presetName); - } else { - snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.", - myRegion->name, regionSpanKHz, requestedBwKHz); - } - LOG_ERROR("%s", err_string); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - snprintf(cn->message, sizeof(cn->message), "%s", err_string); - service->sendClientNotification(cn); - - // Set to default modem preset - loraConfig.use_preset = true; - loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + LOG_INFO("Using custom Coding Rate %u", cr); } else { - validConfig = true; + cr = newcr; } + + } else { // if not using preset, then just use the custom settings + if (validateConfigLora(loraConfig)) { + } else { + LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults", + newRegion->name); + clampConfigLora(loraConfig); + } + bw = bwCodeToKHz(loraConfig.bandwidth); + sf = loraConfig.spread_factor; + cr = loraConfig.coding_rate; } power = loraConfig.tx_power; - if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed)) - power = myRegion->powerLimit; + if ((power == 0) || ((power > newRegion->powerLimit) && !devicestate.owner.is_licensed)) + power = newRegion->powerLimit; if (power == 0) - power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults + power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of newRegion defaults // to 0, currently no region has an actual power limit of 0 [dBm] so we can assume regions which have this // variable set to 0 don't have a valid power limit) // Set final tx_power back onto config loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger - // Calculate the number of channels - uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000))); + uint32_t channel_num; + float freq; - // If user has manually specified a channel num, then use that, otherwise generate one by hashing the name - const char *channelName = channels.getName(channels.getPrimaryIndex()); - // channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1) - uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels; + // Calculate number of frequency slots (aka Channels): + // spacing = gap between channels (0 for continuous spectrum) and at the beginning of the band + // padding = gap at the beginning and end of the channel (0 for no padding) + float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (bw / 1000); // in MHz + uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); - // Check if we use the default frequency slot - RadioInterface::uses_default_frequency_slot = - channel_num == - hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; - - // Old frequency selection formula - // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); - - // New frequency selection formula - float freq = myRegion->freqStart + (bw / 2000) + (channel_num * (bw / 1000)); + // Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one. + // Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to + // (numFreqSlots - 1). + uint32_t presetNameHashSlot = + hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) % numFreqSlots; // override if we have a verbatim frequency if (loraConfig.override_frequency) { freq = loraConfig.override_frequency; channel_num = -1; + uses_default_frequency_slot = false; + } else { + + // If user has not manually specified a frequency slot, or has not specified one that is different than the default or the + // override for the new region, then use the default or override. If the user has not specified one, but has specified a + // custom channel name, then use the hash of that channel name to pick a frequency slot. Note that channel_num is actually + // (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to (numFreqSlots - 1). + // NB: channel_num is also know as frequency slot but it's too late to fix now. + if (uses_default_frequency_slot) { + // if there's an override slot, use that + if (newRegion->profile->overrideSlot != 0) { + channel_num = newRegion->profile->overrideSlot - 1; + } else { + channel_num = presetNameHashSlot; + } + } else { // use the manually defined one + channel_num = loraConfig.channel_num - 1; + } + + // Calculate frequency: freqStart is band edge, add half bandwidth (plus optional padding) to get middle of first channel + // subsequent channels are spaced by freqSlotWidth + freq = newRegion->freqStart + (bw / 2000) + newRegion->profile->padding + (channel_num * freqSlotWidth); // in MHz } saveChannelNum(channel_num); saveFreq(freq + loraConfig.frequency_offset); + const char *channelName = channels.getName(channels.getPrimaryIndex()); + + if (newRegion->wideLora) { // clamp if wide freq range + preambleLength = wideLoraPreambleLengthDefault; // 12 is the default for operation above 2GHz + } else { + preambleLength = + preambleLengthDefault; // 8 is default, but we use longer to increase the amount of sleep time when receiving + } slotTimeMsec = computeSlotTimeMsec(); preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw); LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset); - LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset, + LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", newRegion->name, channelName, loraConfig.modem_preset, channel_num, power); - LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd, - myRegion->freqEnd - myRegion->freqStart); - LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw); + LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd, + newRegion->freqEnd - newRegion->freqStart); + LOG_INFO("numFreqSlots: %d x %.3fkHz", numFreqSlots, bw); + if (newRegion->profile->overrideSlot != 0) { + LOG_INFO("Using region override slot: %d", newRegion->profile->overrideSlot); + } LOG_INFO("channel_num: %d", channel_num + 1); LOG_INFO("frequency: %f", getFreq()); LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec); -} +} // end of applyModemConfig /** Slottime is the time to detect a transmission has started, consisting of: - CAD duration; @@ -928,6 +1097,12 @@ void RadioInterface::limitPower(int8_t loraMaxPower) power = maxPower; } +#if HAS_LORA_FEM + if (!devicestate.owner.is_licensed) { + power = loraFEMInterface.powerConversion(power); + } +#else +// todo:All entries containing "lora fem" are grouped together above. #ifdef ARCH_PORTDUINO size_t num_pa_points = portduino_config.num_pa_points; const uint16_t *tx_gain = portduino_config.tx_gain_lora; @@ -943,9 +1118,9 @@ void RadioInterface::limitPower(int8_t loraMaxPower) } } else if (!devicestate.owner.is_licensed) { // we have an array of PA gain values. Find the highest power setting that works. - for (int radio_dbm = 0; radio_dbm < num_pa_points; radio_dbm++) { + for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) { if (((radio_dbm + tx_gain[radio_dbm]) > power) || - ((radio_dbm == (num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { + ((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { // we've exceeded the power limit, or hit the max we can do LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]); power -= tx_gain[radio_dbm]; @@ -953,7 +1128,7 @@ void RadioInterface::limitPower(int8_t loraMaxPower) } } } - +#endif if (power > loraMaxPower) // Clamp power to maximum defined level power = loraMaxPower; @@ -999,4 +1174,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) sendingPacket = p; return p->encrypted.size + sizeof(PacketHeader); -} +} \ No newline at end of file diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index cb092bc6d..a1c692e24 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -6,6 +6,11 @@ #include "PointerQueue.h" #include "airtime.h" #include "error.h" +#include + +#if HAS_LORA_FEM +#include "LoRaFEMInterface.h" +#endif // Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file. typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig; @@ -87,15 +92,20 @@ class RadioInterface uint8_t sf = 9; uint8_t cr = 5; - const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 - const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 + static constexpr uint8_t NUM_SYM_CAD = + 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 + static constexpr uint8_t NUM_SYM_CAD_24GHZ = + 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 uint32_t slotTimeMsec = computeSlotTimeMsec(); - uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving - uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast - const uint32_t PROCESSING_TIME_MSEC = - 4500; // time to construct, process and construct a packet again (empirically determined) - const uint8_t CWmin = 3; // minimum CWsize - const uint8_t CWmax = 8; // maximum CWsize + uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving + static constexpr uint16_t preambleLengthDefault = + 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving + static constexpr uint16_t wideLoraPreambleLengthDefault = 12; // 12 is default for wide Lora + uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast + static constexpr uint32_t PROCESSING_TIME_MSEC = + 4500; // time to construct, process and construct a packet again (empirically determined) + static constexpr uint8_t CWmin = 3; // minimum CWsize + static constexpr uint8_t CWmax = 8; // maximum CWsize meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending uint32_t lastTxStart = 0L; @@ -122,7 +132,7 @@ class RadioInterface * Coerce LoRa config fields (bandwidth/spread_factor) derived from presets. * This is used during early bootstrapping so UIs that display these fields directly remain consistent. */ - static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); + // static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); // maybe superseded? /** * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) @@ -151,7 +161,7 @@ class RadioInterface virtual ErrorCode send(meshtastic_MeshPacket *p) = 0; /** Return TX queue status */ - virtual meshtastic_QueueStatus getQueueStatus() + [[nodiscard]] virtual meshtastic_QueueStatus getQueueStatus() { meshtastic_QueueStatus qs; qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; @@ -177,22 +187,22 @@ class RadioInterface virtual bool reconfigure(); /** The delay to use for retransmitting dropped packets */ - uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); + [[nodiscard]] uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); /** The delay to use when we want to send something */ - uint32_t getTxDelayMsec(); + [[nodiscard]] uint32_t getTxDelayMsec(); /** The CW to use when calculating SNR_based delays */ - uint8_t getCWsize(float snr); + [[nodiscard]] uint8_t getCWsize(float snr); /** The worst-case SNR_based packet delay */ - uint32_t getTxDelayMsecWeightedWorst(float snr); + [[nodiscard]] uint32_t getTxDelayMsecWeightedWorst(float snr); /** Returns true if we should rebroadcast early like a ROUTER */ - bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); + [[nodiscard]] bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ - uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); + [[nodiscard]] uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); /** If the packet is not already in the late rebroadcast window, move it there */ virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } @@ -210,18 +220,18 @@ class RadioInterface * * @return num msecs for the packet */ - uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); - virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; + [[nodiscard]] uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); + [[nodiscard]] virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; /** * Get the channel we saved. */ - uint32_t getChannelNum(); + [[nodiscard]] uint32_t getChannelNum(); /** * Get the frequency we saved. */ - virtual float getFreq(); + [[nodiscard]] virtual float getFreq(); /// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers virtual bool isIRQPending() { return false; } @@ -229,6 +239,20 @@ class RadioInterface // Whether we use the default frequency slot given our LoRa config (region and modem preset) static bool uses_default_frequency_slot; + // Whether we have a custom channel name + static bool uses_custom_channel_name; + + static bool checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp); + + // Check if a candidate region is compatible and valid. + static bool validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig); + + // Check if a candidate radio configuration is valid. + static bool validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig); + + // Make a candidate radio configuration valid, even if it isn't. + static void clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig); + protected: int8_t power = 17; // Set by applyModemConfig() @@ -241,7 +265,7 @@ class RadioInterface * * Used as the first step of */ - size_t beginSending(meshtastic_MeshPacket *p); + [[nodiscard]] size_t beginSending(meshtastic_MeshPacket *p); /** * Some regulatory regions limit xmit power. @@ -279,7 +303,7 @@ class RadioInterface } }; -bool initLoRa(); +std::unique_ptr initLoRa(); /// Debug printing for packets void printPacket(const char *prefix, const meshtastic_MeshPacket *p); diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 80e51b8bc..7ef707e0d 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -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(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. @@ -453,8 +471,11 @@ void RadioLibInterface::handleReceiveInterrupt() } #endif if (state != RADIOLIB_ERR_NONE) { - LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state, - radioBuffer.header.to, radioBuffer.header.from, radioBuffer.header.flags); + // Log PacketHeader similar to RadioInterface::printPacket so we can try to match RX errors to other packets in the logs. + LOG_ERROR("Ignore received packet due to error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i " + "nextHop=0x%x relay=0x%x)", + state, radioBuffer.header.id, radioBuffer.header.from, radioBuffer.header.to, radioBuffer.header.flags, + iface->getSNR(), lround(iface->getRSSI()), radioBuffer.header.next_hop, radioBuffer.header.relay_node); rxBad++; airTime->logAirtime(RX_ALL_LOG, rxMsec); @@ -518,6 +539,27 @@ void RadioLibInterface::startReceive() powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); } +void RadioLibInterface::pollMissedIrqs() +{ + // RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges. + if (isReceiving) { + checkRxDoneIrqFlag(); + } +} + +void RadioLibInterface::resetAGC() +{ + // Base implementation: no-op. Override in chip-specific subclasses. +} + +void RadioLibInterface::checkRxDoneIrqFlag() +{ + if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) { + LOG_WARN("caught missed RX_DONE"); + notify(ISR_RX, true); + } +} + void RadioLibInterface::configHardwareForSend() { powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn); @@ -563,4 +605,4 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) return res == RADIOLIB_ERR_NONE; } -} \ No newline at end of file +} diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 833c88710..310ca76bb 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -19,6 +19,8 @@ // In addition to the default Rx flags, we need the PREAMBLE_DETECTED flag to detect whether we are actively receiving #define MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS (RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1 << RADIOLIB_IRQ_PREAMBLE_DETECTED)) +#define AGC_RESET_INTERVAL_MS (60 * 1000) // 60 seconds + /** * We need to override the RadioLib ArduinoHal class to add mutex protection for SPI bus access */ @@ -112,6 +114,18 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified */ virtual void enableInterrupt(void (*)()) = 0; + /** + * Poll as a backup to catch missed edge-triggered interrupts. + */ + void pollMissedIrqs(); + + /** + * Reset AGC by power-cycling the analog frontend. + * Subclasses override with chip-specific calibration sequences. + * Safe to call periodically — skips if currently sending or receiving. + */ + virtual void resetAGC(); + /** * Debugging counts */ @@ -158,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 */ @@ -264,4 +284,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified */ bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; + + void checkRxDoneIrqFlag(); }; diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 2b9b17183..42c24c783 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -17,12 +17,6 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p) { if (p->want_ack) { - // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our - // message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop - // counts and we want this message to get through the whole mesh, so use the default. - if (p->hop_limit == 0) { - p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - } DEBUG_HEAP_BEFORE; auto copy = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("ReliableRouter::send", copy); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 1a3270040..836cd1a22 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -11,11 +11,15 @@ #include "mesh-pb-constants.h" #include "meshUtils.h" #include "modules/RoutingModule.h" +#if HAS_TRAFFIC_MANAGEMENT +#include "modules/TrafficManagementModule.h" +#endif #if !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" #endif #include "Default.h" #if ARCH_PORTDUINO +#include "Throttle.h" #include "platform/portduino/PortduinoGlue.h" #endif #if ENABLE_JSON_LOGGING || ARCH_PORTDUINO @@ -94,6 +98,20 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) return true; } +#if HAS_TRAFFIC_MANAGEMENT + // When router_preserve_hops is enabled, preserve hops for decoded packets that are not + // position or telemetry (those have their own exhaust_hop controls). + if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled && + moduleConfig.traffic_management.router_preserve_hops && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + p->decoded.portnum != meshtastic_PortNum_POSITION_APP && p->decoded.portnum != meshtastic_PortNum_TELEMETRY_APP) { + LOG_DEBUG("Router hop preserved: port=%d from=0x%08x (traffic_management)", p->decoded.portnum, getFrom(p)); + if (trafficManagementModule) { + trafficManagementModule->recordRouterHopPreserved(); + } + return false; + } +#endif + // For subsequent hops, check if previous relay is a favorite router // Optimized search for favorite routers with matching last byte // Check ordering optimized for IoT devices (cheapest checks first) @@ -266,6 +284,13 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src) } } + // If someone asks for acks on broadcast, we need the hop limit to be at least one, so that first node that receives our + // message will rebroadcast. But asking for hop_limit 0 in that context means the client app has no preference on hop + // counts and we want this message to get through the whole mesh, so use the default. + if (src == RX_SRC_USER && p->want_ack && p->hop_limit == 0) { + p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); + } + return send(p); } } @@ -525,6 +550,25 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); } else if (portduino_config.JSONFilename != "") { + if (portduino_config.JSONFileRotate != 0) { + static uint32_t fileage = 0; + + if (portduino_config.JSONFileRotate != 0 && + (fileage == 0 || !Throttle::isWithinTimespanMs(fileage, portduino_config.JSONFileRotate * 60 * 1000))) { + time_t timestamp = time(NULL); + struct tm *timeinfo; + char buffer[80]; + timeinfo = localtime(×tamp); + strftime(buffer, 80, "%Y%m%d-%H%M%S", timeinfo); + + std::string datetime(buffer); + if (JSONFile.is_open()) { + JSONFile.close(); + } + JSONFile.open(portduino_config.JSONFilename + "_" + datetime, std::ios::out | std::ios::app); + fileage = millis(); + } + } if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; } @@ -613,15 +657,19 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && // Check for valid keys and single node destination - config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && - // Check for a known public key for the destination - (node->user.public_key.size == 32) && + config.security.private_key.size == 32 && !isBroadcast(p->to) && // Some portnums either make no sense to send with PKC p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP && p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) { LOG_DEBUG("Use PKI!"); if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) return meshtastic_Routing_Error_TOO_LARGE; + // Check for a known public key for the destination + if (node == nullptr || node->user.public_key.size != 32) { + LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to, + p->decoded.portnum); + return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY; + } if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, @@ -754,8 +802,32 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) p_encrypted->pki_encrypted = true; // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && - !isFromUs(p) && mqtt) + !isFromUs(p) && mqtt) { + if (decodedState == DecodeState::DECODE_SUCCESS && p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && + moduleConfig.mqtt.encryption_enabled) { + // For TRACEROUTE_APP packets release the original encrypted packet and encrypt a new from the changed packet + // Only release the original after successful allocation to avoid losing an incomplete but valid packet + auto *p_encrypted_new = packetPool.allocCopy(*p); + if (p_encrypted_new) { + auto encodeResult = perhapsEncode(p_encrypted_new); + if (encodeResult != meshtastic_Routing_Error_NONE) { + // Encryption failed, release the new packet and fall back to sending the original encrypted packet to + // MQTT + LOG_WARN("Encryption of new TR packet failed, sending original TR to MQTT"); + packetPool.release(p_encrypted_new); + p_encrypted_new = nullptr; + } else { + // Successfully re-encrypted, release the original encrypted packet and use the new one for MQTT + packetPool.release(p_encrypted); + p_encrypted = p_encrypted_new; + } + } else { + // Allocation failed, log a warning and fall back to sending the original encrypted packet to MQTT + LOG_WARN("Failed to allocate new encrypted packet for TR, sending original TR to MQTT"); + } + } mqtt->onSend(*p_encrypted, *p, p->channel); + } } #endif } @@ -803,6 +875,12 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) return; } + if (shouldDropPacketForPreHop(*p)) { + logHopStartDrop(*p, "pre-hop drop"); + packetPool.release(p); + return; + } + if (shouldFilterReceived(p)) { LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from); packetPool.release(p); diff --git a/src/mesh/Router.h b/src/mesh/Router.h index dbe6f4f39..0f342d57b 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -8,6 +8,7 @@ #include "PointerQueue.h" #include "RadioInterface.h" #include "concurrency/OSThread.h" +#include /** * A mesh aware router that supports multiple interfaces. @@ -20,7 +21,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory PointerQueue fromRadioQueue; protected: - RadioInterface *iface = NULL; + std::unique_ptr iface = nullptr; public: /** @@ -32,7 +33,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory /** * Currently we only allow one interface, that may change in the future */ - void addInterface(RadioInterface *_iface) { iface = _iface; } + void addInterface(std::unique_ptr _iface) { iface = std::move(_iface); } /** * do idle processing @@ -57,14 +58,14 @@ class Router : protected concurrency::OSThread, protected PacketHistory /** Allocate and return a meshpacket which defaults as send to broadcast from the current node. * The returned packet is guaranteed to have a unique packet ID already assigned */ - meshtastic_MeshPacket *allocForSending(); + [[nodiscard]] meshtastic_MeshPacket *allocForSending(); /** Return Underlying interface's TX queue status */ - meshtastic_QueueStatus getQueueStatus(); + [[nodiscard]] meshtastic_QueueStatus getQueueStatus(); /** * @return our local nodenum */ - NodeNum getNodeNum(); + [[nodiscard]] NodeNum getNodeNum(); /** Wake up the router thread ASAP, because we just queued a message for it. * FIXME, this is kinda a hack because we don't have a nice way yet to say 'wake us because we are 'blocked on this queue' diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 9dfc46bee..bcb08f2c5 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -6,6 +6,10 @@ #ifdef ARCH_PORTDUINO #include "PortduinoGlue.h" #endif +#if defined(ARCH_ESP32) +#include +#include +#endif #include "Throttle.h" @@ -52,22 +56,12 @@ template bool SX126xInterface::init() pinMode(SX126X_POWER_EN, OUTPUT); #endif -#if defined(USE_GC1109_PA) - // GC1109 FEM chip initialization - // See variant.h for full pin mapping and control logic documentation - - // VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on) - pinMode(LORA_PA_POWER, OUTPUT); - digitalWrite(LORA_PA_POWER, HIGH); - - // CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX - pinMode(LORA_PA_EN, OUTPUT); - digitalWrite(LORA_PA_EN, HIGH); - - // CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care) - // Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH - pinMode(LORA_PA_TX_EN, OUTPUT); - digitalWrite(LORA_PA_TX_EN, LOW); // Start in RX-ready state +#if HAS_LORA_FEM + loraFEMInterface.init(); + // Apply saved FEM LNA mode from config + if (loraFEMInterface.isLnaCanControl()) { + loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED); + } #endif #ifdef RF95_FAN_EN @@ -171,6 +165,14 @@ template bool SX126xInterface::init() LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); } + // Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity. + // Sets bit 0 of register 0x8B5. + if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) { + LOG_INFO("Applied SX1262 register 0x8B5 patch for RX improvement"); + } else { + LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement"); + } + #if 0 // Read/write a register we are not using (only used for FSK mode) to test SPI comms uint8_t crcLSB = 0; @@ -243,14 +245,21 @@ template bool SX126xInterface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > SX126X_MAX_POWER) // This chip has lower power limits than some - power = SX126X_MAX_POWER; + limitPower(SX126X_MAX_POWER); + // Make sure we reach the minimum power supported to turn the chip on (-9dBm) + if (power < -9) + power = -9; err = lora.setOutputPower(power); if (err != RADIOLIB_ERR_NONE) LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err); assert(err == RADIOLIB_ERR_NONE); + // Apply RX gain mode — valid in STDBY (datasheet §9.6), matches resetAGC() pattern + err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + if (err != RADIOLIB_ERR_NONE) + LOG_WARN("SX126X setRxBoostedGainMode %s%d", radioLibErr, err); + startReceive(); // restart receiving return RADIOLIB_ERR_NONE; @@ -328,6 +337,7 @@ template void SX126xInterface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } @@ -387,25 +397,77 @@ template bool SX126xInterface::sleep() digitalWrite(SX126X_POWER_EN, LOW); #endif -#if defined(USE_GC1109_PA) - /* - * Do not switch the power on and off frequently. - * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. - * // digitalWrite(LORA_PA_POWER, LOW); - */ - digitalWrite(LORA_PA_EN, LOW); - digitalWrite(LORA_PA_TX_EN, LOW); +#if HAS_LORA_FEM + loraFEMInterface.setSleepModeEnable(); #endif + return true; } +template void SX126xInterface::resetAGC() +{ + // Safety: don't reset mid-packet + if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) + return; + + LOG_DEBUG("SX126x AGC reset: warm sleep + Calibrate(0x7F)"); + + // 1. Warm sleep — powers down the entire analog frontend, resetting AGC state. + // A plain standby→startReceive cycle does NOT reset the AGC. + lora.sleep(true); + + // 2. Wake to RC standby for stable calibration + lora.standby(RADIOLIB_SX126X_STANDBY_RC, true); + + // 3. Calibrate all blocks (ADC, PLL, image, RC oscillators) + uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; + module.SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); + + // 4. Wait for calibration to complete (BUSY pin goes low) + module.hal->delay(5); + uint32_t start = millis(); + while (module.hal->digitalRead(module.getGpio())) { + if (millis() - start > 50) + break; + module.hal->yield(); + } + + if (module.hal->digitalRead(module.getGpio())) { + LOG_WARN("SX126x AGC reset: calibration did not complete within 50ms"); + startReceive(); + return; + } + + // 5. Re-calibrate image rejection for actual operating frequency + // Calibrate(0x7F) defaults to 902-928 MHz which is wrong for other regions. + lora.calibrateImage(getFreq()); + + // Re-apply settings that calibration may have reset + + // DIO2 as RF switch +#ifdef SX126X_DIO2_AS_RF_SWITCH + lora.setDio2AsRfSwitch(true); +#elif defined(ARCH_PORTDUINO) + if (portduino_config.dio2_as_rf_switch) + lora.setDio2AsRfSwitch(true); +#endif + + // RX boosted gain mode + lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + + // 6. Resume receiving + startReceive(); +} + /** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */ template void SX126xInterface::setTransmitEnable(bool txon) { -#if defined(USE_GC1109_PA) - digitalWrite(LORA_PA_POWER, HIGH); // Ensure LDO is on - digitalWrite(LORA_PA_EN, HIGH); // CSD=1: Chip enabled - digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) +#if HAS_LORA_FEM + if (txon) { + loraFEMInterface.setTxModeEnable(); + } else { + loraFEMInterface.setRxModeEnable(); + } #endif } diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index b8f16ac6d..67625e115 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -28,6 +28,8 @@ template class SX126xInterface : public RadioLibInterface bool isIRQPending() override { return lora.getIrqFlags() != 0; } + void resetAGC() override; + void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } protected: diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 3ab63df3d..cb21c0770 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -126,7 +126,7 @@ template bool SX128xInterface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - err = lora.setCodingRate(cr); + err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); @@ -144,8 +144,7 @@ template bool SX128xInterface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > SX128X_MAX_POWER) // This chip has lower power limits than some - power = SX128X_MAX_POWER; + limitPower(SX128X_MAX_POWER); err = lora.setOutputPower(power); if (err != RADIOLIB_ERR_NONE) @@ -271,6 +270,7 @@ template void SX128xInterface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 20026767e..2d9230a21 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -27,7 +27,7 @@ int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) /** * Read any rx chars from the link and call handleRecStream */ -int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +int32_t StreamAPI::readStream(const char *buf, uint16_t bufLen) { if (bufLen < 1) { // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time @@ -56,7 +56,7 @@ void StreamAPI::writeStream() } } -int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) +int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen) { uint16_t index = 0; while (bufLen > index) { // Currently we never want to block diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 4ca2c197f..c724871cb 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -52,13 +52,16 @@ class StreamAPI : public PhoneAPI virtual int32_t runOncePart(); virtual int32_t runOncePart(char *buf, uint16_t bufLen); + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override = 0; + private: /** * Read any rx chars from the link and call handleToRadio */ int32_t readStream(); - int32_t readStream(char *buf, uint16_t bufLen); - int32_t handleRecStream(char *buf, uint16_t bufLen); + int32_t readStream(const char *buf, uint16_t bufLen); + int32_t handleRecStream(const char *buf, uint16_t bufLen); /** * call getFromRadio() and deliver encapsulated packets to the Stream @@ -73,9 +76,6 @@ class StreamAPI : public PhoneAPI virtual void onConnectionChanged(bool connected) override; - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override = 0; - /** * Send the current txBuffer over our stream */ diff --git a/src/mesh/Throttle.cpp b/src/mesh/Throttle.cpp index f278cc843..a4f8347b2 100644 --- a/src/mesh/Throttle.cpp +++ b/src/mesh/Throttle.cpp @@ -31,5 +31,6 @@ bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, vo /// @param timeSpanMs The interval in milliseconds of the timespan bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) { - return (millis() - lastExecutionMs) < timeSpanMs; + uint32_t now = millis(); + return (now - lastExecutionMs) < timeSpanMs; } \ No newline at end of file diff --git a/src/mesh/TransmitHistory.cpp b/src/mesh/TransmitHistory.cpp new file mode 100644 index 000000000..33da7d35c --- /dev/null +++ b/src/mesh/TransmitHistory.cpp @@ -0,0 +1,293 @@ +#include "TransmitHistory.h" +#include "FSCommon.h" +#include "RTC.h" +#include "SPILock.h" +#include + +#ifdef FSCom + +TransmitHistory *transmitHistory = nullptr; + +TransmitHistory *TransmitHistory::getInstance() +{ + if (!transmitHistory) { + transmitHistory = new TransmitHistory(); + } + return transmitHistory; +} + +TransmitHistory::StoredTimestamp TransmitHistory::makeStoredTimestamp(uint32_t seconds, uint8_t flags) +{ + StoredTimestamp stored; + stored.seconds = seconds; + stored.flags = flags; + return stored; +} + +TransmitHistory::StoredTimestamp TransmitHistory::decodeLegacyTimestamp(uint32_t seconds) +{ + const bool isProbablyBootRelative = seconds > 0 && seconds <= LEGACY_BOOT_RELATIVE_MAX_SEC; + return makeStoredTimestamp(seconds, isProbablyBootRelative ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE); +} + +void TransmitHistory::loadFromDisk() +{ + spiLock->lock(); + auto file = FSCom.open(FILENAME, FILE_O_READ); + if (file) { + FileHeader header{}; + if (file.read((uint8_t *)&header, sizeof(header)) == sizeof(header) && header.magic == MAGIC && + (header.version == 1 || header.version == VERSION) && header.count <= MAX_ENTRIES) { + for (uint8_t i = 0; i < header.count; i++) { + if (header.version == 1) { + LegacyEntry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) { + history[entry.key] = decodeLegacyTimestamp(entry.epochSeconds); + } + } else { + Entry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) { + history[entry.key] = makeStoredTimestamp(entry.epochSeconds, entry.flags); + // Do NOT seed lastMillis here. + // + // getLastSentToMeshMillis() reconstructs a millis()-relative value + // from the stored epoch, and Throttle::isWithinTimespanMs() uses + // the same unsigned subtraction pattern. Once getTime() has a valid + // wall-clock epoch comparable to stored values, recent reboots still + // throttle correctly while long power-off periods no longer look like + // "just sent" and incorrectly suppress the first send. + // + // Before RTC/NTP/GPS time is valid, persisted absolute epochs do not + // contribute, but boot-relative entries still suppress near-term reboot + // chatter via a narrow recovery window. + // + // If we seeded lastMillis to millis() here, every loaded entry would + // appear to have been sent at boot time, regardless of the true age + // of the last transmission. That was the regression behind #9901. + } + } + } + LOG_INFO("TransmitHistory: loaded %u entries from disk", header.count); + } else { + LOG_WARN("TransmitHistory: invalid file header, starting fresh"); + } + file.close(); + } else { + LOG_INFO("TransmitHistory: no history file found, starting fresh"); + } + spiLock->unlock(); + dirty = false; +} + +void TransmitHistory::setLastSentToMesh(uint16_t key) +{ + lastMillis[key] = millis(); + uint32_t now = getTime(); + if (now >= 2) { + const uint8_t flags = (getRTCQuality() == RTCQualityNone) ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE; + history[key] = makeStoredTimestamp(now, flags); + dirty = true; + // Don't flush to disk on every transmit — flash has limited write endurance. + // The in-memory lastMillis map handles throttle during normal operation. + // Disk is flushed: before deep sleep (sleep.cpp) and periodically here, + // throttled to at most once per 5 minutes. Always save the first time + // after boot so a crash-reboot loop can't avoid persisting. + if (lastDiskSave == 0 || !Throttle::isWithinTimespanMs(lastDiskSave, SAVE_INTERVAL_MS)) { + if (saveToDisk()) { + lastDiskSave = millis(); + } + } + } +} + +#ifdef PIO_UNIT_TESTING +void TransmitHistory::setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds) +{ + if (epochSeconds > 0) { + history[key] = makeStoredTimestamp(epochSeconds, ENTRY_FLAG_NONE); + dirty = true; + } else { + history.erase(key); + lastMillis.erase(key); + } +} + +void TransmitHistory::setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot) +{ + if (secondsSinceBoot > 0) { + history[key] = makeStoredTimestamp(secondsSinceBoot, ENTRY_FLAG_BOOT_RELATIVE); + dirty = true; + } else { + history.erase(key); + lastMillis.erase(key); + } +} +#endif + +uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const +{ + auto it = history.find(key); + if (it != history.end()) { + return it->second.seconds; + } + return 0; +} + +uint32_t TransmitHistory::getLastSentAbsoluteMillis(uint32_t storedEpoch) const +{ + uint32_t now = getTime(); + if (now < 2) { + return 0; + } + + if (storedEpoch > now) { + return 0; + } + + uint32_t secondsAgo = now - storedEpoch; + uint32_t msAgo = secondsAgo * 1000; + + if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { + return 0; + } + + return millis() - msAgo; +} + +uint32_t TransmitHistory::getLastSentBootRelativeMillis(uint32_t storedSeconds) const +{ + if (getRTCQuality() != RTCQualityNone) { + return 0; + } + + uint32_t now = getTime(); + + if (storedSeconds <= now) { + uint32_t secondsAgo = now - storedSeconds; + if (secondsAgo > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) { + return 0; + } + return millis() - (secondsAgo * 1000); + } + + uint32_t secondsAhead = storedSeconds - now; + if (secondsAhead > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) { + return 0; + } + + return millis(); +} + +uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const +{ + // Prefer runtime millis value (accurate within this boot) + auto mit = lastMillis.find(key); + if (mit != lastMillis.end()) { + return mit->second; + } + + // Fall back to epoch conversion (loaded from disk after reboot) + auto it = history.find(key); + if (it == history.end() || it->second.seconds == 0) { + return 0; // No stored time — module has never sent + } + + // Convert to a millis()-relative timestamp: millis() - msAgo. + // + // The result may wrap if msAgo is larger than the current uptime, and that is + // intentional. Throttle::isWithinTimespanMs() also uses unsigned subtraction, + // so the reconstructed age is preserved across wraparound: + // - recent reboot, 5 min ago -> (millis() - lastMs) == 300000, still throttled + // - long reboot, 30 min ago -> (millis() - lastMs) == 1800000, allowed + if ((it->second.flags & ENTRY_FLAG_BOOT_RELATIVE) != 0) { + return getLastSentBootRelativeMillis(it->second.seconds); + } + + return getLastSentAbsoluteMillis(it->second.seconds); +} + +bool TransmitHistory::saveToDisk() +{ + if (!dirty) { + return true; + } + + spiLock->lock(); + + FSCom.mkdir("/prefs"); + + // Remove old file first + if (FSCom.exists(FILENAME)) { + FSCom.remove(FILENAME); + } + + auto file = FSCom.open(FILENAME, FILE_O_WRITE); + if (file) { + FileHeader header{}; + header.magic = MAGIC; + header.version = VERSION; + header.count = (uint8_t)min((size_t)MAX_ENTRIES, history.size()); + + file.write((uint8_t *)&header, sizeof(header)); + + uint8_t written = 0; + for (const auto &[key, stored] : history) { + if (written >= MAX_ENTRIES) + break; + Entry entry{}; + entry.key = key; + entry.epochSeconds = stored.seconds; + entry.flags = stored.flags; + file.write((uint8_t *)&entry, sizeof(entry)); + written++; + } + file.flush(); + file.close(); + LOG_DEBUG("TransmitHistory: saved %u entries to disk", written); + dirty = false; + spiLock->unlock(); + return true; + } else { + LOG_WARN("TransmitHistory: failed to open file for writing"); + } + + spiLock->unlock(); + return false; +} + +#else +// No filesystem available — provide stub with in-memory tracking +TransmitHistory *transmitHistory = nullptr; + +TransmitHistory *TransmitHistory::getInstance() +{ + if (!transmitHistory) { + transmitHistory = new TransmitHistory(); + } + return transmitHistory; +} + +void TransmitHistory::loadFromDisk() {} + +void TransmitHistory::setLastSentToMesh(uint16_t key) +{ + lastMillis[key] = millis(); +} + +uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const +{ + return 0; +} + +uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const +{ + auto mit = lastMillis.find(key); + return (mit != lastMillis.end()) ? mit->second : 0; +} + +bool TransmitHistory::saveToDisk() +{ + return true; +} + +#endif diff --git a/src/mesh/TransmitHistory.h b/src/mesh/TransmitHistory.h new file mode 100644 index 000000000..1a79048ea --- /dev/null +++ b/src/mesh/TransmitHistory.h @@ -0,0 +1,128 @@ +#pragma once + +#include "configuration.h" +#include +#include + +/** + * TransmitHistory persists the last broadcast transmit time (epoch seconds) per portnum + * to the filesystem so that throttle checks survive reboots/crashes. + * + * On boot, modules call getLastSentToMeshMillis() to recover a millis()-relative timestamp + * from the stored epoch time, which plugs directly into existing throttle logic. + * + * On every broadcast transmit, modules call setLastSentToMesh() which updates the + * in-memory cache and flushes to disk. + * + * Keys are meshtastic_PortNum values (one entry per portnum). + */ + +#include "mesh/generated/meshtastic/portnums.pb.h" + +class TransmitHistory +{ + public: + static TransmitHistory *getInstance(); + + /** + * Load persisted transmit times from disk. Call once during init after filesystem is ready. + */ + void loadFromDisk(); + + /** + * Record that a broadcast was sent for the given key right now. + * Stores epoch seconds and flushes to disk. + */ + void setLastSentToMesh(uint16_t key); + +#ifdef PIO_UNIT_TESTING + /** + * Directly set the stored epoch for a key without touching the runtime lastMillis map. + * Intended for testing purposes: lets tests simulate "the last broadcast happened N + * seconds ago" without needing to fake the system clock. + */ + void setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds); + + /** + * Directly set a boot-relative timestamp (seconds since boot) for testing. + */ + void setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot); +#endif + + /** + * Get the raw persisted timestamp seconds for a given key, or 0 if unknown. + * + * The returned value is an absolute epoch when persisted with valid RTC/NTP/GPS time, + * or boot-relative seconds when ENTRY_FLAG_BOOT_RELATIVE is set. + */ + uint32_t getLastSentToMeshEpoch(uint16_t key) const; + + /** + * Convert a stored epoch timestamp into a millis()-relative timestamp suitable + * for use with Throttle::isWithinTimespanMs(). + * + * Returns 0 if no valid time is stored or if the stored time is in the future + * (which shouldn't happen but guards against clock weirdness). + * + * Example: if the stored epoch is 300 seconds ago, and millis() is currently 10000, + * this returns 10000 - 300000 (wrapped appropriately for uint32_t arithmetic). + */ + uint32_t getLastSentToMeshMillis(uint16_t key) const; + + /** + * Flush dirty entries to disk. Called periodically or on demand. + * + * @return true if the data is persisted (or there was nothing to write), false on write/open failure. + */ + bool saveToDisk(); + + private: + TransmitHistory() = default; + + static constexpr const char *FILENAME = "/prefs/transmit_history.dat"; + static constexpr uint32_t MAGIC = 0x54485354; // "THST" + static constexpr uint8_t VERSION = 2; + static constexpr uint8_t MAX_ENTRIES = 16; + static constexpr uint32_t SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + static constexpr uint32_t BOOT_RELATIVE_RECOVERY_WINDOW_SEC = 2 * 60; + static constexpr uint32_t LEGACY_BOOT_RELATIVE_MAX_SEC = 365UL * 24 * 60 * 60; + + enum EntryFlags : uint8_t { + ENTRY_FLAG_NONE = 0, + ENTRY_FLAG_BOOT_RELATIVE = 0x01, + }; + + struct StoredTimestamp { + uint32_t seconds = 0; + uint8_t flags = ENTRY_FLAG_NONE; + }; + + struct __attribute__((packed)) Entry { + uint16_t key; + uint32_t epochSeconds; + uint8_t flags; + }; + + struct __attribute__((packed)) LegacyEntry { + uint16_t key; + uint32_t epochSeconds; + }; + + struct __attribute__((packed)) FileHeader { + uint32_t magic; + uint8_t version; + uint8_t count; + }; + + uint32_t getLastSentAbsoluteMillis(uint32_t storedEpoch) const; + uint32_t getLastSentBootRelativeMillis(uint32_t storedSeconds) const; + static StoredTimestamp makeStoredTimestamp(uint32_t seconds, uint8_t flags = ENTRY_FLAG_NONE); + static StoredTimestamp decodeLegacyTimestamp(uint32_t seconds); + + std::map history; // key -> persisted transmit time + std::map lastMillis; // key -> millis() value (for runtime throttle) + bool dirty = false; + uint32_t lastDiskSave = 0; // millis() of last disk flush +}; + +extern TransmitHistory *transmitHistory; diff --git a/src/mesh/aes-ccm.cpp b/src/mesh/aes-ccm.cpp index 420d80e9a..5ed7ff928 100644 --- a/src/mesh/aes-ccm.cpp +++ b/src/mesh/aes-ccm.cpp @@ -33,7 +33,7 @@ static int constant_time_compare(const void *a_, const void *b_, size_t len) d |= (a[i] ^ b[i]); } /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ - return (1 & ((d - 1) >> 8)) - 1; + return (1 & (((unsigned int)d - 1) >> 8)) - 1; } static void WPA_PUT_BE16(uint8_t *a, uint16_t val) diff --git a/src/mesh/api/PacketAPI.h b/src/mesh/api/PacketAPI.h index fc08ab209..aececf85e 100644 --- a/src/mesh/api/PacketAPI.h +++ b/src/mesh/api/PacketAPI.h @@ -15,12 +15,12 @@ class PacketAPI : public PhoneAPI, public concurrency::OSThread static PacketAPI *create(PacketServer *_server); virtual ~PacketAPI(){}; virtual int32_t runOnce(); - - protected: - PacketAPI(PacketServer *_server); // Check the current underlying physical queue to see if the client is fetching packets bool checkIsConnected() override; + protected: + explicit PacketAPI(PacketServer *_server); + void onNowHasData(uint32_t fromRadioNum) override {} void onConnectionChanged(bool connected) override {} diff --git a/src/mesh/api/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp index 1a506421c..f3e7854ca 100644 --- a/src/mesh/api/ServerAPI.cpp +++ b/src/mesh/api/ServerAPI.cpp @@ -1,7 +1,10 @@ #include "ServerAPI.h" +#include "Throttle.h" #include "configuration.h" #include +static constexpr uint32_t TCP_IDLE_TIMEOUT_MS = 15 * 60 * 1000UL; + template ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) { @@ -28,9 +31,16 @@ template bool ServerAPI::checkIsConnected() template int32_t ServerAPI::runOnce() { if (client.connected()) { + if (lastContactMsec > 0 && !Throttle::isWithinTimespanMs(lastContactMsec, TCP_IDLE_TIMEOUT_MS)) { + LOG_WARN("TCP connection timeout, no data for %lu ms", (unsigned long)(millis() - lastContactMsec)); + close(); + enabled = false; + return 0; + } return StreamAPI::runOncePart(); } else { LOG_INFO("Client dropped connection, suspend API service"); + close(); enabled = false; // we no longer need to run return 0; } @@ -45,13 +55,18 @@ template void APIServerPort::init() template int32_t APIServerPort::runOnce() { + // Clean up previous connection if its client already disconnected + if (openAPI && !openAPI->checkIsConnected()) { + openAPI.reset(); + } + #ifdef ARCH_ESP32 #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) auto client = U::accept(); #else auto client = U::available(); #endif -#elif defined(ARCH_RP2040) +#elif defined(ARCH_RP2040) || defined(ARCH_NRF52) auto client = U::accept(); #else auto client = U::available(); @@ -70,10 +85,10 @@ template int32_t APIServerPort::runOnce() } #endif LOG_INFO("Force close previous TCP connection"); - delete openAPI; + openAPI.reset(); } - openAPI = new T(client); + openAPI.reset(new T(client)); } #if RAK_4631 diff --git a/src/mesh/api/ServerAPI.h b/src/mesh/api/ServerAPI.h index 111314476..2da77c8e9 100644 --- a/src/mesh/api/ServerAPI.h +++ b/src/mesh/api/ServerAPI.h @@ -1,6 +1,7 @@ #pragma once #include "StreamAPI.h" +#include #define SERVER_API_DEFAULT_PORT 4403 @@ -21,15 +22,15 @@ template class ServerAPI : public StreamAPI, private concurrency::OSTh /// override close to also shutdown the TCP link virtual void close(); + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override; + protected: /// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the board to /// stay in the POWERED state to prevent disabling wifi) virtual void onConnectionChanged(bool connected) override {} virtual int32_t runOnce() override; // Check for dropped client connections - - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override; }; /** @@ -42,7 +43,7 @@ template class APIServerPort : public U, private concurrency: * FIXME: We currently only allow one open TCP connection at a time, because we depend on the loop() call in this class to * delegate to the worker. Once coroutines are implemented we can relax this restriction. */ - T *openAPI = NULL; + std::unique_ptr openAPI; #if defined(RAK_4631) || defined(RAK11310) // Track wait time for RAK13800 Ethernet requests int32_t waitTime = 100; diff --git a/src/mesh/api/ethServerAPI.cpp b/src/mesh/api/ethServerAPI.cpp index 10ff06df2..43ed74cf8 100644 --- a/src/mesh/api/ethServerAPI.cpp +++ b/src/mesh/api/ethServerAPI.cpp @@ -17,6 +17,15 @@ void initApiServer(int port) } } +void deInitApiServer() +{ + if (apiPort) { + LOG_INFO("Deinit API server"); + delete apiPort; + apiPort = nullptr; + } +} + ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) { LOG_INFO("Incoming ethernet connection"); diff --git a/src/mesh/api/ethServerAPI.h b/src/mesh/api/ethServerAPI.h index c616c87be..8f81ee6ff 100644 --- a/src/mesh/api/ethServerAPI.h +++ b/src/mesh/api/ethServerAPI.h @@ -24,4 +24,5 @@ class ethServerPort : public APIServerPort }; void initApiServer(int port = SERVER_API_DEFAULT_PORT); +void deInitApiServer(); #endif diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index a811ec16c..440f7b76a 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -32,6 +32,69 @@ static Periodic *ethEvent; static int32_t reconnectETH() { if (config.network.eth_enabled) { + + // Detect W5100S chip reset by verifying the MAC address register. + // PoE power instability can brownout the W5100S while the MCU keeps running, + // causing all chip registers (MAC, IP, sockets) to revert to defaults. + uint8_t currentMac[6]; + Ethernet.MACAddress(currentMac); + + uint8_t expectedMac[6]; + getMacAddr(expectedMac); + expectedMac[0] &= 0xfe; + + if (memcmp(currentMac, expectedMac, 6) != 0) { + LOG_WARN("W5100S MAC mismatch (chip reset detected), reinitializing Ethernet"); + + syslog.disable(); +#if !MESHTASTIC_EXCLUDE_SOCKETAPI + deInitApiServer(); +#endif +#if HAS_UDP_MULTICAST + if (udpHandler) { + udpHandler->stop(); + } +#endif + + ethStartupComplete = false; +#ifndef DISABLE_NTP + ntp_renew = 0; +#endif + +#ifdef PIN_ETHERNET_RESET + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + delay(100); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + delay(100); +#endif + +#ifdef RAK11310 + ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); + ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); + ETH_SPI_PORT.setRX(PIN_SPI0_MISO); + ETH_SPI_PORT.begin(); +#endif + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + int status = 0; + if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { + status = Ethernet.begin(expectedMac); + } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { + Ethernet.begin(expectedMac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, + config.network.ipv4_config.gateway, config.network.ipv4_config.subnet); + status = 1; + } + + if (status == 0) { + LOG_ERROR("Ethernet re-initialization failed, will retry"); + return 5000; + } + + LOG_INFO("Ethernet reinitialized - IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], + Ethernet.localIP()[2], Ethernet.localIP()[3]); + } + Ethernet.maintain(); if (!ethStartupComplete) { // Start web server @@ -39,7 +102,6 @@ static int32_t reconnectETH() #ifndef DISABLE_NTP LOG_INFO("Start NTP time client"); - timeClient.begin(); timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif @@ -96,6 +158,7 @@ static int32_t reconnectETH() LOG_ERROR("NTP Update failed"); ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes } + timeClient.end(); // W5100S: release UDP socket for other services } #endif diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index 01d3fa910..3dcc241d9 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -36,6 +36,12 @@ PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO) PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO) +PB_BIND(meshtastic_SCD30_config, meshtastic_SCD30_config, AUTO) + + +PB_BIND(meshtastic_SHTXX_config, meshtastic_SHTXX_config, AUTO) + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index f545ed9bf..58e0356ca 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -79,7 +79,11 @@ typedef enum _meshtastic_AdminMessage_ModuleConfigType { /* TODO: REPLACE */ meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12, /* TODO: REPLACE */ - meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13 + meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13, + /* Traffic management module config */ + meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG = 14, + /* TAK module config */ + meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG = 15 } meshtastic_AdminMessage_ModuleConfigType; typedef enum _meshtastic_AdminMessage_BackupLocation { @@ -204,6 +208,33 @@ typedef struct _meshtastic_SEN5X_config { bool set_one_shot_mode; } meshtastic_SEN5X_config; +typedef struct _meshtastic_SCD30_config { + /* Set Automatic self-calibration enabled */ + bool has_set_asc; + bool set_asc; + /* Recalibration target CO2 concentration in ppm (FRC or ASC) */ + bool has_set_target_co2_conc; + uint32_t set_target_co2_conc; + /* Reference temperature in degC */ + bool has_set_temperature; + float set_temperature; + /* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */ + bool has_set_altitude; + uint32_t set_altitude; + /* Power mode for sensor (true for low power, false for normal) */ + bool has_set_measurement_interval; + uint32_t set_measurement_interval; + /* Perform a factory reset of the sensor */ + bool has_soft_reset; + bool soft_reset; +} meshtastic_SCD30_config; + +typedef struct _meshtastic_SHTXX_config { + /* Accuracy mode (0 = low, 1 = medium, 2 = high) */ + bool has_set_accuracy; + uint32_t set_accuracy; +} meshtastic_SHTXX_config; + typedef struct _meshtastic_SensorConfig { /* SCD4X CO2 Sensor configuration */ bool has_scd4x_config; @@ -211,6 +242,12 @@ typedef struct _meshtastic_SensorConfig { /* SEN5X PM Sensor configuration */ bool has_sen5x_config; meshtastic_SEN5X_config sen5x_config; + /* SCD30 CO2 Sensor configuration */ + bool has_scd30_config; + meshtastic_SCD30_config scd30_config; + /* SHTXX temperature and relative humidity sensor configuration */ + bool has_shtxx_config; + meshtastic_SHTXX_config shtxx_config; } meshtastic_SensorConfig; typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t; @@ -369,8 +406,8 @@ extern "C" { #define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1)) #define _meshtastic_AdminMessage_ModuleConfigType_MIN meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG -#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG -#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG+1)) +#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG +#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG+1)) #define _meshtastic_AdminMessage_BackupLocation_MIN meshtastic_AdminMessage_BackupLocation_FLASH #define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD @@ -398,6 +435,8 @@ extern "C" { + + /* Initializer values for message structs */ #define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0} @@ -406,9 +445,11 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} #define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0} #define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} -#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default} +#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default, false, meshtastic_SHTXX_config_init_default} #define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SEN5X_config_init_default {false, 0, false, 0} +#define meshtastic_SCD30_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_SHTXX_config_init_default {false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} #define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}} @@ -416,9 +457,11 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} #define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0} #define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} -#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero} +#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero, false, meshtastic_SHTXX_config_init_zero} #define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SEN5X_config_init_zero {false, 0, false, 0} +#define meshtastic_SCD30_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_SHTXX_config_init_zero {false, 0} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_AdminMessage_InputEvent_event_code_tag 1 @@ -449,8 +492,17 @@ extern "C" { #define meshtastic_SCD4X_config_set_power_mode_tag 7 #define meshtastic_SEN5X_config_set_temperature_tag 1 #define meshtastic_SEN5X_config_set_one_shot_mode_tag 2 +#define meshtastic_SCD30_config_set_asc_tag 1 +#define meshtastic_SCD30_config_set_target_co2_conc_tag 2 +#define meshtastic_SCD30_config_set_temperature_tag 3 +#define meshtastic_SCD30_config_set_altitude_tag 4 +#define meshtastic_SCD30_config_set_measurement_interval_tag 5 +#define meshtastic_SCD30_config_soft_reset_tag 6 +#define meshtastic_SHTXX_config_set_accuracy_tag 1 #define meshtastic_SensorConfig_scd4x_config_tag 1 #define meshtastic_SensorConfig_sen5x_config_tag 2 +#define meshtastic_SensorConfig_scd30_config_tag 3 +#define meshtastic_SensorConfig_shtxx_config_tag 4 #define meshtastic_AdminMessage_get_channel_request_tag 1 #define meshtastic_AdminMessage_get_channel_response_tag 2 #define meshtastic_AdminMessage_get_owner_request_tag 3 @@ -640,11 +692,15 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4) #define meshtastic_SensorConfig_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \ -X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) +X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, shtxx_config, 4) #define meshtastic_SensorConfig_CALLBACK NULL #define meshtastic_SensorConfig_DEFAULT NULL #define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config #define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config +#define meshtastic_SensorConfig_scd30_config_MSGTYPE meshtastic_SCD30_config +#define meshtastic_SensorConfig_shtxx_config_MSGTYPE meshtastic_SHTXX_config #define meshtastic_SCD4X_config_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ @@ -663,6 +719,21 @@ X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2) #define meshtastic_SEN5X_config_CALLBACK NULL #define meshtastic_SEN5X_config_DEFAULT NULL +#define meshtastic_SCD30_config_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ +X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \ +X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \ +X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \ +X(a, STATIC, OPTIONAL, UINT32, set_measurement_interval, 5) \ +X(a, STATIC, OPTIONAL, BOOL, soft_reset, 6) +#define meshtastic_SCD30_config_CALLBACK NULL +#define meshtastic_SCD30_config_DEFAULT NULL + +#define meshtastic_SHTXX_config_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, UINT32, set_accuracy, 1) +#define meshtastic_SHTXX_config_CALLBACK NULL +#define meshtastic_SHTXX_config_DEFAULT NULL + extern const pb_msgdesc_t meshtastic_AdminMessage_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg; @@ -673,6 +744,8 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; extern const pb_msgdesc_t meshtastic_SensorConfig_msg; extern const pb_msgdesc_t meshtastic_SCD4X_config_msg; extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; +extern const pb_msgdesc_t meshtastic_SCD30_config_msg; +extern const pb_msgdesc_t meshtastic_SHTXX_config_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg @@ -685,6 +758,8 @@ extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; #define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg #define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg #define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg +#define meshtastic_SCD30_config_fields &meshtastic_SCD30_config_msg +#define meshtastic_SHTXX_config_fields &meshtastic_SHTXX_config_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size @@ -694,9 +769,11 @@ extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; #define meshtastic_HamParameters_size 31 #define meshtastic_KeyVerificationAdmin_size 25 #define meshtastic_NodeRemoteHardwarePinsResponse_size 496 +#define meshtastic_SCD30_config_size 27 #define meshtastic_SCD4X_config_size 29 #define meshtastic_SEN5X_config_size 7 -#define meshtastic_SensorConfig_size 40 +#define meshtastic_SHTXX_config_size 6 +#define meshtastic_SensorConfig_size 77 #define meshtastic_SharedContact_size 127 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/apponly.pb.h b/src/mesh/generated/meshtastic/apponly.pb.h index f4c33bd79..ce766878b 100644 --- a/src/mesh/generated/meshtastic/apponly.pb.h +++ b/src/mesh/generated/meshtastic/apponly.pb.h @@ -55,7 +55,7 @@ extern const pb_msgdesc_t meshtastic_ChannelSet_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size -#define meshtastic_ChannelSet_size 679 +#define meshtastic_ChannelSet_size 682 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/atak.pb.cpp b/src/mesh/generated/meshtastic/atak.pb.cpp index a0368cf6b..dda9fddaf 100644 --- a/src/mesh/generated/meshtastic/atak.pb.cpp +++ b/src/mesh/generated/meshtastic/atak.pb.cpp @@ -22,6 +22,80 @@ PB_BIND(meshtastic_Contact, meshtastic_Contact, AUTO) 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) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mesh/generated/meshtastic/atak.pb.h b/src/mesh/generated/meshtastic/atak.pb.h index 8533bcbf9..d69b14009 100644 --- a/src/mesh/generated/meshtastic/atak.pb.h +++ b/src/mesh/generated/meshtastic/atak.pb.h @@ -65,10 +65,479 @@ typedef enum _meshtastic_MemberRole { meshtastic_MemberRole_K9 = 8 } meshtastic_MemberRole; +/* CoT how field values. + Represents how the coordinates were generated. */ +typedef enum _meshtastic_CotHow { + /* Unspecified */ + meshtastic_CotHow_CotHow_Unspecified = 0, + /* Human entered */ + meshtastic_CotHow_CotHow_h_e = 1, + /* Machine generated */ + meshtastic_CotHow_CotHow_m_g = 2, + /* Human GPS/INS derived */ + meshtastic_CotHow_CotHow_h_g_i_g_o = 3, + /* Machine relayed (imported from another system/gateway) */ + meshtastic_CotHow_CotHow_m_r = 4, + /* Machine fused (corroborated from multiple sources) */ + meshtastic_CotHow_CotHow_m_f = 5, + /* Machine predicted */ + meshtastic_CotHow_CotHow_m_p = 6, + /* Machine simulated */ + meshtastic_CotHow_CotHow_m_s = 7 +} meshtastic_CotHow; + +/* Well-known CoT event types. + When the type is known, use the enum value for efficient encoding. + For unknown types, set cot_type_id to CotType_Other and populate cot_type_str. */ +typedef enum _meshtastic_CotType { + /* Unknown or unmapped type, use cot_type_str */ + meshtastic_CotType_CotType_Other = 0, + /* a-f-G-U-C: Friendly ground unit combat */ + meshtastic_CotType_CotType_a_f_G_U_C = 1, + /* a-f-G-U-C-I: Friendly ground unit combat infantry */ + meshtastic_CotType_CotType_a_f_G_U_C_I = 2, + /* a-n-A-C-F: Neutral aircraft civilian fixed-wing */ + meshtastic_CotType_CotType_a_n_A_C_F = 3, + /* a-n-A-C-H: Neutral aircraft civilian helicopter */ + meshtastic_CotType_CotType_a_n_A_C_H = 4, + /* a-n-A-C: Neutral aircraft civilian */ + meshtastic_CotType_CotType_a_n_A_C = 5, + /* a-f-A-M-H: Friendly aircraft military helicopter */ + meshtastic_CotType_CotType_a_f_A_M_H = 6, + /* a-f-A-M: Friendly aircraft military */ + meshtastic_CotType_CotType_a_f_A_M = 7, + /* a-f-A-M-F-F: Friendly aircraft military fixed-wing fighter */ + meshtastic_CotType_CotType_a_f_A_M_F_F = 8, + /* a-f-A-M-H-A: Friendly aircraft military helicopter attack */ + meshtastic_CotType_CotType_a_f_A_M_H_A = 9, + /* a-f-A-M-H-U-M: Friendly aircraft military helicopter utility medium */ + meshtastic_CotType_CotType_a_f_A_M_H_U_M = 10, + /* a-h-A-M-F-F: Hostile aircraft military fixed-wing fighter */ + meshtastic_CotType_CotType_a_h_A_M_F_F = 11, + /* a-h-A-M-H-A: Hostile aircraft military helicopter attack */ + meshtastic_CotType_CotType_a_h_A_M_H_A = 12, + /* a-u-A-C: Unknown aircraft civilian */ + meshtastic_CotType_CotType_a_u_A_C = 13, + /* t-x-d-d: Tasking delete/disconnect */ + meshtastic_CotType_CotType_t_x_d_d = 14, + /* a-f-G-E-S-E: Friendly ground equipment sensor */ + meshtastic_CotType_CotType_a_f_G_E_S_E = 15, + /* a-f-G-E-V-C: Friendly ground equipment vehicle */ + meshtastic_CotType_CotType_a_f_G_E_V_C = 16, + /* a-f-S: Friendly sea */ + meshtastic_CotType_CotType_a_f_S = 17, + /* a-f-A-M-F: Friendly aircraft military fixed-wing */ + meshtastic_CotType_CotType_a_f_A_M_F = 18, + /* a-f-A-M-F-C-H: Friendly aircraft military fixed-wing cargo heavy */ + meshtastic_CotType_CotType_a_f_A_M_F_C_H = 19, + /* a-f-A-M-F-U-L: Friendly aircraft military fixed-wing utility light */ + meshtastic_CotType_CotType_a_f_A_M_F_U_L = 20, + /* a-f-A-M-F-L: Friendly aircraft military fixed-wing liaison */ + meshtastic_CotType_CotType_a_f_A_M_F_L = 21, + /* a-f-A-M-F-P: Friendly aircraft military fixed-wing patrol */ + meshtastic_CotType_CotType_a_f_A_M_F_P = 22, + /* a-f-A-C-H: Friendly aircraft civilian helicopter */ + meshtastic_CotType_CotType_a_f_A_C_H = 23, + /* a-n-A-M-F-Q: Neutral aircraft military fixed-wing drone */ + meshtastic_CotType_CotType_a_n_A_M_F_Q = 24, + /* b-t-f: GeoChat message */ + meshtastic_CotType_CotType_b_t_f = 25, + /* b-r-f-h-c: CASEVAC/MEDEVAC report */ + meshtastic_CotType_CotType_b_r_f_h_c = 26, + /* b-a-o-pan: Ring the bell / alert all */ + meshtastic_CotType_CotType_b_a_o_pan = 27, + /* b-a-o-opn: Troops in contact */ + meshtastic_CotType_CotType_b_a_o_opn = 28, + /* b-a-o-can: Cancel alert */ + meshtastic_CotType_CotType_b_a_o_can = 29, + /* b-a-o-tbl: 911 alert */ + meshtastic_CotType_CotType_b_a_o_tbl = 30, + /* b-a-g: Geofence breach alert */ + meshtastic_CotType_CotType_b_a_g = 31, + /* a-f-G: Friendly ground (generic) */ + meshtastic_CotType_CotType_a_f_G = 32, + /* a-f-G-U: Friendly ground unit (generic) */ + meshtastic_CotType_CotType_a_f_G_U = 33, + /* a-h-G: Hostile ground (generic) */ + meshtastic_CotType_CotType_a_h_G = 34, + /* a-u-G: Unknown ground (generic) */ + meshtastic_CotType_CotType_a_u_G = 35, + /* a-n-G: Neutral ground (generic) */ + meshtastic_CotType_CotType_a_n_G = 36, + /* b-m-r: Route */ + meshtastic_CotType_CotType_b_m_r = 37, + /* b-m-p-w: Route waypoint */ + meshtastic_CotType_CotType_b_m_p_w = 38, + /* b-m-p-s-p-i: Self-position marker */ + meshtastic_CotType_CotType_b_m_p_s_p_i = 39, + /* u-d-f: Freeform shape (line/polygon) */ + meshtastic_CotType_CotType_u_d_f = 40, + /* u-d-r: Rectangle */ + meshtastic_CotType_CotType_u_d_r = 41, + /* u-d-c-c: Circle */ + meshtastic_CotType_CotType_u_d_c_c = 42, + /* u-rb-a: Range/bearing line */ + meshtastic_CotType_CotType_u_rb_a = 43, + /* a-h-A: Hostile aircraft (generic) */ + meshtastic_CotType_CotType_a_h_A = 44, + /* a-u-A: Unknown aircraft (generic) */ + meshtastic_CotType_CotType_a_u_A = 45, + /* a-f-A-M-H-Q: Friendly aircraft military helicopter observation */ + meshtastic_CotType_CotType_a_f_A_M_H_Q = 46, + /* a-f-A-C-F: Friendly aircraft civilian fixed-wing */ + meshtastic_CotType_CotType_a_f_A_C_F = 47, + /* a-f-A-C: Friendly aircraft civilian (generic) */ + meshtastic_CotType_CotType_a_f_A_C = 48, + /* a-f-A-C-L: Friendly aircraft civilian lighter-than-air */ + meshtastic_CotType_CotType_a_f_A_C_L = 49, + /* a-f-A: Friendly aircraft (generic) */ + meshtastic_CotType_CotType_a_f_A = 50, + /* a-f-A-M-H-C: Friendly aircraft military helicopter cargo */ + meshtastic_CotType_CotType_a_f_A_M_H_C = 51, + /* a-n-A-M-F-F: Neutral aircraft military fixed-wing fighter */ + meshtastic_CotType_CotType_a_n_A_M_F_F = 52, + /* a-u-A-C-F: Unknown aircraft civilian fixed-wing */ + meshtastic_CotType_CotType_a_u_A_C_F = 53, + /* a-f-G-U-C-F-T-A: Friendly ground unit combat forces theater aviation */ + meshtastic_CotType_CotType_a_f_G_U_C_F_T_A = 54, + /* a-f-G-U-C-V-S: Friendly ground unit combat vehicle support */ + meshtastic_CotType_CotType_a_f_G_U_C_V_S = 55, + /* a-f-G-U-C-R-X: Friendly ground unit combat reconnaissance exploitation */ + meshtastic_CotType_CotType_a_f_G_U_C_R_X = 56, + /* a-f-G-U-C-I-Z: Friendly ground unit combat infantry mechanized */ + meshtastic_CotType_CotType_a_f_G_U_C_I_Z = 57, + /* a-f-G-U-C-E-C-W: Friendly ground unit combat engineer construction wheeled */ + meshtastic_CotType_CotType_a_f_G_U_C_E_C_W = 58, + /* a-f-G-U-C-I-L: Friendly ground unit combat infantry light */ + meshtastic_CotType_CotType_a_f_G_U_C_I_L = 59, + /* a-f-G-U-C-R-O: Friendly ground unit combat reconnaissance other */ + meshtastic_CotType_CotType_a_f_G_U_C_R_O = 60, + /* a-f-G-U-C-R-V: Friendly ground unit combat reconnaissance cavalry */ + meshtastic_CotType_CotType_a_f_G_U_C_R_V = 61, + /* a-f-G-U-H: Friendly ground unit headquarters */ + meshtastic_CotType_CotType_a_f_G_U_H = 62, + /* a-f-G-U-U-M-S-E: Friendly ground unit support medical surgical evacuation */ + meshtastic_CotType_CotType_a_f_G_U_U_M_S_E = 63, + /* a-f-G-U-S-M-C: Friendly ground unit support maintenance collection */ + meshtastic_CotType_CotType_a_f_G_U_S_M_C = 64, + /* a-f-G-E-S: Friendly ground equipment sensor (generic) */ + meshtastic_CotType_CotType_a_f_G_E_S = 65, + /* a-f-G-E: Friendly ground equipment (generic) */ + meshtastic_CotType_CotType_a_f_G_E = 66, + /* a-f-G-E-V-C-U: Friendly ground equipment vehicle utility */ + meshtastic_CotType_CotType_a_f_G_E_V_C_U = 67, + /* a-f-G-E-V-C-ps: Friendly ground equipment vehicle public safety */ + meshtastic_CotType_CotType_a_f_G_E_V_C_ps = 68, + /* a-u-G-E-V: Unknown ground equipment vehicle */ + meshtastic_CotType_CotType_a_u_G_E_V = 69, + /* a-f-S-N-N-R: Friendly sea surface non-naval rescue */ + meshtastic_CotType_CotType_a_f_S_N_N_R = 70, + /* a-f-F-B: Friendly force boundary */ + meshtastic_CotType_CotType_a_f_F_B = 71, + /* b-m-p-s-p-loc: Self-position location marker */ + meshtastic_CotType_CotType_b_m_p_s_p_loc = 72, + /* b-i-v: Imagery/video */ + meshtastic_CotType_CotType_b_i_v = 73, + /* b-f-t-r: File transfer request */ + meshtastic_CotType_CotType_b_f_t_r = 74, + /* b-f-t-a: File transfer acknowledgment */ + meshtastic_CotType_CotType_b_f_t_a = 75, + /* u-d-f-m: Freehand telestration / annotation. Anchor at event point, + geometry carried via DrawnShape.vertices. May be truncated to + MAX_VERTICES by the sender. */ + meshtastic_CotType_CotType_u_d_f_m = 76, + /* u-d-p: Closed polygon. Geometry carried via DrawnShape.vertices, + implicitly closed (receiver duplicates first vertex as needed). */ + meshtastic_CotType_CotType_u_d_p = 77, + /* b-m-p-s-m: Spot map marker (colored dot at a point of interest). */ + meshtastic_CotType_CotType_b_m_p_s_m = 78, + /* b-m-p-c: Checkpoint (intermediate route control point). */ + meshtastic_CotType_CotType_b_m_p_c = 79, + /* u-r-b-c-c: Ranging circle (range rings centered on the event point). */ + meshtastic_CotType_CotType_u_r_b_c_c = 80, + /* u-r-b-bullseye: Bullseye with configurable range rings and bearing + reference (magnetic / true / grid). */ + meshtastic_CotType_CotType_u_r_b_bullseye = 81, + /* a-f-G-E-V-A: Friendly armored vehicle, user-selectable self PLI. */ + meshtastic_CotType_CotType_a_f_G_E_V_A = 82, + /* a-n-A: Neutral aircraft (friendly/hostile/unknown already present). */ + meshtastic_CotType_CotType_a_n_A = 83, + /* --- 2525 quick-drop: artillery (4) ---------------------------------- */ + meshtastic_CotType_CotType_a_u_G_U_C_F = 84, + meshtastic_CotType_CotType_a_n_G_U_C_F = 85, + meshtastic_CotType_CotType_a_h_G_U_C_F = 86, + meshtastic_CotType_CotType_a_f_G_U_C_F = 87, + /* --- 2525 quick-drop: building (4) ----------------------------------- */ + meshtastic_CotType_CotType_a_u_G_I = 88, + meshtastic_CotType_CotType_a_n_G_I = 89, + meshtastic_CotType_CotType_a_h_G_I = 90, + meshtastic_CotType_CotType_a_f_G_I = 91, + /* --- 2525 quick-drop: mine (4) --------------------------------------- */ + meshtastic_CotType_CotType_a_u_G_E_X_M = 92, + meshtastic_CotType_CotType_a_n_G_E_X_M = 93, + meshtastic_CotType_CotType_a_h_G_E_X_M = 94, + meshtastic_CotType_CotType_a_f_G_E_X_M = 95, + /* --- 2525 quick-drop: ship (3; a-f-S already at 17) ------------------ */ + meshtastic_CotType_CotType_a_u_S = 96, + meshtastic_CotType_CotType_a_n_S = 97, + meshtastic_CotType_CotType_a_h_S = 98, + /* --- 2525 quick-drop: sniper (4) ------------------------------------- */ + meshtastic_CotType_CotType_a_u_G_U_C_I_d = 99, + meshtastic_CotType_CotType_a_n_G_U_C_I_d = 100, + meshtastic_CotType_CotType_a_h_G_U_C_I_d = 101, + meshtastic_CotType_CotType_a_f_G_U_C_I_d = 102, + /* --- 2525 quick-drop: tank (4) --------------------------------------- */ + meshtastic_CotType_CotType_a_u_G_E_V_A_T = 103, + meshtastic_CotType_CotType_a_n_G_E_V_A_T = 104, + meshtastic_CotType_CotType_a_h_G_E_V_A_T = 105, + meshtastic_CotType_CotType_a_f_G_E_V_A_T = 106, + /* --- 2525 quick-drop: troops (3; a-f-G-U-C-I already at 2) ----------- */ + meshtastic_CotType_CotType_a_u_G_U_C_I = 107, + meshtastic_CotType_CotType_a_n_G_U_C_I = 108, + meshtastic_CotType_CotType_a_h_G_U_C_I = 109, + /* --- 2525 quick-drop: generic vehicle (3; a-u-G-E-V already at 69) --- */ + meshtastic_CotType_CotType_a_n_G_E_V = 110, + meshtastic_CotType_CotType_a_h_G_E_V = 111, + meshtastic_CotType_CotType_a_f_G_E_V = 112, + /* b-m-p-w-GOTO: Go To / bloodhound navigation target. */ + meshtastic_CotType_CotType_b_m_p_w_GOTO = 113, + /* b-m-p-c-ip: Initial point (mission planning). */ + meshtastic_CotType_CotType_b_m_p_c_ip = 114, + /* b-m-p-c-cp: Contact point (mission planning). */ + meshtastic_CotType_CotType_b_m_p_c_cp = 115, + /* b-m-p-s-p-op: Observation post. */ + meshtastic_CotType_CotType_b_m_p_s_p_op = 116, + /* u-d-v: 2D vehicle outline drawn on the map. */ + meshtastic_CotType_CotType_u_d_v = 117, + /* u-d-v-m: 3D vehicle model reference. */ + meshtastic_CotType_CotType_u_d_v_m = 118, + /* u-d-c-e: Non-circular ellipse (circle with distinct major/minor axes). */ + meshtastic_CotType_CotType_u_d_c_e = 119, + /* b-i-x-i: Quick Pic geotagged image marker. The image itself does not + ride on LoRa; this event references the image via iconset metadata. */ + meshtastic_CotType_CotType_b_i_x_i = 120, + /* b-t-f-d: GeoChat delivered receipt. Carried on the existing `chat` + payload_variant via GeoChat.receipt_for_uid + receipt_type. */ + meshtastic_CotType_CotType_b_t_f_d = 121, + /* b-t-f-r: GeoChat read receipt. Same wire slot as b-t-f-d. */ + meshtastic_CotType_CotType_b_t_f_r = 122, + /* b-a-o-c: Custom / generic emergency beacon. */ + meshtastic_CotType_CotType_b_a_o_c = 123, + /* t-s: Task / engage request. Structured payload carried via the new + TaskRequest typed variant. */ + meshtastic_CotType_CotType_t_s = 124 +} meshtastic_CotType; + +/* Geopoint and altitude source */ +typedef enum _meshtastic_GeoPointSource { + /* Unspecified */ + meshtastic_GeoPointSource_GeoPointSource_Unspecified = 0, + /* GPS derived */ + meshtastic_GeoPointSource_GeoPointSource_GPS = 1, + /* User entered */ + meshtastic_GeoPointSource_GeoPointSource_USER = 2, + /* Network/external */ + meshtastic_GeoPointSource_GeoPointSource_NETWORK = 3 +} meshtastic_GeoPointSource; + +/* Receipt discriminator. Set alongside cot_type_id = b-t-f-d (delivered) + or b-t-f-r (read). ReceiptType_None is the default for a normal chat + message (cot_type_id = b-t-f). + + Receivers can detect a receipt by checking receipt_type != ReceiptType_None + without re-parsing the envelope cot_type_id. */ +typedef enum _meshtastic_GeoChat_ReceiptType { + meshtastic_GeoChat_ReceiptType_ReceiptType_None = 0, /* normal chat message */ + meshtastic_GeoChat_ReceiptType_ReceiptType_Delivered = 1, /* b-t-f-d delivered receipt */ + meshtastic_GeoChat_ReceiptType_ReceiptType_Read = 2 /* b-t-f-r read receipt */ +} meshtastic_GeoChat_ReceiptType; + +/* Shape kind discriminator. Drives receiver rendering and also controls + which optional fields below are meaningful. */ +typedef enum _meshtastic_DrawnShape_Kind { + /* Unspecified (do not use on the wire) */ + meshtastic_DrawnShape_Kind_Kind_Unspecified = 0, + /* u-d-c-c: User-drawn circle (uses major/minor/angle, anchor = event point) */ + meshtastic_DrawnShape_Kind_Kind_Circle = 1, + /* u-d-r: User-drawn rectangle (uses vertices = 4 corners) */ + meshtastic_DrawnShape_Kind_Kind_Rectangle = 2, + /* u-d-f: User-drawn polyline (uses vertices, not closed) */ + meshtastic_DrawnShape_Kind_Kind_Freeform = 3, + /* u-d-f-m: Freehand telestration / annotation (uses vertices, may be truncated) */ + meshtastic_DrawnShape_Kind_Kind_Telestration = 4, + /* u-d-p: Closed polygon (uses vertices, implicitly closed) */ + meshtastic_DrawnShape_Kind_Kind_Polygon = 5, + /* u-r-b-c-c: Ranging circle (major/minor/angle, stroke + optional fill) */ + meshtastic_DrawnShape_Kind_Kind_RangingCircle = 6, + /* u-r-b-bullseye: Bullseye ring with range rings and bearing reference */ + meshtastic_DrawnShape_Kind_Kind_Bullseye = 7, + /* u-d-c-e: Ellipse with distinct major/minor axes (same storage as + Kind_Circle — uses major_cm/minor_cm/angle_deg — but receivers + render it as a non-circular ellipse rather than a round circle). */ + meshtastic_DrawnShape_Kind_Kind_Ellipse = 8, + /* u-d-v: 2D vehicle outline drawn on the map. Vertices carry the + outline polygon; receivers draw it as a filled polygon. */ + meshtastic_DrawnShape_Kind_Kind_Vehicle2D = 9, + /* u-d-v-m: 3D vehicle model reference. Same vertex polygon as + Kind_Vehicle2D; receivers that support 3D rendering extrude it. */ + meshtastic_DrawnShape_Kind_Kind_Vehicle3D = 10 +} meshtastic_DrawnShape_Kind; + +/* Explicit stroke/fill/both discriminator. + + ATAK's source XML distinguishes "stroke-only polyline" from "closed shape + with both stroke and fill" by the presence of the element. + Both states can hash to all-zero color fields, so we carry the signal + explicitly. Parser sets this from (sawStrokeColor, sawFillColor) at the + end of parse; builder uses it to decide which of / + to emit in the reconstructed XML. */ +typedef enum _meshtastic_DrawnShape_StyleMode { + /* Unspecified — receiver infers from which color fields are non-zero. */ + meshtastic_DrawnShape_StyleMode_StyleMode_Unspecified = 0, + /* Stroke only. No in the source XML. Used for polylines, + ranging lines, bullseye rings. */ + meshtastic_DrawnShape_StyleMode_StyleMode_StrokeOnly = 1, + /* Fill only. No in the source XML. Rare but valid in + ATAK (solid region with no outline). */ + meshtastic_DrawnShape_StyleMode_StyleMode_FillOnly = 2, + /* Both stroke and fill present. Closed shapes: circle, rectangle, + polygon, ranging circle. */ + meshtastic_DrawnShape_StyleMode_StyleMode_StrokeAndFill = 3 +} meshtastic_DrawnShape_StyleMode; + +/* Marker kind. Used to pick sensible receiver defaults when the CoT type + alone is ambiguous (e.g. a-u-G could be a 2525 symbol or a custom icon + depending on the iconset path). */ +typedef enum _meshtastic_Marker_Kind { + /* Unspecified — fall back to TAKPacketV2.cot_type_id */ + meshtastic_Marker_Kind_Kind_Unspecified = 0, + /* b-m-p-s-m: Spot map marker */ + meshtastic_Marker_Kind_Kind_Spot = 1, + /* b-m-p-w: Route waypoint */ + meshtastic_Marker_Kind_Kind_Waypoint = 2, + /* b-m-p-c: Checkpoint */ + meshtastic_Marker_Kind_Kind_Checkpoint = 3, + /* b-m-p-s-p-i / b-m-p-s-p-loc: Self-position marker */ + meshtastic_Marker_Kind_Kind_SelfPosition = 4, + /* 2525B/C military symbol (iconsetpath = COT_MAPPING_2525B/...) */ + meshtastic_Marker_Kind_Kind_Symbol2525 = 5, + /* COT_MAPPING_SPOTMAP icon (e.g. colored dot) */ + meshtastic_Marker_Kind_Kind_SpotMap = 6, + /* Custom icon set (UUID/GroupName/filename.png) */ + meshtastic_Marker_Kind_Kind_CustomIcon = 7, + /* b-m-p-w-GOTO: Go To / bloodhound navigation waypoint. */ + meshtastic_Marker_Kind_Kind_GoToPoint = 8, + /* b-m-p-c-ip: Initial point (mission planning control point). */ + meshtastic_Marker_Kind_Kind_InitialPoint = 9, + /* b-m-p-c-cp: Contact point (mission planning control point). */ + meshtastic_Marker_Kind_Kind_ContactPoint = 10, + /* b-m-p-s-p-op: Observation post. */ + meshtastic_Marker_Kind_Kind_ObservationPost = 11, + /* b-i-x-i: Quick Pic geotagged image marker. iconset carries the + image reference (local filename or remote URL); the image itself + does not ride on the LoRa wire. */ + meshtastic_Marker_Kind_Kind_ImageMarker = 12 +} meshtastic_Marker_Kind; + +/* Travel method for the route. */ +typedef enum _meshtastic_Route_Method { + /* Unspecified / unknown */ + meshtastic_Route_Method_Method_Unspecified = 0, + /* Driving / vehicle */ + meshtastic_Route_Method_Method_Driving = 1, + /* Walking / foot */ + meshtastic_Route_Method_Method_Walking = 2, + /* Flying */ + meshtastic_Route_Method_Method_Flying = 3, + /* Swimming (individual) */ + meshtastic_Route_Method_Method_Swimming = 4, + /* Watercraft (boat) */ + meshtastic_Route_Method_Method_Watercraft = 5 +} meshtastic_Route_Method; + +/* Route direction (infil = ingress, exfil = egress). */ +typedef enum _meshtastic_Route_Direction { + /* Unspecified */ + meshtastic_Route_Direction_Direction_Unspecified = 0, + /* Infiltration (ingress) */ + meshtastic_Route_Direction_Direction_Infil = 1, + /* Exfiltration (egress) */ + meshtastic_Route_Direction_Direction_Exfil = 2 +} meshtastic_Route_Direction; + +/* Line 3: precedence / urgency. */ +typedef enum _meshtastic_CasevacReport_Precedence { + meshtastic_CasevacReport_Precedence_Precedence_Unspecified = 0, + meshtastic_CasevacReport_Precedence_Precedence_Urgent = 1, /* A - immediate, life-threatening */ + meshtastic_CasevacReport_Precedence_Precedence_UrgentSurgical = 2, /* B - needs surgery */ + meshtastic_CasevacReport_Precedence_Precedence_Priority = 3, /* C - within 4 hours */ + meshtastic_CasevacReport_Precedence_Precedence_Routine = 4, /* D - within 24 hours */ + meshtastic_CasevacReport_Precedence_Precedence_Convenience = 5 /* E - convenience */ +} meshtastic_CasevacReport_Precedence; + +/* Line 7: HLZ marking method. */ +typedef enum _meshtastic_CasevacReport_HlzMarking { + meshtastic_CasevacReport_HlzMarking_HlzMarking_Unspecified = 0, + meshtastic_CasevacReport_HlzMarking_HlzMarking_Panels = 1, + meshtastic_CasevacReport_HlzMarking_HlzMarking_PyroSignal = 2, + meshtastic_CasevacReport_HlzMarking_HlzMarking_Smoke = 3, + meshtastic_CasevacReport_HlzMarking_HlzMarking_None = 4, + meshtastic_CasevacReport_HlzMarking_HlzMarking_Other = 5 +} meshtastic_CasevacReport_HlzMarking; + +/* Line 6: security situation at the pickup zone. */ +typedef enum _meshtastic_CasevacReport_Security { + meshtastic_CasevacReport_Security_Security_Unspecified = 0, + meshtastic_CasevacReport_Security_Security_NoEnemy = 1, /* N - no enemy activity */ + meshtastic_CasevacReport_Security_Security_PossibleEnemy = 2, /* P - possible enemy */ + meshtastic_CasevacReport_Security_Security_EnemyInArea = 3, /* E - enemy, approach with caution */ + meshtastic_CasevacReport_Security_Security_EnemyInArmedContact = 4 /* X - armed escort required */ +} meshtastic_CasevacReport_Security; + +typedef enum _meshtastic_EmergencyAlert_Type { + meshtastic_EmergencyAlert_Type_Type_Unspecified = 0, + meshtastic_EmergencyAlert_Type_Type_Alert911 = 1, /* b-a-o-tbl */ + meshtastic_EmergencyAlert_Type_Type_RingTheBell = 2, /* b-a-o-pan */ + meshtastic_EmergencyAlert_Type_Type_InContact = 3, /* b-a-o-opn */ + meshtastic_EmergencyAlert_Type_Type_GeoFenceBreached = 4, /* b-a-g */ + meshtastic_EmergencyAlert_Type_Type_Custom = 5, /* b-a-o-c */ + meshtastic_EmergencyAlert_Type_Type_Cancel = 6 /* b-a-o-can */ +} meshtastic_EmergencyAlert_Type; + +typedef enum _meshtastic_TaskRequest_Priority { + meshtastic_TaskRequest_Priority_Priority_Unspecified = 0, + meshtastic_TaskRequest_Priority_Priority_Low = 1, + meshtastic_TaskRequest_Priority_Priority_Normal = 2, + meshtastic_TaskRequest_Priority_Priority_High = 3, + meshtastic_TaskRequest_Priority_Priority_Critical = 4 +} meshtastic_TaskRequest_Priority; + +typedef enum _meshtastic_TaskRequest_Status { + meshtastic_TaskRequest_Status_Status_Unspecified = 0, + meshtastic_TaskRequest_Status_Status_Pending = 1, /* assigned, not yet acknowledged */ + meshtastic_TaskRequest_Status_Status_Acknowledged = 2, /* assignee has seen it */ + meshtastic_TaskRequest_Status_Status_InProgress = 3, /* assignee is working it */ + meshtastic_TaskRequest_Status_Status_Completed = 4, /* task done */ + meshtastic_TaskRequest_Status_Status_Cancelled = 5 /* cancelled before completion */ +} meshtastic_TaskRequest_Status; + +/* Coarse sensor category, inferred from `model` on parse when the source + XML doesn't label it. Receivers that render differently per sensor + class (thermal overlay vs daylight cone) use this. */ +typedef enum _meshtastic_SensorFov_SensorType { + meshtastic_SensorFov_SensorType_SensorType_Unspecified = 0, + meshtastic_SensorFov_SensorType_SensorType_Camera = 1, /* daylight / general optical */ + meshtastic_SensorFov_SensorType_SensorType_Thermal = 2, /* FLIR, thermal imager */ + meshtastic_SensorFov_SensorType_SensorType_Laser = 3, /* rangefinder, LRF, designator */ + meshtastic_SensorFov_SensorType_SensorType_Nvg = 4, /* night vision goggles */ + meshtastic_SensorFov_SensorType_SensorType_Rf = 5, /* radio/radar direction-finding */ + meshtastic_SensorFov_SensorType_SensorType_Other = 6 +} meshtastic_SensorFov_SensorType; + /* Struct definitions */ /* ATAK GeoChat message */ typedef struct _meshtastic_GeoChat { - /* The text message */ + /* The text message. Empty for receipts. */ char message[200]; /* Uid recipient of the message */ bool has_to; @@ -76,6 +545,14 @@ typedef struct _meshtastic_GeoChat { /* Callsign of the recipient for the message */ bool has_to_callsign; char to_callsign[120]; + /* UID of the chat message this event is acknowledging. Empty for a + normal chat message; set for delivered / read receipts. Paired with + receipt_type so receivers can match the ack back to the original + outbound GeoChat by its event uid. */ + char receipt_for_uid[48]; + /* Receipt kind discriminator. See ReceiptType doc. Default ReceiptType_None + means this is a regular chat message, not a receipt. */ + meshtastic_GeoChat_ReceiptType receipt_type; } meshtastic_GeoChat; /* ATAK Group @@ -146,6 +623,549 @@ typedef struct _meshtastic_TAKPacket { } payload_variant; } meshtastic_TAKPacket; +/* Aircraft track information from ADS-B or military air tracking. + Covers the majority of observed real-world CoT traffic. */ +typedef struct _meshtastic_AircraftTrack { + /* ICAO hex identifier (e.g. "AD237C") */ + char icao[8]; + /* Aircraft registration (e.g. "N946AK") */ + char registration[16]; + /* Flight number/callsign (e.g. "ASA864") */ + char flight[16]; + /* ICAO aircraft type designator (e.g. "B39M") */ + char aircraft_type[8]; + /* Transponder squawk code (0-7777 octal) */ + uint16_t squawk; + /* ADS-B emitter category (e.g. "A3") */ + char category[4]; + /* Received signal strength * 10 (e.g. -194 for -19.4 dBm) */ + int32_t rssi_x10; + /* Whether receiver has GPS fix */ + bool gps; + /* CoT host ID for source attribution */ + char cot_host_id[64]; +} meshtastic_AircraftTrack; + +/* Compact geographic vertex used by repeated vertex lists in TAK geometry + payloads. Named with a `Cot` prefix to avoid a namespace collision with + `meshtastic.GeoPoint` in `device_ui.proto`, which is an unrelated zoom/ + latitude/longitude type used by the on-device map UI. + + Encoded as a signed DELTA from TAKPacketV2.latitude_i / longitude_i (the + enclosing event's anchor point). The absolute coordinate is recovered by + the receiver as `event.latitude_i + vertex.lat_delta_i` (and likewise for + longitude). + + Why deltas: a 32-vertex telestration with vertices clustered within a few + hundred meters of the anchor has per-vertex deltas in the ±10^4 range. + Under sint32+zigzag those encode as 2 bytes each (tag+varint), versus the + 4 bytes that sfixed32 would always require. At 32 vertices that is ~128 + bytes of savings — the difference between fitting under the LoRa MTU or + not. Absolute coordinates (values ~10^9) would cost sint32 varint 5 bytes + per field, which is why TAKPacketV2's top-level latitude_i / longitude_i + stay sfixed32 — only small values win with sint32. */ +typedef struct _meshtastic_CotGeoPoint { + /* Latitude delta from TAKPacketV2.latitude_i, in 1e-7 degree units. + Add to the enclosing event's latitude_i to recover the absolute latitude. */ + int32_t lat_delta_i; + /* Longitude delta from TAKPacketV2.longitude_i, in 1e-7 degree units. */ + int32_t lon_delta_i; +} meshtastic_CotGeoPoint; + +/* User-drawn tactical graphic: circle, rectangle, polygon, polyline, freehand + telestration, ranging circle, or bullseye. + + Covers CoT types u-d-c-c, u-d-r, u-d-f, u-d-f-m, u-d-p, u-r-b-c-c, + u-r-b-bullseye. The shape's anchor position is carried on + TAKPacketV2.latitude_i/longitude_i; polyline/polygon vertices are in the + `vertices` repeated field as `CotGeoPoint` deltas from that anchor. + + Colors use the Team enum as a 14-color palette (see color encoding below) + with a fixed32 exact-ARGB fallback for custom user-picked colors that + don't map to a palette entry. */ +typedef struct _meshtastic_DrawnShape { + /* Shape kind (circle, rectangle, freeform, etc.) */ + meshtastic_DrawnShape_Kind kind; + /* Explicit stroke/fill/both discriminator. See StyleMode doc. */ + meshtastic_DrawnShape_StyleMode style; + /* Ellipse major radius in centimeters. 0 for non-ellipse kinds. */ + uint32_t major_cm; + /* Ellipse minor radius in centimeters. 0 for non-ellipse kinds. */ + uint32_t minor_cm; + /* Ellipse rotation angle in degrees. Valid values are 0..360 inclusive; + 0 and 360 are equivalent rotations. In proto3, an unset uint32 reads + as 0, so senders should emit 0 when the angle is unspecified. */ + uint16_t angle_deg; + /* Stroke color as a named palette entry from the Team enum. If + Unspecifed_Color, the exact ARGB is carried in stroke_argb. + Valid only when style is StrokeOnly or StrokeAndFill. */ + meshtastic_Team stroke_color; + /* Stroke color as an exact 32-bit ARGB bit pattern. Always populated + on the wire; readers MUST use this value when stroke_color == + Unspecifed_Color and MAY use it to recover the exact original bytes + even when a palette entry is set. */ + uint32_t stroke_argb; + /* Stroke weight in tenths of a unit (e.g. 30 = 3.0). Typical ATAK + range 10..60. */ + uint16_t stroke_weight_x10; + /* Fill color as a named palette entry. See stroke_color docs. + Valid only when style is FillOnly or StrokeAndFill. */ + meshtastic_Team fill_color; + /* Fill color exact ARGB fallback. See stroke_argb docs. */ + uint32_t fill_argb; + /* Whether labels are rendered on this shape. */ + bool labels_on; + /* Vertex list for polyline/polygon/rectangle shapes. Capped at 32 by + the nanopb pool; senders MUST truncate longer inputs and set + `truncated = true`. */ + pb_size_t vertices_count; + meshtastic_CotGeoPoint vertices[32]; + /* True if the sender truncated `vertices` to fit the pool. */ + bool truncated; /* --- Bullseye-only fields. All ignored unless kind == Kind_Bullseye. --- */ + /* Bullseye distance in meters * 10 (e.g. 3285 = 328.5 m). 0 = unset. */ + uint32_t bullseye_distance_dm; + /* Bullseye bearing reference: 0 unset, 1 Magnetic, 2 True, 3 Grid. */ + uint8_t bullseye_bearing_ref; + /* Bullseye attribute bit flags: + bit 0: rangeRingVisible + bit 1: hasRangeRings + bit 2: edgeToCenter + bit 3: mils */ + uint8_t bullseye_flags; + /* Bullseye reference UID (anchor marker). Empty = anchor is self. */ + char bullseye_uid_ref[48]; +} meshtastic_DrawnShape; + +/* Fixed point of interest: spot marker, waypoint, checkpoint, 2525 symbol, + or custom icon. + + Covers CoT types b-m-p-s-m, b-m-p-w, b-m-p-c, b-m-p-s-p-i, b-m-p-s-p-loc, + plus a-u-G / a-f-G / a-h-G / a-n-G with iconset paths. The marker position + is carried on TAKPacketV2.latitude_i/longitude_i; fields below carry only + the marker-specific metadata. */ +typedef struct _meshtastic_Marker { + /* Marker kind */ + meshtastic_Marker_Kind kind; + /* Marker color as a named palette entry. If Unspecifed_Color, the exact + ARGB is in color_argb. */ + meshtastic_Team color; + /* Marker color exact ARGB bit pattern. Always populated on the wire. */ + uint32_t color_argb; + /* Status readiness flag (ATAK ). */ + bool readiness; + /* Parent link UID (ATAK ). Empty = no parent. + For spot/waypoint markers this is typically the producing TAK user's UID. */ + char parent_uid[48]; + /* Parent CoT type (e.g. "a-f-G-U-C"). Usually the parent TAK user's type. */ + char parent_type[24]; + /* Parent callsign (e.g. "HOPE"). */ + char parent_callsign[24]; + /* Iconset path stored verbatim. ATAK emits three flavors: + Kind_Symbol2525 -> "COT_MAPPING_2525B//" + Kind_SpotMap -> "COT_MAPPING_SPOTMAP//" + Kind_CustomIcon -> "//.png" + Stored end-to-end without prefix stripping; the ~19 bytes saved by + stripping well-known prefixes are not worth the builder-side bug + surface, and the dict compresses the repetition effectively. */ + char iconset[80]; +} meshtastic_Marker; + +/* Range and bearing measurement line from the event anchor to a target point. + + Covers CoT type u-rb-a. The anchor position is on + TAKPacketV2.latitude_i/longitude_i; the target endpoint is carried as a + CotGeoPoint — same delta-from-anchor encoding used by DrawnShape.vertices + so a self-anchored RAB (common case) encodes in zero bytes. */ +typedef struct _meshtastic_RangeAndBearing { + /* Target/anchor endpoint (delta-encoded from TAKPacketV2.latitude_i/longitude_i). */ + bool has_anchor; + meshtastic_CotGeoPoint anchor; + /* Anchor UID (from ). Empty = free-standing. */ + char anchor_uid[48]; + /* Range in centimeters (value * 100). Range 0..4294 km. */ + uint32_t range_cm; + /* Bearing in degrees * 100 (0..36000). */ + uint16_t bearing_cdeg; + /* Stroke color as a Team palette entry. See DrawnShape.stroke_color doc. */ + meshtastic_Team stroke_color; + /* Stroke color exact ARGB fallback. */ + uint32_t stroke_argb; + /* Stroke weight * 10 (e.g. 30 = 3.0). */ + uint16_t stroke_weight_x10; +} meshtastic_RangeAndBearing; + +/* Route waypoint or control point. Each link corresponds to one ATAK + entry inside the b-m-r event. */ +typedef struct _meshtastic_Route_Link { + /* Waypoint position (delta-encoded from TAKPacketV2.latitude_i/longitude_i). */ + bool has_point; + meshtastic_CotGeoPoint point; + /* Optional UID (empty = receiver derives). */ + char uid[48]; + /* Optional display callsign (e.g. "CP1"). Empty for unnamed control points. */ + char callsign[16]; + /* Link role: 0 = waypoint (b-m-p-w), 1 = checkpoint (b-m-p-c). */ + uint8_t link_type; +} meshtastic_Route_Link; + +/* Named route consisting of ordered waypoints and control points. + + Covers CoT type b-m-r. The first waypoint's position is on + TAKPacketV2.latitude_i/longitude_i; subsequent waypoints and checkpoints + are in `links`. Link count is capped at 16 by the nanopb pool; senders + MUST truncate longer routes and set `truncated = true`. */ +typedef struct _meshtastic_Route { + /* Travel method */ + meshtastic_Route_Method method; + /* Direction (infil/exfil) */ + meshtastic_Route_Direction direction; + /* Waypoint name prefix (e.g. "CP"). */ + char prefix[8]; + /* Stroke weight * 10 (e.g. 30 = 3.0). 0 = default. */ + uint16_t stroke_weight_x10; + /* Ordered list of route control points. Capped at 16. */ + pb_size_t links_count; + meshtastic_Route_Link links[16]; + /* True if the sender truncated `links` to fit the pool. */ + bool truncated; +} meshtastic_Route; + +/* 9-line MEDEVAC request (CoT type b-r-f-h-c). + + Mirrors the ATAK MedLine tool's <_medevac_> detail element. Every field + is optional (proto3 default); senders omit lines they don't have. The + envelope (TAKPacketV2.uid, cot_type_id=b-r-f-h-c, latitude_i/longitude_i, + altitude, callsign) carries Line 1 (location) and Line 2 (callsign). + + All numeric fields are tight varints so a complete 9-line request fits + in well under 100 bytes of proto on the wire. */ +typedef struct _meshtastic_CasevacReport { + /* Line 3: precedence / urgency. */ + meshtastic_CasevacReport_Precedence precedence; + /* Line 4: special equipment required, as a bitfield. + bit 0: none + bit 1: hoist + bit 2: extraction equipment + bit 3: ventilator + bit 4: blood */ + uint8_t equipment_flags; + /* Line 5: number of litter (stretcher-bound) patients. */ + uint8_t litter_patients; + /* Line 5: number of ambulatory (walking-wounded) patients. */ + uint8_t ambulatory_patients; + /* Line 6: security situation at the PZ. */ + meshtastic_CasevacReport_Security security; + /* Line 7: HLZ marking method. */ + meshtastic_CasevacReport_HlzMarking hlz_marking; + /* Line 7 supplementary: short free-text describing the zone marker + (e.g. "Green smoke", "VS-17 panel west"). Capped tight in options. */ + char zone_marker[16]; + /* --- Line 8: patient nationality counts --- */ + uint8_t us_military; + uint8_t us_civilian; + uint8_t non_us_military; + uint8_t non_us_civilian; + uint8_t epw; /* enemy prisoner of war */ + uint8_t child; + /* Line 9: terrain and obstacles at the PZ, as a bitfield. + bit 0: slope + bit 1: rough + bit 2: loose + bit 3: trees + bit 4: wires + bit 5: other */ + uint8_t terrain_flags; + /* Line 2: radio frequency / callsign metadata (e.g. "38.90 Mhz" or + "Victor 6"). Capped tight in options. */ + char frequency[16]; + /* Short title / MEDEVAC identifier (e.g. "EAGLE.15.181230"). Usually the + same as the envelope callsign but ATAK sometimes carries a distinct + ops-number here. */ + pb_callback_t title; + /* Primary medline free-text — the single most clinically important line + on a MEDLINE form (e.g. "2 urgent litter patients, smoke on approach"). + MUST be preserved under MTU pressure as long as any casevac is sent. */ + pb_callback_t medline_remarks; + /* Line 3 (newer ATAK format): patient counts by precedence level. + Coexists with the enum-style `precedence` field (tag 1) — older ATAK + emits a single enum, newer ATAK emits these counts, and both can be + set simultaneously. Senders populate whichever style(s) the source + XML had; receivers prefer counts when non-zero. */ + uint32_t urgent_count; + uint32_t urgent_surgical_count; + uint32_t priority_count; + uint32_t routine_count; + uint32_t convenience_count; + /* Line 4 supplementary: free-text description of non-standard equipment + (e.g. "Blood warmer"). Pairs with the `equipment_flags` bitfield. */ + pb_callback_t equipment_detail; + /* Line 1 override: MGRS grid when distinct from the event anchor point + (e.g. "34T CQ 12345 67890"). Event lat/lon/hae still carries the + numeric location; this field preserves the exact MGRS string the + medic entered. */ + pb_callback_t zone_protected_coord; + /* Line 9 supplementary: slope direction (e.g. "N", "NE", "SSW") when + `terrain_flags` bit 0 (slope) is set. */ + pb_callback_t terrain_slope_dir; + /* Line 9 supplementary: free-text description of "other" terrain hazards + (e.g. "Loose debris on west edge") when `terrain_flags` bit 5 (other) + is set. Tier-2 strippable under MTU pressure. */ + pb_callback_t terrain_other_detail; + /* Line 7 supplementary: how the zone is being marked right now + (e.g. "Orange smoke", "VS-17 panel"). Complements the structured + `hlz_marking` enum with a specific human-readable description. */ + pb_callback_t marked_by; + /* Nearby obstacles on the approach (e.g. "Power lines north of HLZ"). */ + pb_callback_t obstacles; + /* Wind direction and speed (e.g. "270 at 12 kts"). */ + pb_callback_t winds_are_from; + /* Friendly forces posture near the pickup zone + (e.g. "Squad east of HLZ"). */ + pb_callback_t friendlies; + /* Known or suspected enemy positions near the pickup zone + (e.g. "Possible enemy on south ridge"). */ + pb_callback_t enemy; + /* Free-text description of the HLZ itself + (e.g. "Primary HLZ is soccer field"). */ + pb_callback_t hlz_remarks; + /* Per-patient clinical records. Each entry is one patient's ZMIST card + (Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable — + a mass-casualty event can carry 1-6 entries in practice, limited by + the 237 B LoRa MTU. */ + pb_callback_t zmist; +} meshtastic_CasevacReport; + +/* Per-patient clinical summary record — one entry per patient in a CASEVAC. + Maps directly to ATAK's child element inside . + All fields are optional free-text; senders populate what they have. */ +typedef struct _meshtastic_ZMistEntry { + /* Patient identifier / sequence label (e.g. "ZMIST-1", "ZMIST-2"). */ + pb_callback_t title; + /* Zap number — unique patient tracking ID (often a terse code like + "Gunshot" or a serial). */ + pb_callback_t z; + /* Mechanism of injury (e.g. "Penetrating trauma", "Blast injury"). */ + pb_callback_t m; + /* Injuries observed (e.g. "Left thigh", "Concussion"). */ + pb_callback_t i; + /* Signs / vital stats (e.g. "Stable", "Priority", "BP 110/70"). */ + pb_callback_t s; + /* Treatment given (e.g. "Tourniquet 1810Z", "O2 administered"). */ + pb_callback_t t; +} meshtastic_ZMistEntry; + +/* Emergency alert / 911 beacon (CoT types b-a-o-tbl, b-a-o-pan, b-a-o-opn, + b-a-o-can, b-a-o-c, b-a-g). + + Small, high-priority structured record. The CoT type string is still set + on cot_type_id so receivers that ignore payload_variant can still display + the alert from the enum alone; the typed fields let modern receivers show + the authoring unit and handle cancel-referencing without XML parsing. */ +typedef struct _meshtastic_EmergencyAlert { + /* Alert discriminator. */ + meshtastic_EmergencyAlert_Type type; + /* UID of the unit that raised the alert. Often the same as + TAKPacketV2.uid but can be a parent device uid when a tracker raises + an alert on behalf of a dismount. */ + char authoring_uid[48]; + /* For Type_Cancel: the uid of the alert being cancelled. Empty for + non-cancel alert types. */ + char cancel_reference_uid[48]; +} meshtastic_EmergencyAlert; + +/* Task / engage request (CoT type t-s). + + Mirrors ATAK's TaskCotReceiver / CotTaskBuilder workflow. The envelope + carries the task's originating uid (implicit requester), position, and + creation time; the fields below carry structured metadata the raw-detail + fallback currently loses. + + Fields are deliberately lean — this variant is closer to the MTU ceiling + than the others, so every string is capped in options. */ +typedef struct _meshtastic_TaskRequest { + /* Short tag for the task category (e.g. "engage", "observe", "recon", + "rescue"). Free text on the wire so ATAK-specific task taxonomies + don't need proto coordination; capped tight in options. */ + char task_type[12]; + /* UID of the target / map item being tasked. */ + char target_uid[32]; + /* UID of the assigned unit. Empty = unassigned / broadcast task. */ + char assignee_uid[32]; + meshtastic_TaskRequest_Priority priority; + meshtastic_TaskRequest_Status status; + /* Optional short note (reason, constraints, grid reference). Capped + tight in options to keep the worst-case under the LoRa MTU. */ + char note[48]; +} meshtastic_TaskRequest; + +/* Weather annotation from CoT detail element. + + Attaches to any TAKPacketV2 regardless of payload_variant — an Aircraft, + PLI, or Marker can all carry observed conditions at the emitting station. + ATAK-CIV ships an XSD for but no dedicated handler, so the + element round-trips through the generic detail pipeline; this message + promotes it to a first-class structured field. + + Target wire cost: ~6-8 bytes compressed with a fully populated instance. + + Named `TAKEnvironment` (not just `Environment`) because the bare name + collides with `SwiftUI.Environment` — every SwiftUI view in a consuming + iOS app uses the `@Environment` property wrapper, and importing the + generated proto module would make `Environment` ambiguous in every one + of those files. The `TAK` prefix matches the convention used by the + outer `TAKPacketV2` wrapper and is unambiguous across all target + languages (Swift, Kotlin, Python, TypeScript, C#). */ +typedef struct _meshtastic_TAKEnvironment { + /* Temperature in deci-degrees Celsius. 225 = 22.5°C. + Range covers -50°C to +50°C (-500 to +500) which spans every realistic + outdoor TAK deployment. sint32 because negative temps are common in + cold-weather ops. */ + int32_t temperature_c_x10; + /* Wind direction in whole degrees, 0-359. "Direction FROM" per + meteorological convention (matches CoT / ATAK). */ + uint32_t wind_direction_deg; + /* Wind speed in cm/s. Matches the unit of TAKPacketV2.speed for + consistency. 1200 = 12.00 m/s = ~27 mph. */ + uint32_t wind_speed_cm_s; +} meshtastic_TAKEnvironment; + +/* Sensor field-of-view cone from CoT detail element. + + Encodes the 8 geometry attributes that ATAK-CIV's SensorDetailHandler + reads from the wire; drops the 9 visual-styling attributes that are + receiver-side render hints (fovAlpha, fovRed/Green/Blue, strokeColor, + strokeWeight, displayMagneticReference, hideFov, fovLabels, rangeLines). + The receiving ATAK client restores those from its own defaults, same as + every other CoT carried over Meshtastic today. + + Attaches to any TAKPacketV2 — a PLI with a sensor on the operator's head, + an Aircraft with a FLIR turret, a Marker dropped on a UAV. + Target wire cost: ~7-14 bytes compressed (dominated by model string). */ +typedef struct _meshtastic_SensorFov { + meshtastic_SensorFov_SensorType type; + /* Azimuth in whole degrees, 0-359. "Pointing direction" of the cone axis, + measured clockwise from true north. Whole degrees match ATAK-CIV's + SensorDetailHandler default (270°) and save varint bytes over centi-deg. */ + uint32_t azimuth_deg; + /* Maximum range of the cone in meters. + Optional — if unset, receivers should use the ATAK-CIV default of 100m. */ + bool has_range_m; + uint32_t range_m; + /* Horizontal field of view in whole degrees (cone's angular width). + ATAK-CIV default is 45°. */ + uint32_t fov_horizontal_deg; + /* Vertical field of view in whole degrees. ATAK-CIV default is 45°. + Optional — a value of 0 means "not set / use horizontal FOV". */ + uint32_t fov_vertical_deg; + /* Elevation angle in whole degrees. Positive = up, negative = down. + Range -90 to +90. sint32 for varint efficiency on small negatives. */ + int32_t elevation_deg; + /* Roll (camera tilt) in whole degrees, -180 to +180. + Optional — use 0 if the sensor doesn't track roll. */ + int32_t roll_deg; + /* Free-form device model identifier, e.g. "FLIR-Boson-640", "SEEK". + Optional — empty string means "unknown model" (ATAK-CIV default). */ + pb_callback_t model; +} meshtastic_SensorFov; + +typedef PB_BYTES_ARRAY_T(220) meshtastic_TAKPacketV2_raw_detail_t; +/* ATAK v2 packet with expanded CoT field support and zstd dictionary compression. + Sent on ATAK_PLUGIN_V2 port. The wire payload is: + [1 byte flags][zstd-compressed TAKPacketV2 protobuf] + Flags byte: bits 0-5 = dictionary ID, bits 6-7 = reserved. */ +typedef struct _meshtastic_TAKPacketV2 { + /* Well-known CoT event type enum. + Use CotType_Other with cot_type_str for unknown types. */ + meshtastic_CotType cot_type_id; + /* How the coordinates were generated */ + meshtastic_CotHow how; + /* Callsign */ + char callsign[120]; + /* Team color assignment */ + meshtastic_Team team; + /* Role of the group member */ + meshtastic_MemberRole role; + /* Latitude, multiply by 1e-7 to get degrees in floating point */ + int32_t latitude_i; + /* Longitude, multiply by 1e-7 to get degrees in floating point */ + int32_t longitude_i; + /* Altitude in meters (HAE) */ + int32_t altitude; + /* Speed in cm/s */ + uint32_t speed; + /* Course in degrees * 100 (0-36000) */ + uint16_t course; + /* Battery level 0-100 */ + uint8_t battery; + /* Geopoint source */ + meshtastic_GeoPointSource geo_src; + /* Altitude source */ + meshtastic_GeoPointSource alt_src; + /* Device UID (UUID string or device ID like "ANDROID-xxxx") */ + char uid[48]; + /* Device callsign */ + char device_callsign[120]; + /* Stale time as seconds offset from event time */ + uint16_t stale_seconds; + /* TAK client version string */ + char tak_version[64]; + /* TAK device model */ + char tak_device[32]; + /* TAK platform (ATAK-CIV, WebTAK, etc.) */ + char tak_platform[32]; + /* TAK OS version */ + char tak_os[16]; + /* Connection endpoint */ + char endpoint[32]; + /* Phone number */ + char phone[20]; + /* CoT event type string, only populated when cot_type_id is CotType_Other */ + char cot_type_str[32]; + /* Optional remarks / free-text annotation from the element. + Populated for non-GeoChat payload types (shapes, markers, routes, etc.) + when the original CoT event carried non-empty remarks text. + GeoChat messages carry their text in GeoChat.message instead. + Empty string (proto3 default) means no remarks were present. */ + pb_callback_t remarks; + /* Observed weather conditions (temperature, wind). From . + Type is `TAKEnvironment`, not `Environment`, to avoid colliding with + SwiftUI's `@Environment` property wrapper in iOS consumers. */ + bool has_environment; + meshtastic_TAKEnvironment environment; + /* Sensor field-of-view cone (camera, FLIR, laser, etc.). From . */ + bool has_sensor_fov; + meshtastic_SensorFov sensor_fov; + pb_size_t which_payload_variant; + union { + /* Position report (true = PLI, no extra fields beyond the common ones above) */ + bool pli; + /* ATAK GeoChat message */ + meshtastic_GeoChat chat; + /* Aircraft track data (ADS-B, military air) */ + meshtastic_AircraftTrack aircraft; + /* Generic CoT detail XML for unmapped types. Kept as a fallback for CoT + types not yet promoted to a typed variant; drawings, markers, ranging + tools, and routes have dedicated variants below and should not land here. */ + meshtastic_TAKPacketV2_raw_detail_t raw_detail; + /* User-drawn tactical graphic: circle, rectangle, polygon, polyline, + telestration, ranging circle, or bullseye. See DrawnShape. */ + meshtastic_DrawnShape shape; + /* Fixed point of interest: spot marker, waypoint, checkpoint, 2525 + symbol, or custom icon. See Marker. */ + meshtastic_Marker marker; + /* Range and bearing measurement line. See RangeAndBearing. */ + meshtastic_RangeAndBearing rab; + /* Named route with ordered waypoints and control points. See Route. */ + meshtastic_Route route; + /* 9-line MEDEVAC request. See CasevacReport. */ + meshtastic_CasevacReport casevac; + /* Emergency beacon / 911 alert. See EmergencyAlert. */ + meshtastic_EmergencyAlert emergency; + /* Task / engage request. See TaskRequest. */ + meshtastic_TaskRequest task; + } payload_variant; +} meshtastic_TAKPacketV2; + #ifdef __cplusplus extern "C" { @@ -160,7 +1180,72 @@ extern "C" { #define _meshtastic_MemberRole_MAX meshtastic_MemberRole_K9 #define _meshtastic_MemberRole_ARRAYSIZE ((meshtastic_MemberRole)(meshtastic_MemberRole_K9+1)) +#define _meshtastic_CotHow_MIN meshtastic_CotHow_CotHow_Unspecified +#define _meshtastic_CotHow_MAX meshtastic_CotHow_CotHow_m_s +#define _meshtastic_CotHow_ARRAYSIZE ((meshtastic_CotHow)(meshtastic_CotHow_CotHow_m_s+1)) +#define _meshtastic_CotType_MIN meshtastic_CotType_CotType_Other +#define _meshtastic_CotType_MAX meshtastic_CotType_CotType_t_s +#define _meshtastic_CotType_ARRAYSIZE ((meshtastic_CotType)(meshtastic_CotType_CotType_t_s+1)) + +#define _meshtastic_GeoPointSource_MIN meshtastic_GeoPointSource_GeoPointSource_Unspecified +#define _meshtastic_GeoPointSource_MAX meshtastic_GeoPointSource_GeoPointSource_NETWORK +#define _meshtastic_GeoPointSource_ARRAYSIZE ((meshtastic_GeoPointSource)(meshtastic_GeoPointSource_GeoPointSource_NETWORK+1)) + +#define _meshtastic_GeoChat_ReceiptType_MIN meshtastic_GeoChat_ReceiptType_ReceiptType_None +#define _meshtastic_GeoChat_ReceiptType_MAX meshtastic_GeoChat_ReceiptType_ReceiptType_Read +#define _meshtastic_GeoChat_ReceiptType_ARRAYSIZE ((meshtastic_GeoChat_ReceiptType)(meshtastic_GeoChat_ReceiptType_ReceiptType_Read+1)) + +#define _meshtastic_DrawnShape_Kind_MIN meshtastic_DrawnShape_Kind_Kind_Unspecified +#define _meshtastic_DrawnShape_Kind_MAX meshtastic_DrawnShape_Kind_Kind_Vehicle3D +#define _meshtastic_DrawnShape_Kind_ARRAYSIZE ((meshtastic_DrawnShape_Kind)(meshtastic_DrawnShape_Kind_Kind_Vehicle3D+1)) + +#define _meshtastic_DrawnShape_StyleMode_MIN meshtastic_DrawnShape_StyleMode_StyleMode_Unspecified +#define _meshtastic_DrawnShape_StyleMode_MAX meshtastic_DrawnShape_StyleMode_StyleMode_StrokeAndFill +#define _meshtastic_DrawnShape_StyleMode_ARRAYSIZE ((meshtastic_DrawnShape_StyleMode)(meshtastic_DrawnShape_StyleMode_StyleMode_StrokeAndFill+1)) + +#define _meshtastic_Marker_Kind_MIN meshtastic_Marker_Kind_Kind_Unspecified +#define _meshtastic_Marker_Kind_MAX meshtastic_Marker_Kind_Kind_ImageMarker +#define _meshtastic_Marker_Kind_ARRAYSIZE ((meshtastic_Marker_Kind)(meshtastic_Marker_Kind_Kind_ImageMarker+1)) + +#define _meshtastic_Route_Method_MIN meshtastic_Route_Method_Method_Unspecified +#define _meshtastic_Route_Method_MAX meshtastic_Route_Method_Method_Watercraft +#define _meshtastic_Route_Method_ARRAYSIZE ((meshtastic_Route_Method)(meshtastic_Route_Method_Method_Watercraft+1)) + +#define _meshtastic_Route_Direction_MIN meshtastic_Route_Direction_Direction_Unspecified +#define _meshtastic_Route_Direction_MAX meshtastic_Route_Direction_Direction_Exfil +#define _meshtastic_Route_Direction_ARRAYSIZE ((meshtastic_Route_Direction)(meshtastic_Route_Direction_Direction_Exfil+1)) + +#define _meshtastic_CasevacReport_Precedence_MIN meshtastic_CasevacReport_Precedence_Precedence_Unspecified +#define _meshtastic_CasevacReport_Precedence_MAX meshtastic_CasevacReport_Precedence_Precedence_Convenience +#define _meshtastic_CasevacReport_Precedence_ARRAYSIZE ((meshtastic_CasevacReport_Precedence)(meshtastic_CasevacReport_Precedence_Precedence_Convenience+1)) + +#define _meshtastic_CasevacReport_HlzMarking_MIN meshtastic_CasevacReport_HlzMarking_HlzMarking_Unspecified +#define _meshtastic_CasevacReport_HlzMarking_MAX meshtastic_CasevacReport_HlzMarking_HlzMarking_Other +#define _meshtastic_CasevacReport_HlzMarking_ARRAYSIZE ((meshtastic_CasevacReport_HlzMarking)(meshtastic_CasevacReport_HlzMarking_HlzMarking_Other+1)) + +#define _meshtastic_CasevacReport_Security_MIN meshtastic_CasevacReport_Security_Security_Unspecified +#define _meshtastic_CasevacReport_Security_MAX meshtastic_CasevacReport_Security_Security_EnemyInArmedContact +#define _meshtastic_CasevacReport_Security_ARRAYSIZE ((meshtastic_CasevacReport_Security)(meshtastic_CasevacReport_Security_Security_EnemyInArmedContact+1)) + +#define _meshtastic_EmergencyAlert_Type_MIN meshtastic_EmergencyAlert_Type_Type_Unspecified +#define _meshtastic_EmergencyAlert_Type_MAX meshtastic_EmergencyAlert_Type_Type_Cancel +#define _meshtastic_EmergencyAlert_Type_ARRAYSIZE ((meshtastic_EmergencyAlert_Type)(meshtastic_EmergencyAlert_Type_Type_Cancel+1)) + +#define _meshtastic_TaskRequest_Priority_MIN meshtastic_TaskRequest_Priority_Priority_Unspecified +#define _meshtastic_TaskRequest_Priority_MAX meshtastic_TaskRequest_Priority_Priority_Critical +#define _meshtastic_TaskRequest_Priority_ARRAYSIZE ((meshtastic_TaskRequest_Priority)(meshtastic_TaskRequest_Priority_Priority_Critical+1)) + +#define _meshtastic_TaskRequest_Status_MIN meshtastic_TaskRequest_Status_Status_Unspecified +#define _meshtastic_TaskRequest_Status_MAX meshtastic_TaskRequest_Status_Status_Cancelled +#define _meshtastic_TaskRequest_Status_ARRAYSIZE ((meshtastic_TaskRequest_Status)(meshtastic_TaskRequest_Status_Status_Cancelled+1)) + +#define _meshtastic_SensorFov_SensorType_MIN meshtastic_SensorFov_SensorType_SensorType_Unspecified +#define _meshtastic_SensorFov_SensorType_MAX meshtastic_SensorFov_SensorType_SensorType_Other +#define _meshtastic_SensorFov_SensorType_ARRAYSIZE ((meshtastic_SensorFov_SensorType)(meshtastic_SensorFov_SensorType_SensorType_Other+1)) + + +#define meshtastic_GeoChat_receipt_type_ENUMTYPE meshtastic_GeoChat_ReceiptType #define meshtastic_Group_role_ENUMTYPE meshtastic_MemberRole #define meshtastic_Group_team_ENUMTYPE meshtastic_Team @@ -169,24 +1254,90 @@ extern "C" { + +#define meshtastic_DrawnShape_kind_ENUMTYPE meshtastic_DrawnShape_Kind +#define meshtastic_DrawnShape_style_ENUMTYPE meshtastic_DrawnShape_StyleMode +#define meshtastic_DrawnShape_stroke_color_ENUMTYPE meshtastic_Team +#define meshtastic_DrawnShape_fill_color_ENUMTYPE meshtastic_Team + +#define meshtastic_Marker_kind_ENUMTYPE meshtastic_Marker_Kind +#define meshtastic_Marker_color_ENUMTYPE meshtastic_Team + +#define meshtastic_RangeAndBearing_stroke_color_ENUMTYPE meshtastic_Team + +#define meshtastic_Route_method_ENUMTYPE meshtastic_Route_Method +#define meshtastic_Route_direction_ENUMTYPE meshtastic_Route_Direction + + +#define meshtastic_CasevacReport_precedence_ENUMTYPE meshtastic_CasevacReport_Precedence +#define meshtastic_CasevacReport_security_ENUMTYPE meshtastic_CasevacReport_Security +#define meshtastic_CasevacReport_hlz_marking_ENUMTYPE meshtastic_CasevacReport_HlzMarking + + +#define meshtastic_EmergencyAlert_type_ENUMTYPE meshtastic_EmergencyAlert_Type + +#define meshtastic_TaskRequest_priority_ENUMTYPE meshtastic_TaskRequest_Priority +#define meshtastic_TaskRequest_status_ENUMTYPE meshtastic_TaskRequest_Status + + +#define meshtastic_SensorFov_type_ENUMTYPE meshtastic_SensorFov_SensorType + +#define meshtastic_TAKPacketV2_cot_type_id_ENUMTYPE meshtastic_CotType +#define meshtastic_TAKPacketV2_how_ENUMTYPE meshtastic_CotHow +#define meshtastic_TAKPacketV2_team_ENUMTYPE meshtastic_Team +#define meshtastic_TAKPacketV2_role_ENUMTYPE meshtastic_MemberRole +#define meshtastic_TAKPacketV2_geo_src_ENUMTYPE meshtastic_GeoPointSource +#define meshtastic_TAKPacketV2_alt_src_ENUMTYPE meshtastic_GeoPointSource + + /* Initializer values for message structs */ #define meshtastic_TAKPacket_init_default {0, false, meshtastic_Contact_init_default, false, meshtastic_Group_init_default, false, meshtastic_Status_init_default, 0, {meshtastic_PLI_init_default}} -#define meshtastic_GeoChat_init_default {"", false, "", false, ""} +#define meshtastic_GeoChat_init_default {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN} #define meshtastic_Group_init_default {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN} #define meshtastic_Status_init_default {0} #define meshtastic_Contact_init_default {"", ""} #define meshtastic_PLI_init_default {0, 0, 0, 0, 0} +#define meshtastic_AircraftTrack_init_default {"", "", "", "", 0, "", 0, 0, ""} +#define meshtastic_CotGeoPoint_init_default {0, 0} +#define meshtastic_DrawnShape_init_default {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, {meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default}, 0, 0, 0, 0, ""} +#define meshtastic_Marker_init_default {_meshtastic_Marker_Kind_MIN, _meshtastic_Team_MIN, 0, 0, "", "", "", ""} +#define meshtastic_RangeAndBearing_init_default {false, meshtastic_CotGeoPoint_init_default, "", 0, 0, _meshtastic_Team_MIN, 0, 0} +#define meshtastic_Route_init_default {_meshtastic_Route_Method_MIN, _meshtastic_Route_Direction_MIN, "", 0, 0, {meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default}, 0} +#define meshtastic_Route_Link_init_default {false, meshtastic_CotGeoPoint_init_default, "", "", 0} +#define meshtastic_CasevacReport_init_default {_meshtastic_CasevacReport_Precedence_MIN, 0, 0, 0, _meshtastic_CasevacReport_Security_MIN, _meshtastic_CasevacReport_HlzMarking_MIN, "", 0, 0, 0, 0, 0, 0, 0, "", {{NULL}, NULL}, {{NULL}, NULL}, 0, 0, 0, 0, 0, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} +#define meshtastic_ZMistEntry_init_default {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} +#define meshtastic_EmergencyAlert_init_default {_meshtastic_EmergencyAlert_Type_MIN, "", ""} +#define meshtastic_TaskRequest_init_default {"", "", "", _meshtastic_TaskRequest_Priority_MIN, _meshtastic_TaskRequest_Status_MIN, ""} +#define meshtastic_TAKEnvironment_init_default {0, 0, 0} +#define meshtastic_SensorFov_init_default {_meshtastic_SensorFov_SensorType_MIN, 0, false, 0, 0, 0, 0, 0, {{NULL}, NULL}} +#define meshtastic_TAKPacketV2_init_default {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_default, false, meshtastic_SensorFov_init_default, 0, {0}} #define meshtastic_TAKPacket_init_zero {0, false, meshtastic_Contact_init_zero, false, meshtastic_Group_init_zero, false, meshtastic_Status_init_zero, 0, {meshtastic_PLI_init_zero}} -#define meshtastic_GeoChat_init_zero {"", false, "", false, ""} +#define meshtastic_GeoChat_init_zero {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN} #define meshtastic_Group_init_zero {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN} #define meshtastic_Status_init_zero {0} #define meshtastic_Contact_init_zero {"", ""} #define meshtastic_PLI_init_zero {0, 0, 0, 0, 0} +#define meshtastic_AircraftTrack_init_zero {"", "", "", "", 0, "", 0, 0, ""} +#define meshtastic_CotGeoPoint_init_zero {0, 0} +#define meshtastic_DrawnShape_init_zero {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, {meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero}, 0, 0, 0, 0, ""} +#define meshtastic_Marker_init_zero {_meshtastic_Marker_Kind_MIN, _meshtastic_Team_MIN, 0, 0, "", "", "", ""} +#define meshtastic_RangeAndBearing_init_zero {false, meshtastic_CotGeoPoint_init_zero, "", 0, 0, _meshtastic_Team_MIN, 0, 0} +#define meshtastic_Route_init_zero {_meshtastic_Route_Method_MIN, _meshtastic_Route_Direction_MIN, "", 0, 0, {meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero}, 0} +#define meshtastic_Route_Link_init_zero {false, meshtastic_CotGeoPoint_init_zero, "", "", 0} +#define meshtastic_CasevacReport_init_zero {_meshtastic_CasevacReport_Precedence_MIN, 0, 0, 0, _meshtastic_CasevacReport_Security_MIN, _meshtastic_CasevacReport_HlzMarking_MIN, "", 0, 0, 0, 0, 0, 0, 0, "", {{NULL}, NULL}, {{NULL}, NULL}, 0, 0, 0, 0, 0, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} +#define meshtastic_ZMistEntry_init_zero {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} +#define meshtastic_EmergencyAlert_init_zero {_meshtastic_EmergencyAlert_Type_MIN, "", ""} +#define meshtastic_TaskRequest_init_zero {"", "", "", _meshtastic_TaskRequest_Priority_MIN, _meshtastic_TaskRequest_Status_MIN, ""} +#define meshtastic_TAKEnvironment_init_zero {0, 0, 0} +#define meshtastic_SensorFov_init_zero {_meshtastic_SensorFov_SensorType_MIN, 0, false, 0, 0, 0, 0, 0, {{NULL}, NULL}} +#define meshtastic_TAKPacketV2_init_zero {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_zero, false, meshtastic_SensorFov_init_zero, 0, {0}} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_GeoChat_message_tag 1 #define meshtastic_GeoChat_to_tag 2 #define meshtastic_GeoChat_to_callsign_tag 3 +#define meshtastic_GeoChat_receipt_for_uid_tag 4 +#define meshtastic_GeoChat_receipt_type_tag 5 #define meshtastic_Group_role_tag 1 #define meshtastic_Group_team_tag 2 #define meshtastic_Status_battery_tag 1 @@ -204,6 +1355,155 @@ extern "C" { #define meshtastic_TAKPacket_pli_tag 5 #define meshtastic_TAKPacket_chat_tag 6 #define meshtastic_TAKPacket_detail_tag 7 +#define meshtastic_AircraftTrack_icao_tag 1 +#define meshtastic_AircraftTrack_registration_tag 2 +#define meshtastic_AircraftTrack_flight_tag 3 +#define meshtastic_AircraftTrack_aircraft_type_tag 4 +#define meshtastic_AircraftTrack_squawk_tag 5 +#define meshtastic_AircraftTrack_category_tag 6 +#define meshtastic_AircraftTrack_rssi_x10_tag 7 +#define meshtastic_AircraftTrack_gps_tag 8 +#define meshtastic_AircraftTrack_cot_host_id_tag 9 +#define meshtastic_CotGeoPoint_lat_delta_i_tag 1 +#define meshtastic_CotGeoPoint_lon_delta_i_tag 2 +#define meshtastic_DrawnShape_kind_tag 1 +#define meshtastic_DrawnShape_style_tag 2 +#define meshtastic_DrawnShape_major_cm_tag 3 +#define meshtastic_DrawnShape_minor_cm_tag 4 +#define meshtastic_DrawnShape_angle_deg_tag 5 +#define meshtastic_DrawnShape_stroke_color_tag 6 +#define meshtastic_DrawnShape_stroke_argb_tag 7 +#define meshtastic_DrawnShape_stroke_weight_x10_tag 8 +#define meshtastic_DrawnShape_fill_color_tag 9 +#define meshtastic_DrawnShape_fill_argb_tag 10 +#define meshtastic_DrawnShape_labels_on_tag 11 +#define meshtastic_DrawnShape_vertices_tag 12 +#define meshtastic_DrawnShape_truncated_tag 13 +#define meshtastic_DrawnShape_bullseye_distance_dm_tag 14 +#define meshtastic_DrawnShape_bullseye_bearing_ref_tag 15 +#define meshtastic_DrawnShape_bullseye_flags_tag 16 +#define meshtastic_DrawnShape_bullseye_uid_ref_tag 17 +#define meshtastic_Marker_kind_tag 1 +#define meshtastic_Marker_color_tag 2 +#define meshtastic_Marker_color_argb_tag 3 +#define meshtastic_Marker_readiness_tag 4 +#define meshtastic_Marker_parent_uid_tag 5 +#define meshtastic_Marker_parent_type_tag 6 +#define meshtastic_Marker_parent_callsign_tag 7 +#define meshtastic_Marker_iconset_tag 8 +#define meshtastic_RangeAndBearing_anchor_tag 1 +#define meshtastic_RangeAndBearing_anchor_uid_tag 2 +#define meshtastic_RangeAndBearing_range_cm_tag 3 +#define meshtastic_RangeAndBearing_bearing_cdeg_tag 4 +#define meshtastic_RangeAndBearing_stroke_color_tag 5 +#define meshtastic_RangeAndBearing_stroke_argb_tag 6 +#define meshtastic_RangeAndBearing_stroke_weight_x10_tag 7 +#define meshtastic_Route_Link_point_tag 1 +#define meshtastic_Route_Link_uid_tag 2 +#define meshtastic_Route_Link_callsign_tag 3 +#define meshtastic_Route_Link_link_type_tag 4 +#define meshtastic_Route_method_tag 1 +#define meshtastic_Route_direction_tag 2 +#define meshtastic_Route_prefix_tag 3 +#define meshtastic_Route_stroke_weight_x10_tag 4 +#define meshtastic_Route_links_tag 5 +#define meshtastic_Route_truncated_tag 6 +#define meshtastic_CasevacReport_precedence_tag 1 +#define meshtastic_CasevacReport_equipment_flags_tag 2 +#define meshtastic_CasevacReport_litter_patients_tag 3 +#define meshtastic_CasevacReport_ambulatory_patients_tag 4 +#define meshtastic_CasevacReport_security_tag 5 +#define meshtastic_CasevacReport_hlz_marking_tag 6 +#define meshtastic_CasevacReport_zone_marker_tag 7 +#define meshtastic_CasevacReport_us_military_tag 8 +#define meshtastic_CasevacReport_us_civilian_tag 9 +#define meshtastic_CasevacReport_non_us_military_tag 10 +#define meshtastic_CasevacReport_non_us_civilian_tag 11 +#define meshtastic_CasevacReport_epw_tag 12 +#define meshtastic_CasevacReport_child_tag 13 +#define meshtastic_CasevacReport_terrain_flags_tag 14 +#define meshtastic_CasevacReport_frequency_tag 15 +#define meshtastic_CasevacReport_title_tag 16 +#define meshtastic_CasevacReport_medline_remarks_tag 17 +#define meshtastic_CasevacReport_urgent_count_tag 18 +#define meshtastic_CasevacReport_urgent_surgical_count_tag 19 +#define meshtastic_CasevacReport_priority_count_tag 20 +#define meshtastic_CasevacReport_routine_count_tag 21 +#define meshtastic_CasevacReport_convenience_count_tag 22 +#define meshtastic_CasevacReport_equipment_detail_tag 23 +#define meshtastic_CasevacReport_zone_protected_coord_tag 24 +#define meshtastic_CasevacReport_terrain_slope_dir_tag 25 +#define meshtastic_CasevacReport_terrain_other_detail_tag 26 +#define meshtastic_CasevacReport_marked_by_tag 27 +#define meshtastic_CasevacReport_obstacles_tag 28 +#define meshtastic_CasevacReport_winds_are_from_tag 29 +#define meshtastic_CasevacReport_friendlies_tag 30 +#define meshtastic_CasevacReport_enemy_tag 31 +#define meshtastic_CasevacReport_hlz_remarks_tag 32 +#define meshtastic_CasevacReport_zmist_tag 33 +#define meshtastic_ZMistEntry_title_tag 1 +#define meshtastic_ZMistEntry_z_tag 2 +#define meshtastic_ZMistEntry_m_tag 3 +#define meshtastic_ZMistEntry_i_tag 4 +#define meshtastic_ZMistEntry_s_tag 5 +#define meshtastic_ZMistEntry_t_tag 6 +#define meshtastic_EmergencyAlert_type_tag 1 +#define meshtastic_EmergencyAlert_authoring_uid_tag 2 +#define meshtastic_EmergencyAlert_cancel_reference_uid_tag 3 +#define meshtastic_TaskRequest_task_type_tag 1 +#define meshtastic_TaskRequest_target_uid_tag 2 +#define meshtastic_TaskRequest_assignee_uid_tag 3 +#define meshtastic_TaskRequest_priority_tag 4 +#define meshtastic_TaskRequest_status_tag 5 +#define meshtastic_TaskRequest_note_tag 6 +#define meshtastic_TAKEnvironment_temperature_c_x10_tag 1 +#define meshtastic_TAKEnvironment_wind_direction_deg_tag 2 +#define meshtastic_TAKEnvironment_wind_speed_cm_s_tag 3 +#define meshtastic_SensorFov_type_tag 1 +#define meshtastic_SensorFov_azimuth_deg_tag 2 +#define meshtastic_SensorFov_range_m_tag 3 +#define meshtastic_SensorFov_fov_horizontal_deg_tag 4 +#define meshtastic_SensorFov_fov_vertical_deg_tag 5 +#define meshtastic_SensorFov_elevation_deg_tag 6 +#define meshtastic_SensorFov_roll_deg_tag 7 +#define meshtastic_SensorFov_model_tag 8 +#define meshtastic_TAKPacketV2_cot_type_id_tag 1 +#define meshtastic_TAKPacketV2_how_tag 2 +#define meshtastic_TAKPacketV2_callsign_tag 3 +#define meshtastic_TAKPacketV2_team_tag 4 +#define meshtastic_TAKPacketV2_role_tag 5 +#define meshtastic_TAKPacketV2_latitude_i_tag 6 +#define meshtastic_TAKPacketV2_longitude_i_tag 7 +#define meshtastic_TAKPacketV2_altitude_tag 8 +#define meshtastic_TAKPacketV2_speed_tag 9 +#define meshtastic_TAKPacketV2_course_tag 10 +#define meshtastic_TAKPacketV2_battery_tag 11 +#define meshtastic_TAKPacketV2_geo_src_tag 12 +#define meshtastic_TAKPacketV2_alt_src_tag 13 +#define meshtastic_TAKPacketV2_uid_tag 14 +#define meshtastic_TAKPacketV2_device_callsign_tag 15 +#define meshtastic_TAKPacketV2_stale_seconds_tag 16 +#define meshtastic_TAKPacketV2_tak_version_tag 17 +#define meshtastic_TAKPacketV2_tak_device_tag 18 +#define meshtastic_TAKPacketV2_tak_platform_tag 19 +#define meshtastic_TAKPacketV2_tak_os_tag 20 +#define meshtastic_TAKPacketV2_endpoint_tag 21 +#define meshtastic_TAKPacketV2_phone_tag 22 +#define meshtastic_TAKPacketV2_cot_type_str_tag 23 +#define meshtastic_TAKPacketV2_remarks_tag 24 +#define meshtastic_TAKPacketV2_environment_tag 25 +#define meshtastic_TAKPacketV2_sensor_fov_tag 26 +#define meshtastic_TAKPacketV2_pli_tag 30 +#define meshtastic_TAKPacketV2_chat_tag 31 +#define meshtastic_TAKPacketV2_aircraft_tag 32 +#define meshtastic_TAKPacketV2_raw_detail_tag 33 +#define meshtastic_TAKPacketV2_shape_tag 34 +#define meshtastic_TAKPacketV2_marker_tag 35 +#define meshtastic_TAKPacketV2_rab_tag 36 +#define meshtastic_TAKPacketV2_route_tag 37 +#define meshtastic_TAKPacketV2_casevac_tag 38 +#define meshtastic_TAKPacketV2_emergency_tag 39 +#define meshtastic_TAKPacketV2_task_tag 40 /* Struct field encoding specification for nanopb */ #define meshtastic_TAKPacket_FIELDLIST(X, a) \ @@ -225,7 +1525,9 @@ X(a, STATIC, ONEOF, BYTES, (payload_variant,detail,payload_variant.detai #define meshtastic_GeoChat_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, message, 1) \ X(a, STATIC, OPTIONAL, STRING, to, 2) \ -X(a, STATIC, OPTIONAL, STRING, to_callsign, 3) +X(a, STATIC, OPTIONAL, STRING, to_callsign, 3) \ +X(a, STATIC, SINGULAR, STRING, receipt_for_uid, 4) \ +X(a, STATIC, SINGULAR, UENUM, receipt_type, 5) #define meshtastic_GeoChat_CALLBACK NULL #define meshtastic_GeoChat_DEFAULT NULL @@ -255,12 +1557,247 @@ X(a, STATIC, SINGULAR, UINT32, course, 5) #define meshtastic_PLI_CALLBACK NULL #define meshtastic_PLI_DEFAULT NULL +#define meshtastic_AircraftTrack_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, icao, 1) \ +X(a, STATIC, SINGULAR, STRING, registration, 2) \ +X(a, STATIC, SINGULAR, STRING, flight, 3) \ +X(a, STATIC, SINGULAR, STRING, aircraft_type, 4) \ +X(a, STATIC, SINGULAR, UINT32, squawk, 5) \ +X(a, STATIC, SINGULAR, STRING, category, 6) \ +X(a, STATIC, SINGULAR, SINT32, rssi_x10, 7) \ +X(a, STATIC, SINGULAR, BOOL, gps, 8) \ +X(a, STATIC, SINGULAR, STRING, cot_host_id, 9) +#define meshtastic_AircraftTrack_CALLBACK NULL +#define meshtastic_AircraftTrack_DEFAULT NULL + +#define meshtastic_CotGeoPoint_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, SINT32, lat_delta_i, 1) \ +X(a, STATIC, SINGULAR, SINT32, lon_delta_i, 2) +#define meshtastic_CotGeoPoint_CALLBACK NULL +#define meshtastic_CotGeoPoint_DEFAULT NULL + +#define meshtastic_DrawnShape_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, kind, 1) \ +X(a, STATIC, SINGULAR, UENUM, style, 2) \ +X(a, STATIC, SINGULAR, UINT32, major_cm, 3) \ +X(a, STATIC, SINGULAR, UINT32, minor_cm, 4) \ +X(a, STATIC, SINGULAR, UINT32, angle_deg, 5) \ +X(a, STATIC, SINGULAR, UENUM, stroke_color, 6) \ +X(a, STATIC, SINGULAR, FIXED32, stroke_argb, 7) \ +X(a, STATIC, SINGULAR, UINT32, stroke_weight_x10, 8) \ +X(a, STATIC, SINGULAR, UENUM, fill_color, 9) \ +X(a, STATIC, SINGULAR, FIXED32, fill_argb, 10) \ +X(a, STATIC, SINGULAR, BOOL, labels_on, 11) \ +X(a, STATIC, REPEATED, MESSAGE, vertices, 12) \ +X(a, STATIC, SINGULAR, BOOL, truncated, 13) \ +X(a, STATIC, SINGULAR, UINT32, bullseye_distance_dm, 14) \ +X(a, STATIC, SINGULAR, UINT32, bullseye_bearing_ref, 15) \ +X(a, STATIC, SINGULAR, UINT32, bullseye_flags, 16) \ +X(a, STATIC, SINGULAR, STRING, bullseye_uid_ref, 17) +#define meshtastic_DrawnShape_CALLBACK NULL +#define meshtastic_DrawnShape_DEFAULT NULL +#define meshtastic_DrawnShape_vertices_MSGTYPE meshtastic_CotGeoPoint + +#define meshtastic_Marker_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, kind, 1) \ +X(a, STATIC, SINGULAR, UENUM, color, 2) \ +X(a, STATIC, SINGULAR, FIXED32, color_argb, 3) \ +X(a, STATIC, SINGULAR, BOOL, readiness, 4) \ +X(a, STATIC, SINGULAR, STRING, parent_uid, 5) \ +X(a, STATIC, SINGULAR, STRING, parent_type, 6) \ +X(a, STATIC, SINGULAR, STRING, parent_callsign, 7) \ +X(a, STATIC, SINGULAR, STRING, iconset, 8) +#define meshtastic_Marker_CALLBACK NULL +#define meshtastic_Marker_DEFAULT NULL + +#define meshtastic_RangeAndBearing_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, anchor, 1) \ +X(a, STATIC, SINGULAR, STRING, anchor_uid, 2) \ +X(a, STATIC, SINGULAR, UINT32, range_cm, 3) \ +X(a, STATIC, SINGULAR, UINT32, bearing_cdeg, 4) \ +X(a, STATIC, SINGULAR, UENUM, stroke_color, 5) \ +X(a, STATIC, SINGULAR, FIXED32, stroke_argb, 6) \ +X(a, STATIC, SINGULAR, UINT32, stroke_weight_x10, 7) +#define meshtastic_RangeAndBearing_CALLBACK NULL +#define meshtastic_RangeAndBearing_DEFAULT NULL +#define meshtastic_RangeAndBearing_anchor_MSGTYPE meshtastic_CotGeoPoint + +#define meshtastic_Route_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, method, 1) \ +X(a, STATIC, SINGULAR, UENUM, direction, 2) \ +X(a, STATIC, SINGULAR, STRING, prefix, 3) \ +X(a, STATIC, SINGULAR, UINT32, stroke_weight_x10, 4) \ +X(a, STATIC, REPEATED, MESSAGE, links, 5) \ +X(a, STATIC, SINGULAR, BOOL, truncated, 6) +#define meshtastic_Route_CALLBACK NULL +#define meshtastic_Route_DEFAULT NULL +#define meshtastic_Route_links_MSGTYPE meshtastic_Route_Link + +#define meshtastic_Route_Link_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, point, 1) \ +X(a, STATIC, SINGULAR, STRING, uid, 2) \ +X(a, STATIC, SINGULAR, STRING, callsign, 3) \ +X(a, STATIC, SINGULAR, UINT32, link_type, 4) +#define meshtastic_Route_Link_CALLBACK NULL +#define meshtastic_Route_Link_DEFAULT NULL +#define meshtastic_Route_Link_point_MSGTYPE meshtastic_CotGeoPoint + +#define meshtastic_CasevacReport_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, precedence, 1) \ +X(a, STATIC, SINGULAR, UINT32, equipment_flags, 2) \ +X(a, STATIC, SINGULAR, UINT32, litter_patients, 3) \ +X(a, STATIC, SINGULAR, UINT32, ambulatory_patients, 4) \ +X(a, STATIC, SINGULAR, UENUM, security, 5) \ +X(a, STATIC, SINGULAR, UENUM, hlz_marking, 6) \ +X(a, STATIC, SINGULAR, STRING, zone_marker, 7) \ +X(a, STATIC, SINGULAR, UINT32, us_military, 8) \ +X(a, STATIC, SINGULAR, UINT32, us_civilian, 9) \ +X(a, STATIC, SINGULAR, UINT32, non_us_military, 10) \ +X(a, STATIC, SINGULAR, UINT32, non_us_civilian, 11) \ +X(a, STATIC, SINGULAR, UINT32, epw, 12) \ +X(a, STATIC, SINGULAR, UINT32, child, 13) \ +X(a, STATIC, SINGULAR, UINT32, terrain_flags, 14) \ +X(a, STATIC, SINGULAR, STRING, frequency, 15) \ +X(a, CALLBACK, SINGULAR, STRING, title, 16) \ +X(a, CALLBACK, SINGULAR, STRING, medline_remarks, 17) \ +X(a, STATIC, SINGULAR, UINT32, urgent_count, 18) \ +X(a, STATIC, SINGULAR, UINT32, urgent_surgical_count, 19) \ +X(a, STATIC, SINGULAR, UINT32, priority_count, 20) \ +X(a, STATIC, SINGULAR, UINT32, routine_count, 21) \ +X(a, STATIC, SINGULAR, UINT32, convenience_count, 22) \ +X(a, CALLBACK, SINGULAR, STRING, equipment_detail, 23) \ +X(a, CALLBACK, SINGULAR, STRING, zone_protected_coord, 24) \ +X(a, CALLBACK, SINGULAR, STRING, terrain_slope_dir, 25) \ +X(a, CALLBACK, SINGULAR, STRING, terrain_other_detail, 26) \ +X(a, CALLBACK, SINGULAR, STRING, marked_by, 27) \ +X(a, CALLBACK, SINGULAR, STRING, obstacles, 28) \ +X(a, CALLBACK, SINGULAR, STRING, winds_are_from, 29) \ +X(a, CALLBACK, SINGULAR, STRING, friendlies, 30) \ +X(a, CALLBACK, SINGULAR, STRING, enemy, 31) \ +X(a, CALLBACK, SINGULAR, STRING, hlz_remarks, 32) \ +X(a, CALLBACK, REPEATED, MESSAGE, zmist, 33) +#define meshtastic_CasevacReport_CALLBACK pb_default_field_callback +#define meshtastic_CasevacReport_DEFAULT NULL +#define meshtastic_CasevacReport_zmist_MSGTYPE meshtastic_ZMistEntry + +#define meshtastic_ZMistEntry_FIELDLIST(X, a) \ +X(a, CALLBACK, SINGULAR, STRING, title, 1) \ +X(a, CALLBACK, SINGULAR, STRING, z, 2) \ +X(a, CALLBACK, SINGULAR, STRING, m, 3) \ +X(a, CALLBACK, SINGULAR, STRING, i, 4) \ +X(a, CALLBACK, SINGULAR, STRING, s, 5) \ +X(a, CALLBACK, SINGULAR, STRING, t, 6) +#define meshtastic_ZMistEntry_CALLBACK pb_default_field_callback +#define meshtastic_ZMistEntry_DEFAULT NULL + +#define meshtastic_EmergencyAlert_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, type, 1) \ +X(a, STATIC, SINGULAR, STRING, authoring_uid, 2) \ +X(a, STATIC, SINGULAR, STRING, cancel_reference_uid, 3) +#define meshtastic_EmergencyAlert_CALLBACK NULL +#define meshtastic_EmergencyAlert_DEFAULT NULL + +#define meshtastic_TaskRequest_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, task_type, 1) \ +X(a, STATIC, SINGULAR, STRING, target_uid, 2) \ +X(a, STATIC, SINGULAR, STRING, assignee_uid, 3) \ +X(a, STATIC, SINGULAR, UENUM, priority, 4) \ +X(a, STATIC, SINGULAR, UENUM, status, 5) \ +X(a, STATIC, SINGULAR, STRING, note, 6) +#define meshtastic_TaskRequest_CALLBACK NULL +#define meshtastic_TaskRequest_DEFAULT NULL + +#define meshtastic_TAKEnvironment_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, SINT32, temperature_c_x10, 1) \ +X(a, STATIC, SINGULAR, UINT32, wind_direction_deg, 2) \ +X(a, STATIC, SINGULAR, UINT32, wind_speed_cm_s, 3) +#define meshtastic_TAKEnvironment_CALLBACK NULL +#define meshtastic_TAKEnvironment_DEFAULT NULL + +#define meshtastic_SensorFov_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, type, 1) \ +X(a, STATIC, SINGULAR, UINT32, azimuth_deg, 2) \ +X(a, STATIC, OPTIONAL, UINT32, range_m, 3) \ +X(a, STATIC, SINGULAR, UINT32, fov_horizontal_deg, 4) \ +X(a, STATIC, SINGULAR, UINT32, fov_vertical_deg, 5) \ +X(a, STATIC, SINGULAR, SINT32, elevation_deg, 6) \ +X(a, STATIC, SINGULAR, SINT32, roll_deg, 7) \ +X(a, CALLBACK, SINGULAR, STRING, model, 8) +#define meshtastic_SensorFov_CALLBACK pb_default_field_callback +#define meshtastic_SensorFov_DEFAULT NULL + +#define meshtastic_TAKPacketV2_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, cot_type_id, 1) \ +X(a, STATIC, SINGULAR, UENUM, how, 2) \ +X(a, STATIC, SINGULAR, STRING, callsign, 3) \ +X(a, STATIC, SINGULAR, UENUM, team, 4) \ +X(a, STATIC, SINGULAR, UENUM, role, 5) \ +X(a, STATIC, SINGULAR, SFIXED32, latitude_i, 6) \ +X(a, STATIC, SINGULAR, SFIXED32, longitude_i, 7) \ +X(a, STATIC, SINGULAR, SINT32, altitude, 8) \ +X(a, STATIC, SINGULAR, UINT32, speed, 9) \ +X(a, STATIC, SINGULAR, UINT32, course, 10) \ +X(a, STATIC, SINGULAR, UINT32, battery, 11) \ +X(a, STATIC, SINGULAR, UENUM, geo_src, 12) \ +X(a, STATIC, SINGULAR, UENUM, alt_src, 13) \ +X(a, STATIC, SINGULAR, STRING, uid, 14) \ +X(a, STATIC, SINGULAR, STRING, device_callsign, 15) \ +X(a, STATIC, SINGULAR, UINT32, stale_seconds, 16) \ +X(a, STATIC, SINGULAR, STRING, tak_version, 17) \ +X(a, STATIC, SINGULAR, STRING, tak_device, 18) \ +X(a, STATIC, SINGULAR, STRING, tak_platform, 19) \ +X(a, STATIC, SINGULAR, STRING, tak_os, 20) \ +X(a, STATIC, SINGULAR, STRING, endpoint, 21) \ +X(a, STATIC, SINGULAR, STRING, phone, 22) \ +X(a, STATIC, SINGULAR, STRING, cot_type_str, 23) \ +X(a, CALLBACK, SINGULAR, STRING, remarks, 24) \ +X(a, STATIC, OPTIONAL, MESSAGE, environment, 25) \ +X(a, STATIC, OPTIONAL, MESSAGE, sensor_fov, 26) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,pli,payload_variant.pli), 30) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,chat,payload_variant.chat), 31) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,aircraft,payload_variant.aircraft), 32) \ +X(a, STATIC, ONEOF, BYTES, (payload_variant,raw_detail,payload_variant.raw_detail), 33) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,shape,payload_variant.shape), 34) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,marker,payload_variant.marker), 35) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,rab,payload_variant.rab), 36) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,route,payload_variant.route), 37) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,casevac,payload_variant.casevac), 38) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,emergency,payload_variant.emergency), 39) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,task,payload_variant.task), 40) +#define meshtastic_TAKPacketV2_CALLBACK pb_default_field_callback +#define meshtastic_TAKPacketV2_DEFAULT NULL +#define meshtastic_TAKPacketV2_environment_MSGTYPE meshtastic_TAKEnvironment +#define meshtastic_TAKPacketV2_sensor_fov_MSGTYPE meshtastic_SensorFov +#define meshtastic_TAKPacketV2_payload_variant_chat_MSGTYPE meshtastic_GeoChat +#define meshtastic_TAKPacketV2_payload_variant_aircraft_MSGTYPE meshtastic_AircraftTrack +#define meshtastic_TAKPacketV2_payload_variant_shape_MSGTYPE meshtastic_DrawnShape +#define meshtastic_TAKPacketV2_payload_variant_marker_MSGTYPE meshtastic_Marker +#define meshtastic_TAKPacketV2_payload_variant_rab_MSGTYPE meshtastic_RangeAndBearing +#define meshtastic_TAKPacketV2_payload_variant_route_MSGTYPE meshtastic_Route +#define meshtastic_TAKPacketV2_payload_variant_casevac_MSGTYPE meshtastic_CasevacReport +#define meshtastic_TAKPacketV2_payload_variant_emergency_MSGTYPE meshtastic_EmergencyAlert +#define meshtastic_TAKPacketV2_payload_variant_task_MSGTYPE meshtastic_TaskRequest + extern const pb_msgdesc_t meshtastic_TAKPacket_msg; extern const pb_msgdesc_t meshtastic_GeoChat_msg; extern const pb_msgdesc_t meshtastic_Group_msg; extern const pb_msgdesc_t meshtastic_Status_msg; extern const pb_msgdesc_t meshtastic_Contact_msg; extern const pb_msgdesc_t meshtastic_PLI_msg; +extern const pb_msgdesc_t meshtastic_AircraftTrack_msg; +extern const pb_msgdesc_t meshtastic_CotGeoPoint_msg; +extern const pb_msgdesc_t meshtastic_DrawnShape_msg; +extern const pb_msgdesc_t meshtastic_Marker_msg; +extern const pb_msgdesc_t meshtastic_RangeAndBearing_msg; +extern const pb_msgdesc_t meshtastic_Route_msg; +extern const pb_msgdesc_t meshtastic_Route_Link_msg; +extern const pb_msgdesc_t meshtastic_CasevacReport_msg; +extern const pb_msgdesc_t meshtastic_ZMistEntry_msg; +extern const pb_msgdesc_t meshtastic_EmergencyAlert_msg; +extern const pb_msgdesc_t meshtastic_TaskRequest_msg; +extern const pb_msgdesc_t meshtastic_TAKEnvironment_msg; +extern const pb_msgdesc_t meshtastic_SensorFov_msg; +extern const pb_msgdesc_t meshtastic_TAKPacketV2_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_TAKPacket_fields &meshtastic_TAKPacket_msg @@ -269,15 +1806,43 @@ extern const pb_msgdesc_t meshtastic_PLI_msg; #define meshtastic_Status_fields &meshtastic_Status_msg #define meshtastic_Contact_fields &meshtastic_Contact_msg #define meshtastic_PLI_fields &meshtastic_PLI_msg +#define meshtastic_AircraftTrack_fields &meshtastic_AircraftTrack_msg +#define meshtastic_CotGeoPoint_fields &meshtastic_CotGeoPoint_msg +#define meshtastic_DrawnShape_fields &meshtastic_DrawnShape_msg +#define meshtastic_Marker_fields &meshtastic_Marker_msg +#define meshtastic_RangeAndBearing_fields &meshtastic_RangeAndBearing_msg +#define meshtastic_Route_fields &meshtastic_Route_msg +#define meshtastic_Route_Link_fields &meshtastic_Route_Link_msg +#define meshtastic_CasevacReport_fields &meshtastic_CasevacReport_msg +#define meshtastic_ZMistEntry_fields &meshtastic_ZMistEntry_msg +#define meshtastic_EmergencyAlert_fields &meshtastic_EmergencyAlert_msg +#define meshtastic_TaskRequest_fields &meshtastic_TaskRequest_msg +#define meshtastic_TAKEnvironment_fields &meshtastic_TAKEnvironment_msg +#define meshtastic_SensorFov_fields &meshtastic_SensorFov_msg +#define meshtastic_TAKPacketV2_fields &meshtastic_TAKPacketV2_msg /* Maximum encoded size of messages (where known) */ -#define MESHTASTIC_MESHTASTIC_ATAK_PB_H_MAX_SIZE meshtastic_TAKPacket_size +/* meshtastic_CasevacReport_size depends on runtime parameters */ +/* meshtastic_ZMistEntry_size depends on runtime parameters */ +/* meshtastic_SensorFov_size depends on runtime parameters */ +/* meshtastic_TAKPacketV2_size depends on runtime parameters */ +#define MESHTASTIC_MESHTASTIC_ATAK_PB_H_MAX_SIZE meshtastic_Route_size +#define meshtastic_AircraftTrack_size 134 #define meshtastic_Contact_size 242 -#define meshtastic_GeoChat_size 444 +#define meshtastic_CotGeoPoint_size 12 +#define meshtastic_DrawnShape_size 553 +#define meshtastic_EmergencyAlert_size 100 +#define meshtastic_GeoChat_size 495 #define meshtastic_Group_size 4 +#define meshtastic_Marker_size 191 #define meshtastic_PLI_size 31 +#define meshtastic_RangeAndBearing_size 84 +#define meshtastic_Route_Link_size 83 +#define meshtastic_Route_size 1379 #define meshtastic_Status_size 3 -#define meshtastic_TAKPacket_size 705 +#define meshtastic_TAKEnvironment_size 18 +#define meshtastic_TAKPacket_size 756 +#define meshtastic_TaskRequest_size 132 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/config.pb.cpp b/src/mesh/generated/meshtastic/config.pb.cpp index 52a591f33..c554ca43c 100644 --- a/src/mesh/generated/meshtastic/config.pb.cpp +++ b/src/mesh/generated/meshtastic/config.pb.cpp @@ -67,6 +67,8 @@ PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig, + + diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index d93f6fafa..c82dd5ff5 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -318,6 +318,15 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9 } meshtastic_Config_LoRaConfig_ModemPreset; +typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode { + /* FEM_LNA is present but disabled */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED = 0, + /* FEM_LNA is present and enabled */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED = 1, + /* FEM_LNA is not present on the device */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT = 2 +} meshtastic_Config_LoRaConfig_FEM_LNA_Mode; + typedef enum _meshtastic_Config_BluetoothConfig_PairingMode { /* Device generates a random PIN that will be shown on the screen of the device for pairing */ meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN = 0, @@ -505,6 +514,8 @@ typedef struct _meshtastic_Config_DisplayConfig { /* If false (default), the device will use short names for various display screens. If true, node names will show in long format */ bool use_long_node_name; + /* If true, the device will display message bubbles on screen. */ + bool enable_message_bubbles; } meshtastic_Config_DisplayConfig; /* Lora Config */ @@ -577,6 +588,8 @@ typedef struct _meshtastic_Config_LoRaConfig { bool ignore_mqtt; /* Sets the ok_to_mqtt bit on outgoing packets */ bool config_ok_to_mqtt; + /* Set where LORA FEM is enabled, disabled, or not present */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode; } meshtastic_Config_LoRaConfig; typedef struct _meshtastic_Config_BluetoothConfig { @@ -696,6 +709,10 @@ extern "C" { #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO #define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1)) +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_FEM_LNA_Mode)(meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT+1)) + #define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_ARRAYSIZE ((meshtastic_Config_BluetoothConfig_PairingMode)(meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN+1)) @@ -719,6 +736,7 @@ extern "C" { #define meshtastic_Config_LoRaConfig_modem_preset_ENUMTYPE meshtastic_Config_LoRaConfig_ModemPreset #define meshtastic_Config_LoRaConfig_region_ENUMTYPE meshtastic_Config_LoRaConfig_RegionCode +#define meshtastic_Config_LoRaConfig_fem_lna_mode_ENUMTYPE meshtastic_Config_LoRaConfig_FEM_LNA_Mode #define meshtastic_Config_BluetoothConfig_mode_ENUMTYPE meshtastic_Config_BluetoothConfig_PairingMode @@ -732,8 +750,8 @@ extern "C" { #define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0} -#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0} -#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} +#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0} +#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN} #define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} #define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} #define meshtastic_Config_SessionkeyConfig_init_default {0} @@ -743,8 +761,8 @@ extern "C" { #define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0} -#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0} -#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} +#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0} +#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN} #define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} #define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} #define meshtastic_Config_SessionkeyConfig_init_zero {0} @@ -811,6 +829,7 @@ extern "C" { #define meshtastic_Config_DisplayConfig_compass_orientation_tag 11 #define meshtastic_Config_DisplayConfig_use_12h_clock_tag 12 #define meshtastic_Config_DisplayConfig_use_long_node_name_tag 13 +#define meshtastic_Config_DisplayConfig_enable_message_bubbles_tag 14 #define meshtastic_Config_LoRaConfig_use_preset_tag 1 #define meshtastic_Config_LoRaConfig_modem_preset_tag 2 #define meshtastic_Config_LoRaConfig_bandwidth_tag 3 @@ -829,6 +848,7 @@ extern "C" { #define meshtastic_Config_LoRaConfig_ignore_incoming_tag 103 #define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104 #define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105 +#define meshtastic_Config_LoRaConfig_fem_lna_mode_tag 106 #define meshtastic_Config_BluetoothConfig_enabled_tag 1 #define meshtastic_Config_BluetoothConfig_mode_tag 2 #define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3 @@ -957,7 +977,8 @@ X(a, STATIC, SINGULAR, BOOL, heading_bold, 9) \ X(a, STATIC, SINGULAR, BOOL, wake_on_tap_or_motion, 10) \ X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) \ X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) \ -X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) +X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) \ +X(a, STATIC, SINGULAR, BOOL, enable_message_bubbles, 14) #define meshtastic_Config_DisplayConfig_CALLBACK NULL #define meshtastic_Config_DisplayConfig_DEFAULT NULL @@ -979,7 +1000,8 @@ X(a, STATIC, SINGULAR, FLOAT, override_frequency, 14) \ X(a, STATIC, SINGULAR, BOOL, pa_fan_disabled, 15) \ X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \ X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \ -X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) +X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) \ +X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106) #define meshtastic_Config_LoRaConfig_CALLBACK NULL #define meshtastic_Config_LoRaConfig_DEFAULT NULL @@ -1035,8 +1057,8 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; #define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size #define meshtastic_Config_BluetoothConfig_size 10 #define meshtastic_Config_DeviceConfig_size 100 -#define meshtastic_Config_DisplayConfig_size 34 -#define meshtastic_Config_LoRaConfig_size 85 +#define meshtastic_Config_DisplayConfig_size 36 +#define meshtastic_Config_LoRaConfig_size 88 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 #define meshtastic_Config_NetworkConfig_size 204 #define meshtastic_Config_PositionConfig_size 62 diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 57e7df8fc..1d6cd32f9 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2362 +#define meshtastic_BackupPreferences_size 2429 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index f11b13419..8425c122a 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -90,6 +90,12 @@ typedef struct _meshtastic_LocalModuleConfig { /* StatusMessage Config */ bool has_statusmessage; meshtastic_ModuleConfig_StatusMessageConfig statusmessage; + /* The part of the config that is specific to the Traffic Management module */ + bool has_traffic_management; + meshtastic_ModuleConfig_TrafficManagementConfig traffic_management; + /* TAK Config */ + bool has_tak; + meshtastic_ModuleConfig_TAKConfig tak; } meshtastic_LocalModuleConfig; @@ -99,9 +105,9 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_LocalConfig_init_default {false, meshtastic_Config_DeviceConfig_init_default, false, meshtastic_Config_PositionConfig_init_default, false, meshtastic_Config_PowerConfig_init_default, false, meshtastic_Config_NetworkConfig_init_default, false, meshtastic_Config_DisplayConfig_init_default, false, meshtastic_Config_LoRaConfig_init_default, false, meshtastic_Config_BluetoothConfig_init_default, 0, false, meshtastic_Config_SecurityConfig_init_default} -#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default} +#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_default, false, meshtastic_ModuleConfig_TAKConfig_init_default} #define meshtastic_LocalConfig_init_zero {false, meshtastic_Config_DeviceConfig_init_zero, false, meshtastic_Config_PositionConfig_init_zero, false, meshtastic_Config_PowerConfig_init_zero, false, meshtastic_Config_NetworkConfig_init_zero, false, meshtastic_Config_DisplayConfig_init_zero, false, meshtastic_Config_LoRaConfig_init_zero, false, meshtastic_Config_BluetoothConfig_init_zero, 0, false, meshtastic_Config_SecurityConfig_init_zero} -#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero} +#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_zero, false, meshtastic_ModuleConfig_TAKConfig_init_zero} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_LocalConfig_device_tag 1 @@ -128,6 +134,8 @@ extern "C" { #define meshtastic_LocalModuleConfig_detection_sensor_tag 13 #define meshtastic_LocalModuleConfig_paxcounter_tag 14 #define meshtastic_LocalModuleConfig_statusmessage_tag 15 +#define meshtastic_LocalModuleConfig_traffic_management_tag 16 +#define meshtastic_LocalModuleConfig_tak_tag 17 /* Struct field encoding specification for nanopb */ #define meshtastic_LocalConfig_FIELDLIST(X, a) \ @@ -166,7 +174,9 @@ X(a, STATIC, OPTIONAL, MESSAGE, neighbor_info, 11) \ X(a, STATIC, OPTIONAL, MESSAGE, ambient_lighting, 12) \ X(a, STATIC, OPTIONAL, MESSAGE, detection_sensor, 13) \ X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) \ -X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) +X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) \ +X(a, STATIC, OPTIONAL, MESSAGE, traffic_management, 16) \ +X(a, STATIC, OPTIONAL, MESSAGE, tak, 17) #define meshtastic_LocalModuleConfig_CALLBACK NULL #define meshtastic_LocalModuleConfig_DEFAULT NULL #define meshtastic_LocalModuleConfig_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig @@ -183,6 +193,8 @@ X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) #define meshtastic_LocalModuleConfig_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig #define meshtastic_LocalModuleConfig_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig #define meshtastic_LocalModuleConfig_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig +#define meshtastic_LocalModuleConfig_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig +#define meshtastic_LocalModuleConfig_tak_MSGTYPE meshtastic_ModuleConfig_TAKConfig extern const pb_msgdesc_t meshtastic_LocalConfig_msg; extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; @@ -193,8 +205,8 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size -#define meshtastic_LocalConfig_size 749 -#define meshtastic_LocalModuleConfig_size 758 +#define meshtastic_LocalConfig_size 754 +#define meshtastic_LocalModuleConfig_size 820 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp index 7f1a738c6..3648d8850 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.cpp +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -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 + + diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index aeae4bd84..d7ff32cb4 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -300,6 +300,21 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_TBEAM_1_WATT = 122, /* LilyGo T5 S3 ePaper Pro (V1 and V2) */ meshtastic_HardwareModel_T5_S3_EPAPER_PRO = 123, + /* LilyGo T-Beam BPF (144-148Mhz) */ + meshtastic_HardwareModel_TBEAM_BPF = 124, + /* LilyGo T-Mini E-paper S3 Kit */ + meshtastic_HardwareModel_MINI_EPAPER_S3 = 125, + /* LilyGo T-Display S3 Pro LR1121 */ + 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. */ + 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. ------------------------------------------------------------------------------------------------------------------------------------------ */ @@ -503,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. @@ -835,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 */ @@ -1375,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)) @@ -1405,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 + @@ -1449,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} @@ -1482,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} @@ -1571,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 @@ -1803,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) \ @@ -2085,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; @@ -2120,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 @@ -2175,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 diff --git a/src/mesh/generated/meshtastic/module_config.pb.cpp b/src/mesh/generated/meshtastic/module_config.pb.cpp index bb57c3f2d..f2fe5d967 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.cpp +++ b/src/mesh/generated/meshtastic/module_config.pb.cpp @@ -30,6 +30,9 @@ PB_BIND(meshtastic_ModuleConfig_AudioConfig, meshtastic_ModuleConfig_AudioConfig PB_BIND(meshtastic_ModuleConfig_PaxcounterConfig, meshtastic_ModuleConfig_PaxcounterConfig, AUTO) +PB_BIND(meshtastic_ModuleConfig_TrafficManagementConfig, meshtastic_ModuleConfig_TrafficManagementConfig, AUTO) + + PB_BIND(meshtastic_ModuleConfig_SerialConfig, meshtastic_ModuleConfig_SerialConfig, AUTO) @@ -54,6 +57,9 @@ PB_BIND(meshtastic_ModuleConfig_AmbientLightingConfig, meshtastic_ModuleConfig_A PB_BIND(meshtastic_ModuleConfig_StatusMessageConfig, meshtastic_ModuleConfig_StatusMessageConfig, AUTO) +PB_BIND(meshtastic_ModuleConfig_TAKConfig, meshtastic_ModuleConfig_TAKConfig, AUTO) + + PB_BIND(meshtastic_RemoteHardwarePin, meshtastic_RemoteHardwarePin, AUTO) diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 46a7164d2..b8cf60bf0 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -4,6 +4,7 @@ #ifndef PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED #define PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED #include +#include "meshtastic/atak.pb.h" #if PB_PROTO_HEADER_VERSION != 40 #error Regenerate this file with the current version of nanopb generator. @@ -228,6 +229,39 @@ typedef struct _meshtastic_ModuleConfig_PaxcounterConfig { int32_t ble_threshold; } meshtastic_ModuleConfig_PaxcounterConfig; +/* Config for the Traffic Management module. + Provides packet inspection and traffic shaping to help reduce channel utilization */ +typedef struct _meshtastic_ModuleConfig_TrafficManagementConfig { + /* Master enable for traffic management module */ + bool enabled; + /* Enable position deduplication to drop redundant position broadcasts */ + bool position_dedup_enabled; + /* Number of bits of precision for position deduplication (0-32) */ + uint32_t position_precision_bits; + /* Minimum interval in seconds between position updates from the same node */ + uint32_t position_min_interval_secs; + /* Enable direct response to NodeInfo requests from local cache */ + bool nodeinfo_direct_response; + /* Minimum hop distance from requestor before responding to NodeInfo requests */ + uint32_t nodeinfo_direct_response_max_hops; + /* Enable per-node rate limiting to throttle chatty nodes */ + bool rate_limit_enabled; + /* Time window in seconds for rate limiting calculations */ + uint32_t rate_limit_window_secs; + /* Maximum packets allowed per node within the rate limit window */ + uint32_t rate_limit_max_packets; + /* Enable dropping of unknown/undecryptable packets per rate_limit_window_secs */ + bool drop_unknown_enabled; + /* Number of unknown packets before dropping from a node */ + uint32_t unknown_packet_threshold; + /* Set hop_limit to 0 for relayed telemetry broadcasts (own packets unaffected) */ + bool exhaust_hop_telemetry; + /* Set hop_limit to 0 for relayed position broadcasts (own packets unaffected) */ + bool exhaust_hop_position; + /* Preserve hop_limit for router-to-router traffic */ + bool router_preserve_hops; +} meshtastic_ModuleConfig_TrafficManagementConfig; + /* Serial Config */ typedef struct _meshtastic_ModuleConfig_SerialConfig { /* Preferences for the SerialModule */ @@ -415,6 +449,16 @@ typedef struct _meshtastic_ModuleConfig_StatusMessageConfig { char node_status[80]; } meshtastic_ModuleConfig_StatusMessageConfig; +/* TAK team/role configuration */ +typedef struct _meshtastic_ModuleConfig_TAKConfig { + /* Team color. + Default Unspecifed_Color -> firmware uses Cyan */ + meshtastic_Team team; + /* Member role. + Default Unspecifed -> firmware uses TeamMember */ + meshtastic_MemberRole role; +} meshtastic_ModuleConfig_TAKConfig; + /* A GPIO pin definition for remote hardware module */ typedef struct _meshtastic_RemoteHardwarePin { /* GPIO Pin number (must match Arduino) */ @@ -468,6 +512,10 @@ typedef struct _meshtastic_ModuleConfig { meshtastic_ModuleConfig_PaxcounterConfig paxcounter; /* TODO: REPLACE */ meshtastic_ModuleConfig_StatusMessageConfig statusmessage; + /* Traffic management module config for mesh network optimization */ + meshtastic_ModuleConfig_TrafficManagementConfig traffic_management; + /* TAK team/role configuration for TAK_TRACKER */ + meshtastic_ModuleConfig_TAKConfig tak; } payload_variant; } meshtastic_ModuleConfig; @@ -511,6 +559,7 @@ extern "C" { #define meshtastic_ModuleConfig_AudioConfig_bitrate_ENUMTYPE meshtastic_ModuleConfig_AudioConfig_Audio_Baud + #define meshtastic_ModuleConfig_SerialConfig_baud_ENUMTYPE meshtastic_ModuleConfig_SerialConfig_Serial_Baud #define meshtastic_ModuleConfig_SerialConfig_mode_ENUMTYPE meshtastic_ModuleConfig_SerialConfig_Serial_Mode @@ -524,6 +573,9 @@ extern "C" { +#define meshtastic_ModuleConfig_TAKConfig_team_ENUMTYPE meshtastic_Team +#define meshtastic_ModuleConfig_TAKConfig_role_ENUMTYPE meshtastic_MemberRole + #define meshtastic_RemoteHardwarePin_type_ENUMTYPE meshtastic_RemoteHardwarePinType @@ -536,6 +588,7 @@ extern "C" { #define meshtastic_ModuleConfig_DetectionSensorConfig_init_default {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0} #define meshtastic_ModuleConfig_AudioConfig_init_default {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0} #define meshtastic_ModuleConfig_PaxcounterConfig_init_default {0, 0, 0, 0} +#define meshtastic_ModuleConfig_TrafficManagementConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_SerialConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} @@ -544,6 +597,7 @@ extern "C" { #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""} +#define meshtastic_ModuleConfig_TAKConfig_init_default {_meshtastic_Team_MIN, _meshtastic_MemberRole_MIN} #define meshtastic_RemoteHardwarePin_init_default {0, "", _meshtastic_RemoteHardwarePinType_MIN} #define meshtastic_ModuleConfig_init_zero {0, {meshtastic_ModuleConfig_MQTTConfig_init_zero}} #define meshtastic_ModuleConfig_MQTTConfig_init_zero {0, "", "", "", 0, 0, 0, "", 0, 0, false, meshtastic_ModuleConfig_MapReportSettings_init_zero} @@ -553,6 +607,7 @@ extern "C" { #define meshtastic_ModuleConfig_DetectionSensorConfig_init_zero {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0} #define meshtastic_ModuleConfig_AudioConfig_init_zero {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0} #define meshtastic_ModuleConfig_PaxcounterConfig_init_zero {0, 0, 0, 0} +#define meshtastic_ModuleConfig_TrafficManagementConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_SerialConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} @@ -561,6 +616,7 @@ extern "C" { #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""} +#define meshtastic_ModuleConfig_TAKConfig_init_zero {_meshtastic_Team_MIN, _meshtastic_MemberRole_MIN} #define meshtastic_RemoteHardwarePin_init_zero {0, "", _meshtastic_RemoteHardwarePinType_MIN} /* Field tags (for use in manual encoding/decoding) */ @@ -600,6 +656,20 @@ extern "C" { #define meshtastic_ModuleConfig_PaxcounterConfig_paxcounter_update_interval_tag 2 #define meshtastic_ModuleConfig_PaxcounterConfig_wifi_threshold_tag 3 #define meshtastic_ModuleConfig_PaxcounterConfig_ble_threshold_tag 4 +#define meshtastic_ModuleConfig_TrafficManagementConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_TrafficManagementConfig_position_dedup_enabled_tag 2 +#define meshtastic_ModuleConfig_TrafficManagementConfig_position_precision_bits_tag 3 +#define meshtastic_ModuleConfig_TrafficManagementConfig_position_min_interval_secs_tag 4 +#define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_tag 5 +#define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_max_hops_tag 6 +#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_enabled_tag 7 +#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_window_secs_tag 8 +#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_max_packets_tag 9 +#define meshtastic_ModuleConfig_TrafficManagementConfig_drop_unknown_enabled_tag 10 +#define meshtastic_ModuleConfig_TrafficManagementConfig_unknown_packet_threshold_tag 11 +#define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_telemetry_tag 12 +#define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_position_tag 13 +#define meshtastic_ModuleConfig_TrafficManagementConfig_router_preserve_hops_tag 14 #define meshtastic_ModuleConfig_SerialConfig_enabled_tag 1 #define meshtastic_ModuleConfig_SerialConfig_echo_tag 2 #define meshtastic_ModuleConfig_SerialConfig_rxd_tag 3 @@ -665,6 +735,8 @@ extern "C" { #define meshtastic_ModuleConfig_AmbientLightingConfig_green_tag 4 #define meshtastic_ModuleConfig_AmbientLightingConfig_blue_tag 5 #define meshtastic_ModuleConfig_StatusMessageConfig_node_status_tag 1 +#define meshtastic_ModuleConfig_TAKConfig_team_tag 1 +#define meshtastic_ModuleConfig_TAKConfig_role_tag 2 #define meshtastic_RemoteHardwarePin_gpio_pin_tag 1 #define meshtastic_RemoteHardwarePin_name_tag 2 #define meshtastic_RemoteHardwarePin_type_tag 3 @@ -685,6 +757,8 @@ extern "C" { #define meshtastic_ModuleConfig_detection_sensor_tag 12 #define meshtastic_ModuleConfig_paxcounter_tag 13 #define meshtastic_ModuleConfig_statusmessage_tag 14 +#define meshtastic_ModuleConfig_traffic_management_tag 15 +#define meshtastic_ModuleConfig_tak_tag 16 /* Struct field encoding specification for nanopb */ #define meshtastic_ModuleConfig_FIELDLIST(X, a) \ @@ -701,7 +775,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,neighbor_info,payload_varian X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ambient_lighting,payload_variant.ambient_lighting), 11) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,detection_sensor,payload_variant.detection_sensor), 12) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,paxcounter,payload_variant.paxcounter), 13) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,traffic_management,payload_variant.traffic_management), 15) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,tak,payload_variant.tak), 16) #define meshtastic_ModuleConfig_CALLBACK NULL #define meshtastic_ModuleConfig_DEFAULT NULL #define meshtastic_ModuleConfig_payload_variant_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig @@ -718,6 +794,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_varian #define meshtastic_ModuleConfig_payload_variant_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig #define meshtastic_ModuleConfig_payload_variant_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig #define meshtastic_ModuleConfig_payload_variant_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig +#define meshtastic_ModuleConfig_payload_variant_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig +#define meshtastic_ModuleConfig_payload_variant_tak_MSGTYPE meshtastic_ModuleConfig_TAKConfig #define meshtastic_ModuleConfig_MQTTConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ @@ -788,6 +866,24 @@ X(a, STATIC, SINGULAR, INT32, ble_threshold, 4) #define meshtastic_ModuleConfig_PaxcounterConfig_CALLBACK NULL #define meshtastic_ModuleConfig_PaxcounterConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_TrafficManagementConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, BOOL, position_dedup_enabled, 2) \ +X(a, STATIC, SINGULAR, UINT32, position_precision_bits, 3) \ +X(a, STATIC, SINGULAR, UINT32, position_min_interval_secs, 4) \ +X(a, STATIC, SINGULAR, BOOL, nodeinfo_direct_response, 5) \ +X(a, STATIC, SINGULAR, UINT32, nodeinfo_direct_response_max_hops, 6) \ +X(a, STATIC, SINGULAR, BOOL, rate_limit_enabled, 7) \ +X(a, STATIC, SINGULAR, UINT32, rate_limit_window_secs, 8) \ +X(a, STATIC, SINGULAR, UINT32, rate_limit_max_packets, 9) \ +X(a, STATIC, SINGULAR, BOOL, drop_unknown_enabled, 10) \ +X(a, STATIC, SINGULAR, UINT32, unknown_packet_threshold, 11) \ +X(a, STATIC, SINGULAR, BOOL, exhaust_hop_telemetry, 12) \ +X(a, STATIC, SINGULAR, BOOL, exhaust_hop_position, 13) \ +X(a, STATIC, SINGULAR, BOOL, router_preserve_hops, 14) +#define meshtastic_ModuleConfig_TrafficManagementConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_TrafficManagementConfig_DEFAULT NULL + #define meshtastic_ModuleConfig_SerialConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ X(a, STATIC, SINGULAR, BOOL, echo, 2) \ @@ -885,6 +981,12 @@ X(a, STATIC, SINGULAR, STRING, node_status, 1) #define meshtastic_ModuleConfig_StatusMessageConfig_CALLBACK NULL #define meshtastic_ModuleConfig_StatusMessageConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_TAKConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, team, 1) \ +X(a, STATIC, SINGULAR, UENUM, role, 2) +#define meshtastic_ModuleConfig_TAKConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_TAKConfig_DEFAULT NULL + #define meshtastic_RemoteHardwarePin_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, gpio_pin, 1) \ X(a, STATIC, SINGULAR, STRING, name, 2) \ @@ -900,6 +1002,7 @@ extern const pb_msgdesc_t meshtastic_ModuleConfig_NeighborInfoConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_DetectionSensorConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_AudioConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_PaxcounterConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_TrafficManagementConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_SerialConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_ExternalNotificationConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_StoreForwardConfig_msg; @@ -908,6 +1011,7 @@ extern const pb_msgdesc_t meshtastic_ModuleConfig_TelemetryConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_CannedMessageConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_AmbientLightingConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_StatusMessageConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_TAKConfig_msg; extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ @@ -919,6 +1023,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_DetectionSensorConfig_fields &meshtastic_ModuleConfig_DetectionSensorConfig_msg #define meshtastic_ModuleConfig_AudioConfig_fields &meshtastic_ModuleConfig_AudioConfig_msg #define meshtastic_ModuleConfig_PaxcounterConfig_fields &meshtastic_ModuleConfig_PaxcounterConfig_msg +#define meshtastic_ModuleConfig_TrafficManagementConfig_fields &meshtastic_ModuleConfig_TrafficManagementConfig_msg #define meshtastic_ModuleConfig_SerialConfig_fields &meshtastic_ModuleConfig_SerialConfig_msg #define meshtastic_ModuleConfig_ExternalNotificationConfig_fields &meshtastic_ModuleConfig_ExternalNotificationConfig_msg #define meshtastic_ModuleConfig_StoreForwardConfig_fields &meshtastic_ModuleConfig_StoreForwardConfig_msg @@ -927,6 +1032,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_CannedMessageConfig_fields &meshtastic_ModuleConfig_CannedMessageConfig_msg #define meshtastic_ModuleConfig_AmbientLightingConfig_fields &meshtastic_ModuleConfig_AmbientLightingConfig_msg #define meshtastic_ModuleConfig_StatusMessageConfig_fields &meshtastic_ModuleConfig_StatusMessageConfig_msg +#define meshtastic_ModuleConfig_TAKConfig_fields &meshtastic_ModuleConfig_TAKConfig_msg #define meshtastic_RemoteHardwarePin_fields &meshtastic_RemoteHardwarePin_msg /* Maximum encoded size of messages (where known) */ @@ -945,7 +1051,9 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StatusMessageConfig_size 81 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 +#define meshtastic_ModuleConfig_TAKConfig_size 4 #define meshtastic_ModuleConfig_TelemetryConfig_size 50 +#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52 #define meshtastic_ModuleConfig_size 227 #define meshtastic_RemoteHardwarePin_size 21 diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index d31daa4b2..494ef4a54 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -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 */ @@ -140,6 +142,9 @@ typedef enum _meshtastic_PortNum { meshtastic_PortNum_MAP_REPORT_APP = 73, /* PowerStress based monitoring support (for automated power consumption testing) */ meshtastic_PortNum_POWERSTRESS_APP = 74, + /* LoraWAN Payload Transport + ENCODING: compact binary LoRaWAN uplink (10-byte RF metadata + PHY payload) - see LoRaWANBridgeModule */ + meshtastic_PortNum_LORAWAN_BRIDGE = 75, /* Reticulum Network Stack Tunnel App ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface */ meshtastic_PortNum_RETICULUM_TUNNEL_APP = 76, @@ -147,6 +152,14 @@ typedef enum _meshtastic_PortNum { arbitrary telemetry over meshtastic that is not covered by telemetry.proto ENCODING: CayenneLLP */ meshtastic_PortNum_CAYENNE_APP = 77, + /* ATAK Plugin V2 + Portnum for payloads from the official Meshtastic ATAK plugin using + TAKPacketV2 with zstd dictionary compression. */ + meshtastic_PortNum_ATAK_PLUGIN_V2 = 78, + /* GroupAlarm integration + Used for transporting GroupAlarm-related messages between Meshtastic nodes + and companion applications/services. */ + meshtastic_PortNum_GROUPALARM_APP = 112, /* Private applications should use portnums >= 256. To simplify initial development and testing you can use "PRIVATE_APP" in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh)) */ diff --git a/src/mesh/generated/meshtastic/telemetry.pb.cpp b/src/mesh/generated/meshtastic/telemetry.pb.cpp index fff75ebc1..bc21b9dcb 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.cpp +++ b/src/mesh/generated/meshtastic/telemetry.pb.cpp @@ -21,6 +21,9 @@ PB_BIND(meshtastic_AirQualityMetrics, meshtastic_AirQualityMetrics, AUTO) PB_BIND(meshtastic_LocalStats, meshtastic_LocalStats, AUTO) +PB_BIND(meshtastic_TrafficManagementStats, meshtastic_TrafficManagementStats, AUTO) + + PB_BIND(meshtastic_HealthMetrics, meshtastic_HealthMetrics, AUTO) diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index dc9d876dc..8c0fdd563 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -26,7 +26,7 @@ typedef enum _meshtastic_TelemetrySensorType { meshtastic_TelemetrySensorType_INA219 = 5, /* High accuracy temperature and pressure */ meshtastic_TelemetrySensorType_BMP280 = 6, - /* High accuracy temperature and humidity */ + /* TODO - REMOVE High accuracy temperature and humidity */ meshtastic_TelemetrySensorType_SHTC3 = 7, /* High accuracy pressure */ meshtastic_TelemetrySensorType_LPS22 = 8, @@ -36,7 +36,7 @@ typedef enum _meshtastic_TelemetrySensorType { meshtastic_TelemetrySensorType_QMI8658 = 10, /* 3-Axis magnetic sensor */ meshtastic_TelemetrySensorType_QMC5883L = 11, - /* High accuracy temperature and humidity */ + /* TODO - REMOVE High accuracy temperature and humidity */ meshtastic_TelemetrySensorType_SHT31 = 12, /* PM2.5 air quality sensor */ meshtastic_TelemetrySensorType_PMSA003I = 13, @@ -46,7 +46,7 @@ typedef enum _meshtastic_TelemetrySensorType { meshtastic_TelemetrySensorType_BMP085 = 15, /* RCWL-9620 Doppler Radar Distance Sensor, used for water level detection */ meshtastic_TelemetrySensorType_RCWL9620 = 16, - /* Sensirion High accuracy temperature and humidity */ + /* TODO - REMOVE Sensirion High accuracy temperature and humidity */ meshtastic_TelemetrySensorType_SHT4X = 17, /* VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. */ meshtastic_TelemetrySensorType_VEML7700 = 18, @@ -103,7 +103,19 @@ typedef enum _meshtastic_TelemetrySensorType { /* TSL2561 light sensor */ meshtastic_TelemetrySensorType_TSL2561 = 44, /* BH1750 light sensor */ - meshtastic_TelemetrySensorType_BH1750 = 45 + meshtastic_TelemetrySensorType_BH1750 = 45, + /* HDC1080 Temperature and Humidity Sensor */ + meshtastic_TelemetrySensorType_HDC1080 = 46, + /* TODO - REMOVE STH21 Temperature and R. Humidity sensor */ + meshtastic_TelemetrySensorType_SHT21 = 47, + /* Sensirion STC31 CO2 sensor */ + meshtastic_TelemetrySensorType_STC31 = 48, + /* SCD30 CO2, humidity, temperature sensor */ + meshtastic_TelemetrySensorType_SCD30 = 49, + /* SHT family of sensors for temperature and humidity */ + meshtastic_TelemetrySensorType_SHTXX = 50, + /* DS248X Bridge for one-wire temperature sensors */ + meshtastic_TelemetrySensorType_DS248X = 51 } meshtastic_TelemetrySensorType; /* Struct definitions */ @@ -196,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) */ @@ -365,6 +380,24 @@ typedef struct _meshtastic_LocalStats { int32_t noise_floor; } meshtastic_LocalStats; +/* Traffic management statistics for mesh network optimization */ +typedef struct _meshtastic_TrafficManagementStats { + /* Total number of packets inspected by traffic management */ + uint32_t packets_inspected; + /* Number of position packets dropped due to deduplication */ + uint32_t position_dedup_drops; + /* Number of NodeInfo requests answered from cache */ + uint32_t nodeinfo_cache_hits; + /* Number of packets dropped due to rate limiting */ + uint32_t rate_limit_drops; + /* Number of unknown/undecryptable packets dropped */ + uint32_t unknown_packet_drops; + /* Number of packets with hop_limit exhausted for local-only broadcast */ + uint32_t hop_exhausted_packets; + /* Number of times router hop preservation was applied */ + uint32_t router_hops_preserved; +} meshtastic_TrafficManagementStats; + /* Health telemetry metrics */ typedef struct _meshtastic_HealthMetrics { /* Heart rate (beats per minute) */ @@ -424,6 +457,8 @@ typedef struct _meshtastic_Telemetry { meshtastic_HealthMetrics health_metrics; /* Linux host metrics */ meshtastic_HostMetrics host_metrics; + /* Traffic management statistics */ + meshtastic_TrafficManagementStats traffic_management_stats; } variant; } meshtastic_Telemetry; @@ -461,8 +496,9 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET -#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_BH1750 -#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_BH1750+1)) +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_DS248X +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_DS248X+1)) + @@ -477,20 +513,22 @@ 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} +#define meshtastic_TrafficManagementStats_init_default {0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0} #define meshtastic_HostMetrics_init_default {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}} #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} +#define meshtastic_TrafficManagementStats_init_zero {0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0} #define meshtastic_HostMetrics_init_zero {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_zero {0, 0, {meshtastic_DeviceMetrics_init_zero}} @@ -525,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 @@ -581,6 +620,13 @@ extern "C" { #define meshtastic_LocalStats_heap_free_bytes_tag 13 #define meshtastic_LocalStats_num_tx_dropped_tag 14 #define meshtastic_LocalStats_noise_floor_tag 15 +#define meshtastic_TrafficManagementStats_packets_inspected_tag 1 +#define meshtastic_TrafficManagementStats_position_dedup_drops_tag 2 +#define meshtastic_TrafficManagementStats_nodeinfo_cache_hits_tag 3 +#define meshtastic_TrafficManagementStats_rate_limit_drops_tag 4 +#define meshtastic_TrafficManagementStats_unknown_packet_drops_tag 5 +#define meshtastic_TrafficManagementStats_hop_exhausted_packets_tag 6 +#define meshtastic_TrafficManagementStats_router_hops_preserved_tag 7 #define meshtastic_HealthMetrics_heart_bpm_tag 1 #define meshtastic_HealthMetrics_spO2_tag 2 #define meshtastic_HealthMetrics_temperature_tag 3 @@ -601,6 +647,7 @@ extern "C" { #define meshtastic_Telemetry_local_stats_tag 6 #define meshtastic_Telemetry_health_metrics_tag 7 #define meshtastic_Telemetry_host_metrics_tag 8 +#define meshtastic_Telemetry_traffic_management_stats_tag 9 #define meshtastic_Nau7802Config_zeroOffset_tag 1 #define meshtastic_Nau7802Config_calibrationFactor_tag 2 #define meshtastic_SEN5XState_last_cleaning_time_tag 1 @@ -642,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 @@ -714,6 +762,17 @@ X(a, STATIC, SINGULAR, INT32, noise_floor, 15) #define meshtastic_LocalStats_CALLBACK NULL #define meshtastic_LocalStats_DEFAULT NULL +#define meshtastic_TrafficManagementStats_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, packets_inspected, 1) \ +X(a, STATIC, SINGULAR, UINT32, position_dedup_drops, 2) \ +X(a, STATIC, SINGULAR, UINT32, nodeinfo_cache_hits, 3) \ +X(a, STATIC, SINGULAR, UINT32, rate_limit_drops, 4) \ +X(a, STATIC, SINGULAR, UINT32, unknown_packet_drops, 5) \ +X(a, STATIC, SINGULAR, UINT32, hop_exhausted_packets, 6) \ +X(a, STATIC, SINGULAR, UINT32, router_hops_preserved, 7) +#define meshtastic_TrafficManagementStats_CALLBACK NULL +#define meshtastic_TrafficManagementStats_DEFAULT NULL + #define meshtastic_HealthMetrics_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, UINT32, heart_bpm, 1) \ X(a, STATIC, OPTIONAL, UINT32, spO2, 2) \ @@ -742,7 +801,8 @@ X(a, STATIC, ONEOF, MESSAGE, (variant,air_quality_metrics,variant.air_qual X(a, STATIC, ONEOF, MESSAGE, (variant,power_metrics,variant.power_metrics), 5) \ X(a, STATIC, ONEOF, MESSAGE, (variant,local_stats,variant.local_stats), 6) \ X(a, STATIC, ONEOF, MESSAGE, (variant,health_metrics,variant.health_metrics), 7) \ -X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), 8) +X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), 8) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,traffic_management_stats,variant.traffic_management_stats), 9) #define meshtastic_Telemetry_CALLBACK NULL #define meshtastic_Telemetry_DEFAULT NULL #define meshtastic_Telemetry_variant_device_metrics_MSGTYPE meshtastic_DeviceMetrics @@ -752,6 +812,7 @@ X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), #define meshtastic_Telemetry_variant_local_stats_MSGTYPE meshtastic_LocalStats #define meshtastic_Telemetry_variant_health_metrics_MSGTYPE meshtastic_HealthMetrics #define meshtastic_Telemetry_variant_host_metrics_MSGTYPE meshtastic_HostMetrics +#define meshtastic_Telemetry_variant_traffic_management_stats_MSGTYPE meshtastic_TrafficManagementStats #define meshtastic_Nau7802Config_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, INT32, zeroOffset, 1) \ @@ -774,6 +835,7 @@ extern const pb_msgdesc_t meshtastic_EnvironmentMetrics_msg; extern const pb_msgdesc_t meshtastic_PowerMetrics_msg; extern const pb_msgdesc_t meshtastic_AirQualityMetrics_msg; extern const pb_msgdesc_t meshtastic_LocalStats_msg; +extern const pb_msgdesc_t meshtastic_TrafficManagementStats_msg; extern const pb_msgdesc_t meshtastic_HealthMetrics_msg; extern const pb_msgdesc_t meshtastic_HostMetrics_msg; extern const pb_msgdesc_t meshtastic_Telemetry_msg; @@ -786,6 +848,7 @@ extern const pb_msgdesc_t meshtastic_SEN5XState_msg; #define meshtastic_PowerMetrics_fields &meshtastic_PowerMetrics_msg #define meshtastic_AirQualityMetrics_fields &meshtastic_AirQualityMetrics_msg #define meshtastic_LocalStats_fields &meshtastic_LocalStats_msg +#define meshtastic_TrafficManagementStats_fields &meshtastic_TrafficManagementStats_msg #define meshtastic_HealthMetrics_fields &meshtastic_HealthMetrics_msg #define meshtastic_HostMetrics_fields &meshtastic_HostMetrics_msg #define meshtastic_Telemetry_fields &meshtastic_Telemetry_msg @@ -796,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 @@ -804,6 +867,7 @@ extern const pb_msgdesc_t meshtastic_SEN5XState_msg; #define meshtastic_PowerMetrics_size 81 #define meshtastic_SEN5XState_size 27 #define meshtastic_Telemetry_size 272 +#define meshtastic_TrafficManagementStats_size 42 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index ea8d6af8e..6b3aa4859 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -9,7 +9,6 @@ #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" #endif -#include "Led.h" #include "SPILock.h" #include "power.h" #include "serialization/JSON.h" @@ -47,10 +46,6 @@ using namespace httpsserver; #include "mesh/http/ContentHandler.h" -#include -#include -HTTPClient httpClient; - #define DEST_FS_USES_LITTLEFS // We need to specify some content-type mapping, so the resources get delivered with the @@ -92,7 +87,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); - ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes); ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic); @@ -110,7 +104,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) secureServer->registerNode(nodeRestart); secureServer->registerNode(nodeFormUpload); secureServer->registerNode(nodeJsonScanNetworks); - secureServer->registerNode(nodeJsonBlinkLED); secureServer->registerNode(nodeJsonFsBrowseStatic); secureServer->registerNode(nodeJsonDelete); secureServer->registerNode(nodeJsonReport); @@ -133,7 +126,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) insecureServer->registerNode(nodeRestart); insecureServer->registerNode(nodeFormUpload); insecureServer->registerNode(nodeJsonScanNetworks); - insecureServer->registerNode(nodeJsonBlinkLED); insecureServer->registerNode(nodeJsonFsBrowseStatic); insecureServer->registerNode(nodeJsonDelete); insecureServer->registerNode(nodeJsonReport); @@ -348,11 +340,6 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) res->print(jsonString.c_str()); delete value; - - // Clean up the fileList to prevent memory leak - for (auto *val : fileList) { - delete val; - } } void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) @@ -547,6 +534,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res) if (name != "file") { LOG_DEBUG("Skip unexpected field"); res->println("

No file found.

"); + delete parser; return; } @@ -554,6 +542,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res) if (filename == "") { LOG_DEBUG("Skip unexpected field"); res->println("

No file found.

"); + delete parser; return; } @@ -627,7 +616,7 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) } // Helper lambda to create JSON array and clean up memory properly - auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * { + auto createJSONArrayFromLog = [](const uint32_t *logArray, int count) -> JSONValue * { JSONArray tempArray; for (int i = 0; i < count; i++) { tempArray.push_back(new JSONValue((int)logArray[i])); @@ -787,11 +776,6 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res) std::string jsonString = value->Stringify(); res->print(jsonString.c_str()); delete value; - - // Clean up the nodesArray to prevent memory leak - for (auto *val : nodesArray) { - delete val; - } } /* @@ -904,45 +888,6 @@ void handleRestart(HTTPRequest *req, HTTPResponse *res) webServerThread->requestRestart = (millis() / 1000) + 5; } -void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "POST"); - - ResourceParameters *params = req->getParams(); - std::string blink_target; - - if (!params->getQueryParameter("blink_target", blink_target)) { - // if no blink_target was supplied in the URL parameters of the - // POST request, then assume we should blink the LED - blink_target = "LED"; - } - - if (blink_target == "LED") { - uint8_t count = 10; - while (count > 0) { - ledBlink.set(true); - delay(50); - ledBlink.set(false); - delay(50); - count = count - 1; - } - } else { -#if HAS_SCREEN - if (screen) - screen->blink(); -#endif - } - - JSONObject jsonObjOuter; - jsonObjOuter["status"] = new JSONValue("ok"); - JSONValue *value = new JSONValue(jsonObjOuter); - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - delete value; -} - void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) { res->setHeader("Content-Type", "application/json"); @@ -984,10 +929,5 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) std::string jsonString = value->Stringify(); res->print(jsonString.c_str()); delete value; - - // Clean up the networkObjs to prevent memory leak - for (auto *val : networkObjs) { - delete val; - } } #endif \ No newline at end of file diff --git a/src/mesh/http/ContentHandler.h b/src/mesh/http/ContentHandler.h index 91cad3359..ed182ad76 100644 --- a/src/mesh/http/ContentHandler.h +++ b/src/mesh/http/ContentHandler.h @@ -11,7 +11,6 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res); void handleScanNetworks(HTTPRequest *req, HTTPResponse *res); void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res); void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res); -void handleBlinkLED(HTTPRequest *req, HTTPResponse *res); void handleReport(HTTPRequest *req, HTTPResponse *res); void handleNodes(HTTPRequest *req, HTTPResponse *res); void handleUpdateFs(HTTPRequest *req, HTTPResponse *res); @@ -28,10 +27,10 @@ class HttpAPI : public PhoneAPI public: HttpAPI() { api_type = TYPE_HTTP; } + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this private: // Nothing here yet protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this }; \ No newline at end of file diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 3a264fa5a..90fd8b084 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -55,6 +56,12 @@ static const int32_t ACTIVE_INTERVAL_MS = 50; static const int32_t MEDIUM_INTERVAL_MS = 200; static const int32_t IDLE_INTERVAL_MS = 1000; +// Maximum concurrent HTTPS connections (reduced from default 4 to save memory) +static const uint8_t MAX_HTTPS_CONNECTIONS = 2; + +// Minimum free heap required for SSL handshake (~40KB for mbedTLS contexts) +static const uint32_t MIN_HEAP_FOR_SSL = 40000; + static SSLCert *cert; static HTTPSServer *secureServer; static HTTPServer *insecureServer; @@ -67,8 +74,20 @@ static void handleWebResponse() if (isWifiAvailable()) { if (isWebServerReady) { - if (secureServer) - secureServer->loop(); + // Check heap before HTTPS processing - SSL requires significant memory + if (secureServer) { + uint32_t freeHeap = ESP.getFreeHeap(); + if (freeHeap >= MIN_HEAP_FOR_SSL) { + secureServer->loop(); + } else { + // Skip HTTPS when memory is low to prevent SSL setup failures + static uint32_t lastHeapWarning = 0; + if (lastHeapWarning == 0 || !Throttle::isWithinTimespanMs(lastHeapWarning, 30000)) { + LOG_WARN("Low heap (%u bytes), skipping HTTPS processing", freeHeap); + lastHeapWarning = millis(); + } + } + } insecureServer->loop(); } } @@ -229,7 +248,7 @@ void initWebServer() LOG_DEBUG("Init Web Server"); // We can now use the new certificate to setup our server as usual. - secureServer = new HTTPSServer(cert); + secureServer = new HTTPSServer(cert, 443, MAX_HTTPS_CONNECTIONS); insecureServer = new HTTPServer(); registerHandlers(insecureServer, secureServer); diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h index e7a29a5a7..762afc618 100644 --- a/src/mesh/http/WebServer.h +++ b/src/mesh/http/WebServer.h @@ -5,6 +5,8 @@ #include #include +#if !MESHTASTIC_EXCLUDE_WEBSERVER + void initWebServer(); void createSSLCert(); @@ -24,3 +26,20 @@ class WebServerThread : private concurrency::OSThread }; extern WebServerThread *webServerThread; + +#else +// Stub implementations when web server is excluded +inline void initWebServer() {} +inline void createSSLCert() {} + +class WebServerThread +{ + public: + WebServerThread() {} + uint32_t requestRestart = 0; + void markActivity() {} +}; + +inline WebServerThread *webServerThread = nullptr; + +#endif diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index e4f65aa28..eea7d4efc 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -69,6 +69,22 @@ static inline int get_max_num_nodes() /// Max number of channels allowed #define MAX_NUM_CHANNELS (member_size(meshtastic_ChannelFile, channels) / member_size(meshtastic_ChannelFile, channels[0])) +// Traffic Management module configuration +// Enable per-variant by defining HAS_TRAFFIC_MANAGEMENT=1 in variant.h +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 0 +#endif + +// Cache size for traffic management (number of nodes to track) +// Can be overridden per-variant based on available memory +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#if HAS_TRAFFIC_MANAGEMENT +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000 +#else +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 0 +#endif +#endif + /// helper function for encoding a record as a protobuf, any failures to encode are fatal and we will panic /// returns the encoded packet size size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc_t *fields, const void *src_struct); @@ -90,4 +106,4 @@ bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count); */ bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count); -#define is_in_repeated(name, n) is_in_helper(n, name, name##_count) \ No newline at end of file +#define is_in_repeated(name, n) is_in_helper(n, name, name##_count) diff --git a/src/mesh/raspihttp/PiWebServer.h b/src/mesh/raspihttp/PiWebServer.h index 5a4adedaa..74b094f8c 100644 --- a/src/mesh/raspihttp/PiWebServer.h +++ b/src/mesh/raspihttp/PiWebServer.h @@ -29,12 +29,13 @@ class HttpAPI : public PhoneAPI public: HttpAPI() { api_type = TYPE_HTTP; } + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this + private: // Nothing here yet protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this }; class PiWebServerThread diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 2df8686a3..493cc5353 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -22,10 +22,14 @@ class UdpMulticastHandler final { public: - UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); } + UdpMulticastHandler() : isRunning(false) { udpIpAddress = IPAddress(224, 0, 0, 69); } void start() { + if (isRunning) { + LOG_DEBUG("UDP multicast already running"); + return; + } if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) { #if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3], @@ -34,13 +38,29 @@ class UdpMulticastHandler final LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str()); #endif udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); }); + isRunning = true; } else { LOG_DEBUG("Failed to listen on UDP"); } } - void onReceive(AsyncUDPPacket packet) + void stop() { + if (!isRunning) { + return; + } + LOG_DEBUG("Stopping UDP multicast"); +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) + udp.close(); +#endif + isRunning = false; + } + + void onReceive(AsyncUDPPacket &packet) + { + if (!isRunning) { + return; + } size_t packetLength = packet.length(); #if defined(ARCH_NRF52) IPAddress ip = packet.remoteIP(); @@ -49,14 +69,16 @@ class UdpMulticastHandler final // FIXME(PORTDUINO): arduino lacks IPAddress::toString() LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif - meshtastic_MeshPacket mp; + meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero; LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + // Drop packets with spoofed local origin — no legitimate LAN node should send from=0 or our own nodeNum + if (isFromUs(&mp)) { + LOG_WARN("UDP packet with spoofed local from=0x%x, dropping", mp.from); + return; + } mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; - mp.pki_encrypted = false; - mp.public_key.size = 0; - memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); // Unset received SNR/RSSI p->rx_snr = 0; @@ -67,7 +89,7 @@ class UdpMulticastHandler final bool onSend(const meshtastic_MeshPacket *mp) { - if (!mp || !udp) { + if (!isRunning || !mp || !udp) { return false; } #if defined(ARCH_NRF52) @@ -85,12 +107,12 @@ class UdpMulticastHandler final LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id); uint8_t buffer[meshtastic_MeshPacket_size]; size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp); - udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); - return true; + return udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); } private: IPAddress udpIpAddress; AsyncUDP udp; + bool isRunning; }; -#endif // HAS_UDP_MULTICAST \ No newline at end of file +#endif // HAS_UDP_MULTICAST diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index a95dfa58f..5d05c7fc6 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -391,6 +391,11 @@ static void WiFiEvent(WiFiEvent_t event) LOG_INFO("Disconnected from WiFi access point"); #ifdef WIFI_LED digitalWrite(WIFI_LED, LOW); +#endif +#if HAS_UDP_MULTICAST + if (udpHandler) { + udpHandler->stop(); + } #endif if (!isReconnecting) { WiFi.disconnect(false, true); @@ -417,6 +422,11 @@ static void WiFiEvent(WiFiEvent_t event) break; case ARDUINO_EVENT_WIFI_STA_LOST_IP: LOG_INFO("Lost IP address and IP address is reset to 0"); +#if HAS_UDP_MULTICAST + if (udpHandler) { + udpHandler->stop(); + } +#endif if (!isReconnecting) { WiFi.disconnect(false, true); syslog.disable(); diff --git a/src/meshUtils.cpp b/src/meshUtils.cpp index ea2ba641b..1a4497101 100644 --- a/src/meshUtils.cpp +++ b/src/meshUtils.cpp @@ -106,4 +106,15 @@ const std::string vformat(const char *const zcFormat, ...) std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs); va_end(vaArgs); return std::string(zc.data(), iLen); +} + +size_t pb_string_length(const char *str, size_t max_len) +{ + size_t len = 0; + for (size_t i = 0; i < max_len; i++) { + if (str[i] != '\0') { + len = i + 1; + } + } + return len; } \ No newline at end of file diff --git a/src/meshUtils.h b/src/meshUtils.h index 9fcf6f8a8..da3a4593b 100644 --- a/src/meshUtils.h +++ b/src/meshUtils.h @@ -35,4 +35,13 @@ bool isOneOf(int item, int count, ...); const std::string vformat(const char *const zcFormat, ...); +// Get actual string length for nanopb char array fields. +size_t pb_string_length(const char *str, size_t max_len); + +/// Calculate 2^n without calling pow() - used for spreading factor and other calculations +inline uint32_t pow_of_2(uint32_t n) +{ + return 1 << n; +} + #define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 1fda9bf13..05bc0aa5d 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -23,7 +23,10 @@ #endif #include "Default.h" +#include "MeshRadio.h" +#include "RadioInterface.h" #include "TypeConversions.h" +#include "mesh/RadioLibInterface.h" #if !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" @@ -37,7 +40,7 @@ #include "modules/PositionModule.h" #endif -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ @@ -196,10 +199,35 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta handleSetOwner(r->set_owner); break; - case meshtastic_AdminMessage_set_config_tag: + case meshtastic_AdminMessage_set_config_tag: { LOG_DEBUG("Client set config"); - handleSetConfig(r->set_config); + + // Non-LoRa configs need no further validation. + if (r->set_config.which_payload_variant != meshtastic_Config_lora_tag) { + LOG_DEBUG("Non-LoRa config, applying directly"); + handleSetConfig(r->set_config, fromOthers); + break; + } + + // Only LORA_24 requires hardware capability validation. + if (r->set_config.payload_variant.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { + LOG_DEBUG("LoRa config, region is not LORA_24, applying directly"); + handleSetConfig(r->set_config, fromOthers); + break; + } + + // Hardware supports 2.4 GHz — apply the config. + // Fail closed: null instance is treated as incapable. + if (RadioLibInterface::instance && RadioLibInterface::instance->wideLora()) { + LOG_DEBUG("LORA_24 requested, radio hardware supports 2.4 GHz, applying"); + handleSetConfig(r->set_config, fromOthers); + break; + } + + LOG_WARN("Radio hardware does not support 2.4 GHz; rejecting LORA_24 region"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); break; + } case meshtastic_AdminMessage_set_module_config_tag: LOG_DEBUG("Client set module config"); @@ -391,7 +419,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta node->has_device_metrics = false; node->has_position = false; node->user.public_key.size = 0; - node->user.public_key.bytes[0] = 0; + memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes)); saveChanges(SEGMENT_NODEDATABASE, false); } break; @@ -452,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; @@ -625,7 +653,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o) } } -void AdminModule::handleSetConfig(const meshtastic_Config &c) +void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) { auto changes = SEGMENT_CONFIG; auto existingRole = config.device.role; @@ -636,19 +664,14 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) case meshtastic_Config_device_tag: LOG_INFO("Set config: Device"); config.has_device = true; -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \ + !MESHTASTIC_EXCLUDE_ACCELEROMETER if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true && accelerometerThread->enabled == false) { config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press; accelerometerThread->enabled = true; accelerometerThread->start(); } -#endif -#ifdef LED_PIN - // Turn LED off if heartbeat by config - if (c.payload_variant.device.led_heartbeat_disabled) { - digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); - } #endif if (config.device.button_gpio == c.payload_variant.device.button_gpio && config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio && @@ -658,9 +681,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) } config.device = c.payload_variant.device; if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_NONE && - config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { + (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; - const char *warning = "Rebroadcast mode can't be set to NONE for a router"; + const char *warning = "Rebroadcast mode can't be set to NONE for a router role"; LOG_WARN(warning); sendWarning(warning); } @@ -743,7 +767,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { config.bluetooth.enabled = false; } -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \ + !MESHTASTIC_EXCLUDE_ACCELEROMETER if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && accelerometerThread->enabled == false) { config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; @@ -762,33 +787,66 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) LOG_INFO("Set config: LoRa"); config.has_lora = true; - if (validatedLora.coding_rate < 4 || validatedLora.coding_rate > 8) { - LOG_WARN("Invalid coding_rate %d, setting to 5", validatedLora.coding_rate); - validatedLora.coding_rate = 5; + if (validatedLora.coding_rate != clampCodingRate(validatedLora.coding_rate)) { + LOG_WARN("Invalid coding_rate %d, setting to %d", validatedLora.coding_rate, LORA_CR_DEFAULT); + validatedLora.coding_rate = LORA_CR_DEFAULT; } - if (validatedLora.spread_factor < 7 || validatedLora.spread_factor > 12) { - LOG_WARN("Invalid spread_factor %d, setting to 11", validatedLora.spread_factor); - validatedLora.spread_factor = 11; + if (validatedLora.spread_factor != clampSpreadFactor(validatedLora.spread_factor)) { + LOG_WARN("Invalid spread_factor %d, setting to %d", validatedLora.spread_factor, LORA_SF_DEFAULT); + validatedLora.spread_factor = LORA_SF_DEFAULT; } - if (validatedLora.bandwidth == 0) { - int originalBandwidth = validatedLora.bandwidth; - validatedLora.bandwidth = myRegion->wideLora ? 800 : 250; - LOG_WARN("Invalid bandwidth %d, setting to default", originalBandwidth); + // If we're setting a new region, check the region is valid and then init the region or discard the change + if (validatedLora.region != myRegion->code) { + // Region has changed so check whether it is valid for e.g. licensing conditions and if the lora config is valid + if (RadioInterface::validateConfigRegion(validatedLora) && RadioInterface::validateConfigLora(validatedLora)) { + // If we're setting region for the first time, init the region and regenerate the keys + if (isRegionUnset && validatedLora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + if (crypto) { + crypto->ensurePkiKeys(config.security, owner); + } +#endif + // new region is valid and we're coming from an unset region, so enable tx + validatedLora.tx_enabled = true; + } + // If we're unsetting the region for some reason, disable tx + if (!isRegionUnset && validatedLora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + validatedLora.tx_enabled = false; + } + // Ensure initRegion() uses the newly validated region + config.lora.region = validatedLora.region; + initRegion(); + if (myRegion->dutyCycle < 100) { + validatedLora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + // Default root is in use, so subscribe to the appropriate MQTT topic for this region + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + } + changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; + } else { + // Region validation has failed, so just copy all of the old config over the new config + validatedLora = oldLoraConfig; + } + } // end of new region handling + + if (!RadioInterface::validateConfigLora(validatedLora)) { + if (fromOthers) { + LOG_WARN("Invalid LoRa config received from another node, rejecting changes"); + // modem_preset set to use the old setting if the check fails + validatedLora.modem_preset = oldLoraConfig.modem_preset; + } else { + LOG_WARN("Invalid LoRa config received from client, using corrected values"); + RadioInterface::clampConfigLora(validatedLora); + } + // use_preset and bandwidth are coerced into valid values by the check. } - // If no lora radio parameters change, don't need to reboot - if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region && - oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth && - oldLoraConfig.spread_factor == validatedLora.spread_factor && - oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power && - oldLoraConfig.frequency_offset == validatedLora.frequency_offset && - oldLoraConfig.override_frequency == validatedLora.override_frequency && - oldLoraConfig.channel_num == validatedLora.channel_num && - oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) { - requiresReboot = false; - } + // All LoRa radio changes apply live via configChanged observer → reconfigure(). + // reconfigure() puts the radio in standby, reprograms all modem parameters, and restarts receive. + requiresReboot = false; #if defined(ARCH_PORTDUINO) // If running on portduino and using SimRadio, do not require reboot @@ -805,52 +863,21 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) digitalWrite(RF95_FAN_EN, HIGH ^ 0); } #endif - config.lora = validatedLora; - // If we're setting region for the first time, init the region and regenerate the keys - if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { -#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } - } + +#if HAS_LORA_FEM + // Apply FEM LNA mode from config (only meaningful on hardware that supports it) + // Note that a rejected lora config will revert this as well. + if (loraFEMInterface.isLnaCanControl()) { + loraFEMInterface.setLNAEnable(validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED); + } else if (validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT) { + // Hardware FEM does not support LNA control; normalize stored config to match actual capability + LOG_WARN("FEM LNA mode configured but current FEM does not support LNA control; normalizing to NOT_PRESENT"); + validatedLora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT; + } #endif - config.lora.tx_enabled = true; - initRegion(); - if (myRegion->dutyCycle < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit - } - // Compare the entire string, we are sure of the length as a topic has never been set - if (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) { - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; - } - } - if (config.lora.region != myRegion->code) { - // Region has changed so check whether there is a regulatory one we should be using instead. - // Additionally as a side-effect, assume a new value under myRegion - initRegion(); - if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { - // Default root is in use, so subscribe to the appropriate MQTT topic for this region - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - } + config.lora = validatedLora; // Finally, return the validated config back to the main config - changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; - } break; } case meshtastic_Config_bluetooth_tag: @@ -898,17 +925,18 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) } if (requiresReboot && !hasOpenEditTransaction) { disableBluetooth(); - } + } // end of switch case which_payload_variant saveChanges(changes, requiresReboot); -} +} // end of handleSetConfig bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { + bool shouldReboot = true; // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth // Otherwise, disable Bluetooth to prevent the phone from interfering with the config - if (!hasOpenEditTransaction && - !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { + if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, + meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) { disableBluetooth(); } @@ -1000,8 +1028,19 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) moduleConfig.has_paxcounter = true; moduleConfig.paxcounter = c.payload_variant.paxcounter; break; + case meshtastic_ModuleConfig_statusmessage_tag: + LOG_INFO("Set module config: StatusMessage"); + moduleConfig.has_statusmessage = true; + moduleConfig.statusmessage = c.payload_variant.statusmessage; + shouldReboot = false; + break; + case meshtastic_ModuleConfig_traffic_management_tag: + LOG_INFO("Set module config: Traffic Management"); + moduleConfig.has_traffic_management = true; + moduleConfig.traffic_management = c.payload_variant.traffic_management; + break; } - saveChanges(SEGMENT_MODULECONFIG); + saveChanges(SEGMENT_MODULECONFIG, shouldReboot); return true; } @@ -1114,73 +1153,85 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; if (req.decoded.want_response) { + const char *configName = "?"; switch (configType) { case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG: - LOG_INFO("Get module config: MQTT"); + configName = "MQTT"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt; break; case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG: - LOG_INFO("Get module config: Serial"); + configName = "Serial"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_serial_tag; res.get_module_config_response.payload_variant.serial = moduleConfig.serial; break; case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG: - LOG_INFO("Get module config: External Notification"); + configName = "External Notification"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification; break; case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG: - LOG_INFO("Get module config: Store & Forward"); + configName = "Store & Forward"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward; break; case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG: - LOG_INFO("Get module config: Range Test"); + configName = "Range Test"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test; break; case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG: - LOG_INFO("Get module config: Telemetry"); + configName = "Telemetry"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry; break; case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG: - LOG_INFO("Get module config: Canned Message"); + configName = "Canned Message"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message; break; case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG: - LOG_INFO("Get module config: Audio"); + configName = "Audio"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag; res.get_module_config_response.payload_variant.audio = moduleConfig.audio; break; case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG: - LOG_INFO("Get module config: Remote Hardware"); + configName = "Remote Hardware"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware; break; case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG: - LOG_INFO("Get module config: Neighbor Info"); + configName = "Neighbor Info"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info; break; case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG: - LOG_INFO("Get module config: Detection Sensor"); + configName = "Detection Sensor"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor; break; case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG: - LOG_INFO("Get module config: Ambient Lighting"); + configName = "Ambient Lighting"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; break; case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG: - LOG_INFO("Get module config: Paxcounter"); + configName = "Paxcounter"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; break; + case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG: + configName = "StatusMessage"; + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag; + res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage; + break; + case meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG: + configName = "Traffic Management"; + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag; + res.get_module_config_response.payload_variant.traffic_management = moduleConfig.traffic_management; + break; } + LOG_INFO("Get module config: %s", configName); // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. // So even if we internally use 0 to represent 'use default' we still need to send the value we are @@ -1363,23 +1414,17 @@ void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uic void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) { // Validate ham parameters before setting since this would bypass validation in the owner struct - if (*p.call_sign) { - const char *start = p.call_sign; - // Skip all whitespace - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character"); - return; - } - } - if (*p.short_name) { - const char *start = p.short_name; - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character"); - return; + const char *fieldsToCheck[] = {p.call_sign, p.short_name}; + const char *fieldNames[] = {"call_sign", "short_name"}; + for (int i = 0; i < 2; i++) { + if (*fieldsToCheck[i]) { + const char *start = fieldsToCheck[i]; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham %s: must contain at least 1 non-whitespace character", fieldNames[i]); + return; + } } } diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index c446887b3..5c690abbd 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -60,7 +60,11 @@ class AdminModule : public ProtobufModule, public Obser */ void handleSetOwner(const meshtastic_User &o); void handleSetChannel(const meshtastic_Channel &cc); - void handleSetConfig(const meshtastic_Config &c); + + protected: + void handleSetConfig(const meshtastic_Config &c, bool fromOthers); + + private: bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); void handleSetChannel(); void handleSetHamMode(const meshtastic_HamParameters &req); diff --git a/src/modules/AtakPluginModule.cpp b/src/modules/AtakPluginModule.cpp index a51ef54c3..bddb6276b 100644 --- a/src/modules/AtakPluginModule.cpp +++ b/src/modules/AtakPluginModule.cpp @@ -6,6 +6,7 @@ #include "configuration.h" #include "main.h" #include "mesh/compression/unishox2.h" +#include "meshUtils.h" #include "meshtastic/atak.pb.h" AtakPluginModule *atakPluginModule; @@ -70,16 +71,17 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast auto compressed = cloneTAKPacketData(t); compressed.is_compressed = true; if (t->has_contact) { - auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign, - sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); + auto length = unishox2_compress_lines( + t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)), + compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet"); return; } LOG_DEBUG("Compressed callsign: %d bytes", length); - length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), - compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, - USX_PSET_DFLT, NULL); + length = unishox2_compress_lines( + t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)), + compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet"); return; @@ -87,9 +89,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast LOG_DEBUG("Compressed device_callsign: %d bytes", length); } if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { - auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), - compressed.payload_variant.chat.message, - sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); + auto length = unishox2_compress_lines( + t->payload_variant.chat.message, + pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)), + compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, + NULL); if (length < 0) { LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet"); return; @@ -98,9 +102,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast if (t->payload_variant.chat.has_to) { compressed.payload_variant.chat.has_to = true; - length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), - compressed.payload_variant.chat.to, - sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); + length = unishox2_compress_lines( + t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)), + compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet"); return; @@ -110,9 +114,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast if (t->payload_variant.chat.has_to_callsign) { compressed.payload_variant.chat.has_to_callsign = true; - length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), - compressed.payload_variant.chat.to_callsign, - sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); + length = unishox2_compress_lines( + t->payload_variant.chat.to_callsign, + pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)), + compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1, + USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet"); return; @@ -134,18 +140,18 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast auto uncompressed = cloneTAKPacketData(t); uncompressed.is_compressed = false; if (t->has_contact) { - auto length = - unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign, - sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); + auto length = unishox2_decompress_lines( + t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)), + uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow contact.callsign. Bailing out"); return; } LOG_DEBUG("Decompressed callsign: %d bytes", length); - length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), - uncompressed.contact.device_callsign, - sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); + length = unishox2_decompress_lines( + t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)), + uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow contact.device_callsign. Bailing out"); return; @@ -153,9 +159,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast LOG_DEBUG("Decompressed device_callsign: %d bytes", length); } if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) { - auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), - uncompressed.payload_variant.chat.message, - sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); + auto length = unishox2_decompress_lines( + t->payload_variant.chat.message, + pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)), + uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, + NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.message. Bailing out"); return; @@ -164,9 +172,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast if (t->payload_variant.chat.has_to) { uncompressed.payload_variant.chat.has_to = true; - length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), - uncompressed.payload_variant.chat.to, - sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); + length = unishox2_decompress_lines( + t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)), + uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.to. Bailing out"); return; @@ -176,10 +184,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast if (t->payload_variant.chat.has_to_callsign) { uncompressed.payload_variant.chat.has_to_callsign = true; - length = - unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), - uncompressed.payload_variant.chat.to_callsign, - sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); + length = unishox2_decompress_lines( + t->payload_variant.chat.to_callsign, + pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)), + uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, + USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.to_callsign. Bailing out"); return; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 8d1ba6346..65e903134 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,10 +13,12 @@ #include "buzz.h" #include "detect/ScanI2C.h" #include "gps/RTC.h" +#include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/MessageRenderer.h" #include "graphics/draw/NotificationRenderer.h" +#include "graphics/draw/UIRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "input/SerialKeyboard.h" @@ -45,71 +47,6 @@ extern MessageStore messageStore; // Remove Canned message screen if no action is taken for some milliseconds #define INACTIVATE_AFTER_MS 20000 -// Tokenize a message string into emote/text segments -static std::vector> tokenizeMessageWithEmotes(const char *msg) -{ - std::vector> tokens; - int msgLen = strlen(msg); - int pos = 0; - while (pos < msgLen) { - const graphics::Emote *foundEmote = nullptr; - int foundLen = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - int labelLen = strlen(label); - if (labelLen == 0) - continue; - if (strncmp(msg + pos, label, labelLen) == 0) { - if (!foundEmote || labelLen > foundLen) { - foundEmote = &graphics::emotes[j]; - foundLen = labelLen; - } - } - } - if (foundEmote) { - tokens.emplace_back(true, String(foundEmote->label)); - pos += foundLen; - } else { - // Find next emote - int nextEmote = msgLen; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *found = strstr(msg + pos, label); - if (found && (found - msg) < nextEmote) { - nextEmote = found - msg; - } - } - int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); - if (textLen > 0) { - tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); - pos += textLen; - } else { - break; - } - } - } - return tokens; -} - -// Render a single emote token centered vertically on a row -static void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label) -{ - const graphics::Emote *emote = nullptr; - for (int j = 0; j < graphics::numEmotes; j++) { - if (label == graphics::emotes[j].label) { - emote = &graphics::emotes[j]; - break; - } - } - if (emote) { - int emoteYOffset = (rowHeight - emote->height) / 2; // vertically center the emote - display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); - nextX += emote->width + 2; // spacing between tokens - } -} - namespace graphics { extern int bannerSignalBars; @@ -130,10 +67,9 @@ CannedMessageModule::CannedMessageModule() : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") { this->loadProtoForModule(); - if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && - !CANNED_MESSAGE_MODULE_ENABLE) { + if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE) { LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); - this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; + this->updateState(CANNED_MESSAGE_RUN_STATE_DISABLED); disable(); } else { LOG_INFO("CannedMessageModule is enabled"); @@ -165,8 +101,7 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan currentMessageIndex = selectDestination; // This triggers the canned message list - runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); @@ -195,8 +130,7 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t lastChannel = channel; lastDestSet = true; - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); @@ -267,19 +201,20 @@ int CannedMessageModule::splitConfiguredMessages() } void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) { - if (graphics::currentResolution == graphics::ScreenResolution::High) { - if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: #%s", channels.getName(this->channel)); - } else { - display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); - } + (void)buffer; + + char header[96]; + if (this->dest == NODENUM_BROADCAST) { + const char *channelName = channels.getName(this->channel); + snprintf(header, sizeof(header), "To: #%s", channelName ? channelName : "?"); } else { - if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: #%.20s", channels.getName(this->channel)); - } else { - display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); - } + snprintf(header, sizeof(header), "To: @%s", getNodeName(this->dest)); } + + const int maxWidth = std::max(0, display->getWidth() - x); + char truncatedHeader[96]; + graphics::UIRenderer::truncateStringWithEmotes(display, header, truncatedHeader, sizeof(truncatedHeader), maxWidth); + graphics::UIRenderer::drawStringWithEmotes(display, x, y, truncatedHeader, FONT_HEIGHT_SMALL, 1, false); } void CannedMessageModule::resetSearch() @@ -373,6 +308,92 @@ bool CannedMessageModule::isCharInputAllowed() const { return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; } + +static int getRowHeightForEmoteText(const char *text, int minimumHeight, int emoteSpacing = 2) +{ + // Grow the row only when an emote is taller than the font. + const auto metrics = + graphics::EmoteRenderer::analyzeLine(nullptr, text ? text : "", 0, graphics::emotes, graphics::numEmotes, emoteSpacing); + return std::max(minimumHeight, metrics.tallestHeight + 2); +} + +static void drawCenteredEmoteText(OLEDDisplay *display, int x, int y, int rowHeight, const char *text, int emoteSpacing = 2) +{ + // Center mixed text and emotes inside the row height. + const auto metrics = graphics::EmoteRenderer::analyzeLine(nullptr, text ? text : "", FONT_HEIGHT_SMALL, graphics::emotes, + graphics::numEmotes, emoteSpacing); + const int contentHeight = std::max(FONT_HEIGHT_SMALL, metrics.tallestHeight); + const int drawY = y + ((rowHeight - contentHeight) / 2); + graphics::EmoteRenderer::drawStringWithEmotes(display, x, drawY, text ? text : "", FONT_HEIGHT_SMALL, graphics::emotes, + graphics::numEmotes, emoteSpacing, false); +} + +static size_t firstWrappedTokenLen(const char *text) +{ + // Fall back to one full emote or one UTF-8 glyph when width is tiny. + if (!text || !*text) + return 0; + + const size_t textLen = strlen(text); + size_t matchLen = 0; + if (graphics::EmoteRenderer::findEmoteAt(text, textLen, 0, matchLen, graphics::emotes, graphics::numEmotes)) + return matchLen; + + return graphics::EmoteRenderer::utf8CharLen(static_cast(text[0])); +} + +static void drawWrappedEmoteText(OLEDDisplay *display, int x, int y, const char *text, int maxWidth, int minimumRowHeight, + int emoteSpacing = 2) +{ + // Wrap onto multiple rows without splitting emotes. + if (!display || !text || maxWidth <= 0) + return; + + constexpr size_t kLineBufferSize = 256; + char lineBuffer[kLineBufferSize]; + const size_t textLen = strlen(text); + size_t offset = 0; + int yCursor = y; + + while (offset < textLen) { + size_t copied = graphics::EmoteRenderer::truncateToWidth(display, text + offset, lineBuffer, sizeof(lineBuffer), maxWidth, + "", graphics::emotes, graphics::numEmotes, emoteSpacing); + size_t consumed = copied; + + if (copied == 0) { + consumed = firstWrappedTokenLen(text + offset); + if (consumed == 0) + break; + + const size_t fallbackLen = std::min(consumed, sizeof(lineBuffer) - 1); + memcpy(lineBuffer, text + offset, fallbackLen); + lineBuffer[fallbackLen] = '\0'; + consumed = fallbackLen; + } else if (text[offset + copied] != '\0') { + // Prefer wrapping at the last space when a full line does not fit. + size_t lastSpace = copied; + while (lastSpace > 0 && lineBuffer[lastSpace - 1] != ' ') + --lastSpace; + + if (lastSpace > 0) { + consumed = lastSpace; + while (consumed > 0 && lineBuffer[consumed - 1] == ' ') + --consumed; + lineBuffer[consumed] = '\0'; + } + } + + if (lineBuffer[0]) { + const int rowHeight = getRowHeightForEmoteText(lineBuffer, minimumRowHeight, emoteSpacing); + drawCenteredEmoteText(display, x, yCursor, rowHeight, lineBuffer, emoteSpacing); + yCursor += rowHeight; + } + + offset += std::max(consumed, 1); + while (offset < textLen && text[offset] == ' ') + ++offset; + } +} /** * Main input event dispatcher for CannedMessageModule. * Routes keyboard/button/touch input to the correct handler based on the current runState. @@ -391,11 +412,10 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) // Matrix keypad: If matrix key, trigger action select for canned message if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, true); payload = INPUT_BROKER_MATRIXKEY; currentMessageIndex = event->kbchar - 1; lastTouchMillis = millis(); - requestFocus(); return 1; } @@ -433,8 +453,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } // Printable char (ASCII) opens free text compose if (event->kbchar >= 32 && event->kbchar <= 126) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); @@ -458,6 +477,20 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) return 0; } +void CannedMessageModule::updateState(cannedMessageModuleRunState newState, bool shouldRequestFocus) +{ + runState = newState; + if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + inputBroker->menuMode = + false; // Allow any key input to be sent to the message composer instead of being interpreted as menu navigation + } else { + inputBroker->menuMode = true; // Re-enable menu navigation for destination selection + } + if (shouldRequestFocus) { + requestFocus(); + } +} + bool CannedMessageModule::isUpEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_UP || @@ -482,15 +515,16 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event) if (event->kbchar != 0x09) return false; - runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT - : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + const cannedMessageModuleRunState targetState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; destIndex = 0; scrollIndex = 0; - // RESTORE THIS! - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + if (targetState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) updateDestinationSelectionList(); - requestFocus(); + + updateState(targetState, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; @@ -596,7 +630,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event } } - runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT, true); returnToCannedList = false; screen->forceDisplay(true); return 1; @@ -604,7 +638,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event // CANCEL if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { - runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT, true); returnToCannedList = false; searchQuery = ""; @@ -635,7 +669,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // Handle Cancel key: go inactive, clear UI state if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; @@ -653,10 +687,10 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // Handle up/down navigation if (isUp && messagesCount > 0) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_UP); handled = true; } else if (isDown && messagesCount > 0) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_DOWN); handled = true; } else if (isSelect) { const char *current = messages[currentMessageIndex]; @@ -664,7 +698,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // [Select Destination] triggers destination selection UI if (strcmp(current, "[Select Destination]") == 0) { returnToCannedList = true; - runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + updateState(CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, true); destIndex = 0; scrollIndex = 0; updateDestinationSelectionList(); // Make sure list is fresh @@ -675,7 +709,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // [Exit] returns to the main/inactive screen if (strcmp(current, "[Exit]") == 0) { // Set runState to inactive so we return to main UI - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); currentMessageIndex = -1; // Notify UI to regenerate frame set and redraw @@ -689,8 +723,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // [Free Text] triggers the free text input (virtual keyboard) #if defined(USE_VIRTUAL_KEYBOARD) if (strcmp(current, "[-- Free Text --]") == 0) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); @@ -709,7 +742,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (!text.empty()) { this->freetext = text.c_str(); this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE); currentMessageIndex = -1; UIFrameEvent e; @@ -726,7 +759,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo graphics::NotificationRenderer::resetBanner(); // Return to inactive state - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; @@ -756,12 +789,12 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { this->currentMessageIndex = savedIndex; this->payload = this->runState; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + this->updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); this->setIntervalFromNow(0); }); #else payload = runState; - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); #endif // Do not immediately set runState; wait for confirmation handled = true; @@ -787,7 +820,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) #if defined(USE_VIRTUAL_KEYBOARD) // Cancel (dismiss freetext screen) if (event->inputEvent == INPUT_BROKER_LEFT) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; @@ -833,7 +866,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) } // Touch enter/submit else if (keyTapped == "↵") { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); // Send the message! payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; shift = false; @@ -859,8 +892,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) // All hardware keys fall through to here (CardKB, physical, etc.) if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { - runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); screen->forceDisplay(); return true; } @@ -877,7 +909,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); lastTouchMillis = millis(); runOnce(); return true; @@ -912,7 +944,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) // Cancel (dismiss freetext screen) if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG || (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; @@ -980,14 +1012,14 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); } cursor += emoteInsert.length(); - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); screen->forceDisplay(); return 1; } // Cancel returns to freetext if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); screen->forceDisplay(); return 1; } @@ -1073,12 +1105,14 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha } else { sm.dest = dest; sm.type = MessageType::DM_TO_US; - // Only add as favorite if our role is NOT CLIENT_BASE - if (config.device.role != 12) { + // Only add as favorite if our role is not router-like (ROUTER, ROUTER_LATE, CLIENT_BASE) + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && + config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && + config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { LOG_INFO("Proactively adding %x as favorite node", dest); nodeDB->set_favorite(true, dest); } else { - LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", dest); + LOG_DEBUG("Not favoriting node %x because role is router-like", dest); } } sm.ackStatus = AckStatus::NONE; @@ -1094,9 +1128,8 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha playComboTune(); - this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE); this->payload = wantReplies ? 1 : 0; - requestFocus(); // Tell Screen to switch to TextMessage frame via UIFrameEvent UIFrameEvent e; @@ -1147,7 +1180,7 @@ int32_t CannedMessageModule::runOnce() } else { // Empty message, just go inactive LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } UIFrameEvent e; @@ -1164,7 +1197,7 @@ int32_t CannedMessageModule::runOnce() this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; @@ -1173,7 +1206,7 @@ int32_t CannedMessageModule::runOnce() } // Handle SENDING_ACTIVE state transition after virtual keyboard message else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; @@ -1185,7 +1218,7 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); // Clean up virtual keyboard if it exists during timeout if (graphics::NotificationRenderer::virtualKeyboard) { @@ -1199,7 +1232,7 @@ int32_t CannedMessageModule::runOnce() if (this->payload == 0) { // [Exit] button pressed - return to inactive state LOG_INFO("Processing [Exit] action - returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); @@ -1210,20 +1243,19 @@ int32_t CannedMessageModule::runOnce() this->cursor = 0; // Tell Screen to jump straight to the TextMessage frame - UIFrameEvent e; e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; this->notifyObservers(&e); // Now deactivate this module - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); - return INT32_MAX; // don’t fall back into canned list + return INT32_MAX; // don't fall back into canned list } else { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } } else { if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); return INT32_MAX; } if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { @@ -1238,17 +1270,16 @@ int32_t CannedMessageModule::runOnce() this->cursor = 0; // Tell Screen to jump straight to the TextMessage frame - UIFrameEvent e; e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; this->notifyObservers(&e); // Now deactivate this module - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); - return INT32_MAX; // don’t fall back into canned list + return INT32_MAX; // don't fall back into canned list } } else { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } } // fallback clean-up if nothing above returned @@ -1256,11 +1287,10 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; - UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); - // Immediately stop, don’t linger on canned screen + // Immediately stop, don't linger on canned screen return INT32_MAX; } // Highlight [Select Destination] initially when entering the message list @@ -1283,14 +1313,14 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = getPrevIndex(); this->freetext = ""; this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { if (this->messagesCount > 0) { this->currentMessageIndex = this->getNextIndex(); this->freetext = ""; this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { @@ -1678,55 +1708,51 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O int xOffset = 0; int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; - char entryText[64] = ""; + std::string entryText; // Draw Channels First if (itemIndex < numActiveChannels) { uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex)); + const char *channelName = channels.getName(channelIndex); + entryText = std::string("#") + (channelName ? channelName : "?"); } // Then Draw Nodes else { int nodeIndex = itemIndex - numActiveChannels; if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node && node->user.long_name) { - strncpy(entryText, node->user.long_name, sizeof(entryText) - 1); - entryText[sizeof(entryText) - 1] = '\0'; + if (node) { + if (display->getWidth() <= 64) { + entryText = node->user.short_name; + } else if (node->user.long_name[0]) { + entryText = node->user.long_name; + } else { + entryText = node->user.short_name; + } } + int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - ((node && node->is_favorite) ? 10 : 0); if (availWidth < 0) availWidth = 0; - - size_t origLen = strlen(entryText); - while (entryText[0] && display->getStringWidth(entryText) > availWidth) { - entryText[strlen(entryText) - 1] = '\0'; - } - if (strlen(entryText) < origLen) { - strcat(entryText, "..."); - } + char truncatedEntry[96]; + graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), + availWidth); + entryText = truncatedEntry; // Prepend "* " if this is a favorite if (node && node->is_favorite) { - size_t len = strlen(entryText); - if (len + 2 < sizeof(entryText)) { - memmove(entryText + 2, entryText, len + 1); - entryText[0] = '*'; - entryText[1] = ' '; - } - } - if (node) { - if (display->getWidth() <= 64) { - snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); - } + entryText = "* " + entryText; } + graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), + availWidth); + entryText = truncatedEntry; } } - if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) - strcpy(entryText, "?"); + if (entryText.empty() || entryText == "Unknown") + entryText = "?"; // Highlight background (if selected) if (itemIndex == destIndex) { @@ -1736,7 +1762,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O } // Draw entry text - display->drawString(xOffset + 2, yOffset, entryText); + graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText.c_str(), FONT_HEIGHT_SMALL, 1, false); display->setColor(WHITE); // Draw key icon (after highlight) @@ -1777,15 +1803,10 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla { const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height const int headerMargin = 2; // Extra pixels below header - const int labelGap = 6; + const int labelGap = 4; const int bitmapGapX = 4; - // Find max emote height (assume all same, or precalculated) - int maxEmoteHeight = 0; - for (int i = 0; i < graphics::numEmotes; ++i) - if (graphics::emotes[i].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[i].height; - + const int maxEmoteHeight = graphics::EmoteRenderer::maxEmoteHeight(); const int rowHeight = maxEmoteHeight + 2; // Place header at top, then compute start of emote list @@ -1832,14 +1853,16 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla display->setColor(BLACK); } - // Emote bitmap (left), 1px margin from highlight bar top - int emoteY = rowY + 1; - display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); + // Emote bitmap (left), centered inside the row + int labelStartX = x + bitmapGapX; + const int emoteY = rowY + ((rowHeight - emote.height) / 2); + display->drawXbm(labelStartX, emoteY, emote.width, emote.height, emote.bitmap); + labelStartX += emote.width; // Emote label (right of bitmap) display->setFont(FONT_MEDIUM); int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); - display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); + display->drawString(labelStartX + labelGap, labelY, emote.label); if (emoteIdx == emotePickerIndex) display->setColor(WHITE); @@ -1999,91 +2022,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st { int inputY = 0 + y + FONT_HEIGHT_SMALL; String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); - - // Tokenize input into (isEmote, token) pairs - const char *msg = msgWithCursor.c_str(); - std::vector> tokens = tokenizeMessageWithEmotes(msg); - - // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed) - std::vector>> lines; - std::vector> currentLine; - int lineWidth = 0; - int maxWidth = display->getWidth(); - for (auto &token : tokens) { - if (token.first) { - // Emote - int tokenWidth = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - if (token.second == graphics::emotes[j].label) { - tokenWidth = graphics::emotes[j].width + 2; - break; - } - } - if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - currentLine.push_back(token); - lineWidth += tokenWidth; - } else { - // Text: split by words and wrap inside word if needed - String text = token.second; - int pos = 0; - while (pos < static_cast(text.length())) { - // Find next space (or end) - int spacePos = text.indexOf(' ', pos); - int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space - String word = text.substring(pos, endPos); - int wordWidth = display->getStringWidth(word); - - if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - // If word itself too big, split by character - if (wordWidth > maxWidth) { - uint16_t charPos = 0; - while (charPos < word.length()) { - String oneChar = word.substring(charPos, charPos + 1); - int charWidth = display->getStringWidth(oneChar); - if (lineWidth + charWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - currentLine.push_back({false, oneChar}); - lineWidth += charWidth; - charPos++; - } - } else { - currentLine.push_back({false, word}); - lineWidth += wordWidth; - } - pos = endPos; - } - } - } - if (!currentLine.empty()) - lines.push_back(currentLine); - - // Draw lines with emotes - int rowHeight = FONT_HEIGHT_SMALL; - int yLine = inputY; - for (auto &line : lines) { - int nextX = x; - for (const auto &token : line) { - if (token.first) { - // Emote rendering centralized in helper - renderEmote(display, nextX, yLine, rowHeight, token.second); - } else { - display->drawString(nextX, yLine, token.second); - nextX += display->getStringWidth(token.second); - } - } - yLine += rowHeight; - } + drawWrappedEmoteText(display, x, inputY, msgWithCursor.c_str(), display->getWidth() - x, FONT_HEIGHT_SMALL); } #endif return; @@ -2098,7 +2037,6 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; int topMsg; - std::vector rowHeights; int _visibleRows; // Draw header (To: ...) @@ -2114,36 +2052,15 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st : 0; int countRows = std::min(messagesCount, _visibleRows); - // Build per-row max height based on all emotes in line - for (int i = 0; i < countRows; i++) { - const char *msg = getMessageByIndex(topMsg + i); - int maxEmoteHeight = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *search = msg; - while ((search = strstr(search, label))) { - if (graphics::emotes[j].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[j].height; - search += strlen(label); // Advance past this emote - } - } - rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); - } - // Draw all message rows with multi-emote support int yCursor = listYOffset; for (int vis = 0; vis < countRows; vis++) { int msgIdx = topMsg + vis; int lineY = yCursor; const char *msg = getMessageByIndex(msgIdx); - int rowHeight = rowHeights[vis]; + int rowHeight = getRowHeightForEmoteText(msg, baseRowSpacing); bool _highlight = (msgIdx == currentMessageIndex); - // Multi-emote tokenization - std::vector> tokens = tokenizeMessageWithEmotes(msg); - // Vertically center based on rowHeight int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; @@ -2160,17 +2077,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int nextX = x + (_highlight ? 2 : 0); #endif - // Draw all tokens left to right - for (const auto &token : tokens) { - if (token.first) { - // Emote rendering centralized in helper - renderEmote(display, nextX, lineY, rowHeight, token.second); - } else { - // Text - display->drawString(nextX, lineY + textYOffset, token.second); - nextX += display->getStringWidth(token.second); - } - } + if (msg && *msg) + drawCenteredEmoteText(display, nextX, lineY, rowHeight, msg); #ifndef USE_EINK if (_highlight) display->setColor(WHITE); diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 3d7c09d87..f6cb4d011 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -27,10 +27,6 @@ enum CannedMessageModuleIconType { shift, backspace, space, enter }; #define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 #define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 -#ifndef CANNED_MESSAGE_MODULE_ENABLE -#define CANNED_MESSAGE_MODULE_ENABLE 0 -#endif - // ============================ // Data Structures // ============================ @@ -187,6 +183,8 @@ class CannedMessageModule : public SinglePortModule, public Observable -#ifdef HAS_NCP5623 -#include -#endif - -#ifdef HAS_LP5562 -#include -#endif - -#ifdef HAS_NEOPIXEL -#include -#endif - -#ifdef UNPHONE -#include "unPhone.h" -extern unPhone unphone; -#endif - #if defined(HAS_RGB_LED) +#include "AmbientLightingThread.h" uint8_t red = 0; uint8_t green = 0; uint8_t blue = 0; @@ -123,32 +107,6 @@ int32_t ExternalNotificationModule::runOnce() green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7 blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7 white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0; -#ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.setColor(red, green, blue); - } -#endif -#ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.setColor(red, green, blue, white); - } -#endif -#ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic - analogWrite(RGBLED_GREEN, 255 - green); - analogWrite(RGBLED_BLUE, 255 - blue); -#elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, red); - analogWrite(RGBLED_GREEN, green); - analogWrite(RGBLED_BLUE, blue); -#endif -#ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); - pixels.show(); -#endif -#ifdef UNPHONE - unphone.rgb(red, green, blue); -#endif if (ascending) { // fade in brightnessIndex++; if (brightnessIndex == (sizeof(brightnessValues) - 1)) { @@ -245,6 +203,10 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on) default: if (output > 0) digitalWrite(output, (moduleConfig.external_notification.active ? on : !on)); +#ifdef PCA_LED_NOTIFICATION + io.digitalWrite(PCA_LED_NOTIFICATION, on); + +#endif break; } @@ -255,34 +217,9 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on) blue = 0; white = 0; } + ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue); #endif -#ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.setColor(red, green, blue); - } -#endif -#ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.setColor(red, green, blue, white); - } -#endif -#ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic - analogWrite(RGBLED_GREEN, 255 - green); - analogWrite(RGBLED_BLUE, 255 - blue); -#elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, red); - analogWrite(RGBLED_GREEN, green); - analogWrite(RGBLED_BLUE, blue); -#endif -#ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); - pixels.show(); -#endif -#ifdef UNPHONE - unphone.rgb(red, green, blue); -#endif #ifdef HAS_DRV2605 if (on) { drv.go(); @@ -407,33 +344,6 @@ ExternalNotificationModule::ExternalNotificationModule() LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio); } } -#ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.begin(); - rgb.setCurrent(10); - } -#endif -#ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.begin(); - rgbw.setCurrent(20); - } -#endif -#ifdef RGBLED_RED - pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins - pinMode(RGBLED_GREEN, OUTPUT); - pinMode(RGBLED_BLUE, OUTPUT); -#endif -#ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed - analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off - analogWrite(RGBLED_BLUE, 255); -#endif -#ifdef HAS_NEOPIXEL - pixels.begin(); // Initialise the pixel(s) - pixels.clear(); // Set all pixel colors to 'off' - pixels.setBrightness(moduleConfig.ambient_lighting.current); -#endif } else { LOG_INFO("External Notification Module Disabled"); disable(); @@ -442,13 +352,8 @@ ExternalNotificationModule::ExternalNotificationModule() ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) { + // Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles if (moduleConfig.external_notification.enabled && !isSilenced) { -#ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 56); - drv.setWaveform(2, 0); - drv.go(); -#endif if (!isFromUs(&mp)) { // Check if the message contains a bell character. Don't do this loop for every pin, just once. auto &p = mp.decoded; @@ -456,132 +361,90 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP for (size_t i = 0; i < p.payload.size; i++) { if (p.payload.bytes[i] == ASCII_BELL) { containsBell = true; + break; } } - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); + const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); // If we receive a broadcast message, apply channel mute setting // If we receive a direct message and the receipent is us, apply DM mute setting // Else we just handle it as not muted. - const bool directToUs = !isBroadcast(mp.to) && isToUs(&mp); - bool is_muted = directToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) - : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); + const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp); + bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) + : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); - if (moduleConfig.external_notification.alert_bell) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell"); - isNagging = true; - setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } + const bool buzzerModeIsDirectOnly = + (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY); - if (moduleConfig.external_notification.alert_bell_vibra) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Vibra)"); - isNagging = true; - setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } + // Each output evaluates its own alert condition independently: + // alert_bell_* fires only when a bell character is present. + // alert_message_* fires on any non-muted message. - if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { - if (containsBell) { - LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); - isNagging = true; - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { -#ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else -#endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } - } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } - } + // Alert when receiving a bell = alertBell: true + // Alert when receiving a message = alertMessage: true + const bool genericShouldAlert = (moduleConfig.external_notification.alert_bell && containsBell) || + (moduleConfig.external_notification.alert_message && !is_muted); - if (moduleConfig.external_notification.alert_message && !is_muted) { - LOG_INFO("externalNotificationModule - Notification Module"); + // Alert GPIO Vibra when receiving a bell = alertBellVibra: true + // Alert GPIO Vibra when receiving a message = alertMessageVibra: true + const bool vibraShouldAlert = (moduleConfig.external_notification.alert_bell_vibra && containsBell) || + (moduleConfig.external_notification.alert_message_vibra && !is_muted); + + // Alert GPIO Buzzer when receiving a bell = alertBellBuzzer: true + // Alert GPIO Buzzer when receiving a message = alertMessageBuzzer: true + const bool buzzerShouldAlert = canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) || + (moduleConfig.external_notification.alert_message_buzzer && !is_muted)); + + if (genericShouldAlert || vibraShouldAlert || buzzerShouldAlert) { + nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout + ? (moduleConfig.external_notification.nag_timeout * 1000) + : moduleConfig.external_notification.output_ms); + LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); isNagging = true; + } + + if (genericShouldAlert) { + LOG_INFO("externalNotificationModule - Generic alert"); setExternalState(0, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } } - if (moduleConfig.external_notification.alert_message_vibra && !is_muted) { - LOG_INFO("externalNotificationModule - Notification Module (Vibra)"); - isNagging = true; + if (vibraShouldAlert) { + LOG_INFO("externalNotificationModule - Vibra alert"); setExternalState(1, true); - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; + } + + if (buzzerShouldAlert) { + LOG_INFO("externalNotificationModule - Buzzer alert"); + if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us +#ifdef HAS_DRV2605 + drv.setWaveform(0, 16); // Long buzzer 100% + drv.setWaveform(1, 0); // Pause + drv.setWaveform(2, 16); + drv.setWaveform(3, 0); + drv.setWaveform(4, 16); + drv.setWaveform(5, 0); + drv.setWaveform(6, 16); + drv.setWaveform(7, 0); + drv.go(); +#endif + + if (moduleConfig.external_notification.use_i2s_as_buzzer) { +#ifdef HAS_I2S + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); +#endif + } else if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } else { + setExternalState(2, true); + } } } - if (moduleConfig.external_notification.alert_message_buzzer && !is_muted) { - LOG_INFO("externalNotificationModule - Notification Module (Buzzer)"); - if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || - (!isBroadcast(mp.to) && isToUs(&mp))) { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us - isNagging = true; -#ifdef T_LORA_PAGER - if (canBuzz()) { - drv.setWaveform(0, 16); // Long buzzer 100% - drv.setWaveform(1, 0); // Pause - drv.setWaveform(2, 16); - drv.setWaveform(3, 0); - drv.setWaveform(4, 16); - drv.setWaveform(5, 0); - drv.setWaveform(6, 16); - drv.setWaveform(7, 0); - drv.go(); - } -#endif - if (!moduleConfig.external_notification.use_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { - setExternalState(2, true); - } else { -#ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else -#endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } - } - if (moduleConfig.external_notification.nag_timeout) { - nagCycleCutoff = millis() + moduleConfig.external_notification.nag_timeout * 1000; - } else { - nagCycleCutoff = millis() + moduleConfig.external_notification.output_ms; - } - } else { - // Don't beep if buzzer mode is "direct messages only" and it is no direct message - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); - } - } setIntervalFromNow(0); // run once so we know if we should do something } } else { @@ -657,4 +520,4 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event) return 1; } return 0; -} \ No newline at end of file +} diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index f667f7be9..94b021360 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -5,6 +5,11 @@ #include "configuration.h" #include "input/InputBroker.h" +#ifdef HAS_RGB_LED +#include "AmbientLightingThread.h" +extern AmbientLightingThread *ambientLightingThread; +#endif + #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) #include #else diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 3b8225763..6d0255d53 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -123,7 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) // generate nonce updateState(); if (currentState != KEY_VERIFICATION_IDLE) { - IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;) + IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;) return false; } currentNonce = random(); @@ -259,7 +259,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) p->priority = meshtastic_MeshPacket_Priority_HIGH; service->sendToMesh(p, RX_SRC_LOCAL, true); currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; - IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);) + IF_SCREEN(screen->requestMenu(graphics::menuHandler::KeyVerificationFinalPrompt);) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index e17868baf..d3ab9076d 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -1,24 +1,11 @@ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_INPUTBROKER #include "buzz/BuzzerFeedbackThread.h" -#include "input/ExpressLRSFiveWay.h" -#include "input/InputBroker.h" -#include "input/RotaryEncoderImpl.h" -#include "input/RotaryEncoderInterruptImpl1.h" -#include "input/SerialKeyboardImpl.h" -#include "input/UpDownInterruptImpl1.h" -#include "input/i2cButton.h" #include "modules/SystemCommandsModule.h" -#if HAS_TRACKBALL -#include "input/TrackballInterruptImpl1.h" #endif - #include "modules/StatusLEDModule.h" - -#if !MESHTASTIC_EXCLUDE_I2C -#include "input/cardKbI2cImpl.h" -#endif -#include "input/kbMatrixImpl.h" +#if !MESHTASTIC_EXCLUDE_REPLYBOT +#include "ReplyBotModule.h" #endif #if !MESHTASTIC_EXCLUDE_PKI #include "KeyVerificationModule.h" @@ -51,6 +38,9 @@ #include "modules/PowerStressModule.h" #endif #include "modules/RoutingModule.h" +#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT +#include "modules/TrafficManagementModule.h" +#endif #include "modules/TextMessageModule.h" #if !MESHTASTIC_EXCLUDE_TRACEROUTE #include "modules/TraceRouteModule.h" @@ -59,8 +49,6 @@ #include "modules/WaypointModule.h" #endif #if ARCH_PORTDUINO -#include "input/LinuxInputImpl.h" -#include "input/SeesawRotary.h" #include "modules/Telemetry/HostMetrics.h" #if !MESHTASTIC_EXCLUDE_STOREFORWARD #include "modules/StoreForwardModule.h" @@ -71,11 +59,15 @@ #endif #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "main.h" -#include "modules/Telemetry/AirQualityTelemetry.h" #include "modules/Telemetry/EnvironmentTelemetry.h" #include "modules/Telemetry/HealthTelemetry.h" #include "modules/Telemetry/Sensor/TelemetrySensor.h" #endif +#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +#include "main.h" +#include "modules/Telemetry/AirQualityTelemetry.h" +#include "modules/Telemetry/Sensor/TelemetrySensor.h" +#endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY #include "modules/Telemetry/PowerTelemetry.h" #endif @@ -108,7 +100,13 @@ #if !MESHTASTIC_EXCLUDE_DROPZONE #include "modules/DropzoneModule.h" #endif +#if !MESHTASTIC_EXCLUDE_STATUS +#include "modules/StatusMessageModule.h" +#endif +#if defined(HAS_HARDWARE_WATCHDOG) +#include "watchdog/watchdogThread.h" +#endif /** * Create module instances here. If you are adding a new module, you must 'new' it here (or somewhere else) */ @@ -121,8 +119,16 @@ void setupModules() buzzerFeedbackThread = new BuzzerFeedbackThread(); } #endif -#if defined(LED_CHARGE) || defined(LED_PAIRING) statusLEDModule = new StatusLEDModule(); +#if !MESHTASTIC_EXCLUDE_REPLYBOT + new ReplyBotModule(); +#endif + +#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT + // Instantiate only when enabled to avoid extra memory use and background work. + if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled) { + trafficManagementModule = new TrafficManagementModule(); + } #endif #if !MESHTASTIC_EXCLUDE_ADMIN @@ -165,6 +171,9 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_DROPZONE dropzoneModule = new DropzoneModule(); #endif +#if !MESHTASTIC_EXCLUDE_STATUS + statusMessageModule = new StatusMessageModule(); +#endif #if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE new GenericThreadModule(); #endif @@ -179,63 +188,6 @@ void setupModules() #endif // Example: Put your module here // new ReplyModule(); -#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { -#if defined(T_LORA_PAGER) - // use a special FSM based rotary encoder version for T-LoRa Pager - rotaryEncoderImpl = new RotaryEncoderImpl(); - if (!rotaryEncoderImpl->init()) { - delete rotaryEncoderImpl; - rotaryEncoderImpl = nullptr; - } -#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) - upDownInterruptImpl1 = new UpDownInterruptImpl1(); - if (!upDownInterruptImpl1->init()) { - delete upDownInterruptImpl1; - upDownInterruptImpl1 = nullptr; - } -#else - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } -#endif - cardKbI2cImpl = new CardKbI2cImpl(); - cardKbI2cImpl->init(); -#if defined(M5STACK_UNITC6L) - i2cButton = new i2cButtonThread("i2cButtonThread"); -#endif -#ifdef INPUTBROKER_MATRIX_TYPE - kbMatrixImpl = new KbMatrixImpl(); - kbMatrixImpl->init(); -#endif // INPUTBROKER_MATRIX_TYPE -#ifdef INPUTBROKER_SERIAL_TYPE - aSerialKeyboardImpl = new SerialKeyboardImpl(); - aSerialKeyboardImpl->init(); -#endif // INPUTBROKER_MATRIX_TYPE - } -#endif // HAS_BUTTON -#if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { - seesawRotary = new SeesawRotary("SeesawRotary"); - if (!seesawRotary->init()) { - delete seesawRotary; - seesawRotary = nullptr; - } - aLinuxInputImpl = new LinuxInputImpl(); - aLinuxInputImpl->init(); - } -#endif -#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - trackballInterruptImpl1 = new TrackballInterruptImpl1(); - trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); - } -#endif -#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE - expressLRSFiveWayInput = new ExpressLRSFiveWay(); -#endif #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { cannedMessageModule = new CannedMessageModule(); @@ -304,6 +256,9 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS if (moduleConfig.has_range_test && moduleConfig.range_test.enabled) new RangeTestModule(); +#endif +#if defined(HAS_HARDWARE_WATCHDOG) + watchdogThread = new WatchdogThread(); #endif // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra // acks diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index a568505ae..4de479241 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -5,6 +5,7 @@ #include "NodeStatus.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "configuration.h" #include "main.h" #include @@ -29,7 +30,8 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes auto p = *pptr; - if (mp.decoded.want_response) { + // Suppress replies to senders we've replied to recently (12H window) + if (mp.decoded.want_response && !isFromUs(&mp)) { const NodeNum sender = getFrom(&mp); const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); auto it = lastNodeInfoSeen.find(sender); @@ -116,9 +118,21 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha } } +void NodeInfoModule::triggerImmediateNodeInfoCheck() +{ + LOG_DEBUG("NodeInfo: scheduling immediate periodic check"); + setIntervalFromNow(0); +} + meshtastic_MeshPacket *NodeInfoModule::allocReply() { - if (suppressReplyForCurrentRequest) { + // Only apply suppression when actually replying to someone else's request, not for periodic broadcasts. + const bool isReplyingToExternalRequest = currentRequest && + currentRequest->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + currentRequest->decoded.portnum == meshtastic_PortNum_NODEINFO_APP && + currentRequest->decoded.want_response && !isFromUs(currentRequest); + + if (suppressReplyForCurrentRequest && isReplyingToExternalRequest) { LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); ignoreRequest = true; suppressReplyForCurrentRequest = false; @@ -133,11 +147,12 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply() // Use graduated scaling based on active mesh size (10 minute base, scales with congestion coefficient) uint32_t timeoutMs = Default::getConfiguredOrDefaultMsScaled(0, 10 * 60, nodeStatus->getNumOnline()); - if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, timeoutMs)) { + uint32_t lastNodeInfo = transmitHistory ? transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP) : 0; + if (!shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) { LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000); ignoreRequest = true; // Mark it as ignored for MeshModule return NULL; - } else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) { + } else if (shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) { // For interactive/urgent requests (e.g., user-triggered or implicit requests), use a shorter 60s timeout LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago"); ignoreRequest = true; @@ -148,7 +163,7 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply() // Strip the public key if the user is licensed if (u.is_licensed && u.public_key.size > 0) { - u.public_key.bytes[0] = 0; + memset(u.public_key.bytes, 0, sizeof(u.public_key.bytes)); u.public_key.size = 0; } @@ -159,7 +174,8 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply() strcpy(u.id, nodeDB->getNodeId().c_str()); LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); return allocDataProtobuf(u); } } diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index d16fbeac2..9b3b66cae 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -24,6 +24,12 @@ class NodeInfoModule : public ProtobufModule, private concurren void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0, bool _shorterTimeout = false); + /** + * Schedule an immediate NodeInfo periodic check. + * Used when external conditions change (for example time source quality). + */ + void triggerImmediateNodeInfoCheck(); + protected: /** Called to handle a particular incoming message @@ -42,7 +48,6 @@ class NodeInfoModule : public ProtobufModule, private concurren virtual int32_t runOnce() override; private: - uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh bool shorterTimeout = false; bool suppressReplyForCurrentRequest = false; std::map lastNodeInfoSeen; diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 3c77df704..c435beb67 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -6,6 +6,7 @@ #include "NodeDB.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "TypeConversions.h" #include "airtime.h" #include "configuration.h" @@ -27,6 +28,15 @@ PositionModule::PositionModule() isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others nodeStatusObserver.observe(&nodeStatus->onNewStatus); + // Seed throttle timer from persisted transmit history so we don't re-broadcast immediately after reboot + if (transmitHistory) { + uint32_t restored = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); + if (restored != 0) { + lastGpsSend = restored; + LOG_INFO("Position: restored lastGpsSend from transmit history"); + } + } + if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { setIntervalFromNow(setStartDelay()); @@ -438,6 +448,8 @@ int32_t PositionModule::runOnce() lastGpsLatitude = node->position.latitude_i; lastGpsLongitude = node->position.longitude_i; + if (transmitHistory) + transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); sendOurPosition(); if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { sendLostAndFoundText(); @@ -455,7 +467,11 @@ int32_t PositionModule::runOnce() if (smartPosition.hasTraveledOverThreshold && Throttle::execute( &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, - []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { + []() { +#ifdef GPS_DEBUG + LOG_DEBUG("Skip send smart broadcast due to time throttling"); +#endif + })) { LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " "minTimeInterval=%ims)", @@ -547,7 +563,11 @@ void PositionModule::handleNewPosition() if (smartPosition.hasTraveledOverThreshold && Throttle::execute( &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, - []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { + []() { +#ifdef GPS_DEBUG + LOG_DEBUG("Skip send smart broadcast due to time throttling"); +#endif + })) { LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " "minTimeInterval=%ims)", localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend, diff --git a/src/modules/PowerStressModule.cpp b/src/modules/PowerStressModule.cpp index d487fe6fc..1c073a10a 100644 --- a/src/modules/PowerStressModule.cpp +++ b/src/modules/PowerStressModule.cpp @@ -1,5 +1,4 @@ #include "PowerStressModule.h" -#include "Led.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" @@ -78,10 +77,12 @@ int32_t PowerStressModule::runOnce() switch (p.cmd) { case meshtastic_PowerStressMessage_Opcode_LED_ON: - ledForceOn.set(true); + // FIXME - implement + // ledForceOn.set(true); break; case meshtastic_PowerStressMessage_Opcode_LED_OFF: - ledForceOn.set(false); + // FIXME - implement + // ledForceOn.set(false); break; case meshtastic_PowerStressMessage_Opcode_GPS_ON: // FIXME - implement diff --git a/src/modules/ReplyBotModule.cpp b/src/modules/ReplyBotModule.cpp new file mode 100644 index 000000000..d391bf093 --- /dev/null +++ b/src/modules/ReplyBotModule.cpp @@ -0,0 +1,183 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_REPLYBOT +/* + * ReplyBotModule.cpp + * + * This module implements a simple reply bot for the Meshtastic firmware. It listens for + * specific text commands ("/ping", "/hello" and "/test") delivered either via a direct + * message (DM) or a broadcast on the primary channel. When a supported command is + * received the bot responds with a short status message that includes the hop count + * (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming + * the network it enforces a per‑sender cooldown between responses. By default the + * module is disabled. See the official firmware documentation for guidance on adding modules. + * To enable this module, set `#undef MESHTASTIC_EXCLUDE_REPLYBOT` in your variant.h file. + */ + +#include "Channels.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "ReplyBotModule.h" +#include "mesh/MeshTypes.h" + +#include +#include +#include + +// +// Rate limiting data structures +// +// Each sender is tracked in a small ring buffer. When a message arrives from a +// sender we check the last time we responded to them. If the difference is +// less than the configured cooldown (different values for DM vs broadcast) +// the message is ignored; otherwise we update the last response time and +// proceed with replying. + +struct ReplyBotCooldownEntry { + uint32_t from = 0; + uint32_t lastMs = 0; +}; + +static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size +static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs +static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts + +static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS]; +static uint8_t replybotCooldownIdx = 0; + +// Return true if a reply should be rate‑limited for this sender, updating the +// entry table as needed. +static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs) +{ + const uint32_t now = millis(); + for (auto &e : replybotCooldown) { + if (e.from == from) { + // Found existing entry; check if cooldown expired + if ((uint32_t)(now - e.lastMs) < cooldownMs) { + return true; + } + e.lastMs = now; + return false; + } + } + // No entry found – insert new sender into the ring + replybotCooldown[replybotCooldownIdx].from = from; + replybotCooldown[replybotCooldownIdx].lastMs = now; + replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS; + return false; +} + +// Constructor – registers a single text port and marks the module promiscuous +// so that broadcast messages on the primary channel are visible. +ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP) +{ + isPromiscuous = true; +} + +void ReplyBotModule::setup() +{ + // In future we may add a protobuf configuration; for now the module is + // always enabled when compiled in. +} + +// Determine whether we want to process this packet. We only care about +// plain text messages addressed to our port. +bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p) +{ + return (p && p->decoded.portnum == ourPortNum); +} + +ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + // Accept only direct messages to us or broadcasts on the Primary channel + // (regardless of modem preset: LongFast, MediumFast, etc). + + const uint32_t ourNode = nodeDB->getNodeNum(); + const bool isDM = (mp.to == ourNode); + const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to); + if (!isDM && !isPrimaryChannel) { + return ProcessMessage::CONTINUE; + } + + // Ignore empty payloads + if (mp.decoded.payload.size == 0) { + return ProcessMessage::CONTINUE; + } + + // Copy payload into a null‑terminated buffer + char buf[260]; + memset(buf, 0, sizeof(buf)); + size_t n = mp.decoded.payload.size; + if (n > sizeof(buf) - 1) + n = sizeof(buf) - 1; + memcpy(buf, mp.decoded.payload.bytes, n); + + // React only to supported slash commands + if (!isCommand(buf)) { + return ProcessMessage::CONTINUE; + } + + // Apply rate limiting per sender depending on DM/broadcast + const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS; + if (replybotRateLimited(mp.from, cooldownMs)) { + return ProcessMessage::CONTINUE; + } + + // Compute hop count indicator – if the relay_node is non‑zero we know + // there was at least one relay. Some firmware builds support a hop_start + // field which could be used for more accurate counts, but here we use + // the available relay_node flag only. + // int hopsAway = mp.hop_start - mp.hop_limit; + int hopsAway = getHopsAway(mp); + + // Normalize RSSI: if positive adjust down by 200 to align with typical values + int rssi = mp.rx_rssi; + if (rssi > 0) { + rssi -= 200; + } + float snr = mp.rx_snr; + + // Build the reply message and send it back via DM + char reply[96]; + snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr); + sendDm(mp, reply); + return ProcessMessage::CONTINUE; +} + +// Check if the message starts with one of the supported commands. Leading +// whitespace is skipped and commands must be followed by end‑of‑string or +// whitespace. +bool ReplyBotModule::isCommand(const char *msg) const +{ + if (!msg) + return false; + while (*msg == ' ' || *msg == '\t') + msg++; + auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast(c)); }; + if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5])) + return true; + if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6])) + return true; + if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5])) + return true; + return false; +} + +// Send a direct message back to the originating node. +void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text) +{ + if (!text) + return; + meshtastic_MeshPacket *p = allocDataPacket(); + p->to = rx.from; + p->channel = rx.channel; + p->want_ack = false; + p->decoded.want_response = false; + size_t len = strlen(text); + if (len > sizeof(p->decoded.payload.bytes)) { + len = sizeof(p->decoded.payload.bytes); + } + p->decoded.payload.size = len; + memcpy(p->decoded.payload.bytes, text, len); + service->sendToMesh(p); +} +#endif // MESHTASTIC_EXCLUDE_REPLYBOT \ No newline at end of file diff --git a/src/modules/ReplyBotModule.h b/src/modules/ReplyBotModule.h new file mode 100644 index 000000000..a5a8f6bb4 --- /dev/null +++ b/src/modules/ReplyBotModule.h @@ -0,0 +1,19 @@ +#pragma once +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_REPLYBOT +#include "SinglePortModule.h" +#include "mesh/generated/meshtastic/mesh.pb.h" + +class ReplyBotModule : public SinglePortModule +{ + public: + ReplyBotModule(); + void setup() override; + bool wantPacket(const meshtastic_MeshPacket *p) override; + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + protected: + bool isCommand(const char *msg) const; + void sendDm(const meshtastic_MeshPacket &rx, const char *text); +}; +#endif // MESHTASTIC_EXCLUDE_REPLYBOT \ No newline at end of file diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index e9e1fc786..85e7f8c06 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -20,10 +20,11 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && (nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user)) return false; - } else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) { - // Don't let licensed users to rebroadcast packets from unlicensed users + } else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) || + (nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) { + // Don't let licensed users to rebroadcast packets to or from unlicensed users // If we know they are in-fact unlicensed - LOG_DEBUG("Packet from unlicensed user, ignoring packet"); + LOG_DEBUG("Packet to or from unlicensed user, ignoring packet"); return false; } @@ -67,6 +68,8 @@ uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) #if !(EVENTMODE) // This falls through to the default. return hopsUsed; // If the request used more hops than the limit, use the same amount of hops #endif + } else if (mp.hop_start == 0) { + return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path. } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different } diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 5699f3be6..20d4d7d8c 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -63,29 +63,26 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(TTGO_T_ECHO_PLUS) || defined(CANARYONE) || defined(MESHLINK) || \ - defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M5) || \ - defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M3) || defined(MUZI_BASE) - -SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial; -#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) || defined(EBYTE_E77_MBL) -SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial1; -#else -SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Serial") -{ - api_type = TYPE_SERIAL; -} -static Print *serialPrint = &Serial2; +#ifndef SERIAL_PRINT_PORT +#define SERIAL_PRINT_PORT 2 #endif +#if SERIAL_PRINT_PORT == 0 +#define SERIAL_PRINT_OBJECT Serial +#elif SERIAL_PRINT_PORT == 1 +#define SERIAL_PRINT_OBJECT Serial1 +#elif SERIAL_PRINT_PORT == 2 +#define SERIAL_PRINT_OBJECT Serial2 +#else +#error "Unsupported SERIAL_PRINT_PORT value. Allowed values are 0, 1, or 2." +#endif + +SerialModule::SerialModule() : StreamAPI(&SERIAL_PRINT_OBJECT), concurrency::OSThread("Serial") +{ + api_type = TYPE_SERIAL; +} +static Print *serialPrint = &SERIAL_PRINT_OBJECT; + char serialBytes[512]; size_t serialPayloadSize; @@ -205,9 +202,7 @@ int32_t SerialModule::runOnce() Serial.begin(baud); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } -#elif !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ - !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) +#elif SERIAL_PRINT_PORT != 0 if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { #ifdef ARCH_RP2040 @@ -264,9 +259,7 @@ int32_t SerialModule::runOnce() } } -#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(MESHLINK) && \ - !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M3) && !defined(ELECROW_ThinkNode_M4) && \ - !defined(ELECROW_ThinkNode_M5) && !defined(MUZI_BASE) +#if SERIAL_PRINT_PORT != 0 else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); @@ -540,11 +533,7 @@ ParsedLine parseLine(const char *line) */ void SerialModule::processWXSerial() { -#if !defined(TTGO_T_ECHO) && !defined(TTGO_T_ECHO_PLUS) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && \ - !defined(CONFIG_IDF_TARGET_ESP32C6) && !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && \ - !defined(ELECROW_ThinkNode_M3) && \ - !defined(ELECROW_ThinkNode_M4) && \ - !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) && !defined(MUZI_BASE) +#if SERIAL_PRINT_PORT != 0 && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. @@ -662,7 +651,7 @@ void SerialModule::processWXSerial() LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr), strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum); } - if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) { + if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis) && velCount > 0 && dirCount > 0) { // calculate averages and send to the mesh float velAvg = 1.0 * velSum / velCount; diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index fed035513..f828f4a16 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -13,15 +13,16 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus); +#if !MESHTASTIC_EXCLUDE_INPUTBROKER if (inputBroker) inputObserver.observe(inputBroker); +#endif } int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) { switch (arg->getStatusType()) { case STATUS_TYPE_POWER: { - meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg; if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { power_state = charging; if (powerStatus->getBatteryChargePercent() >= 100) { @@ -37,7 +38,6 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) break; } case STATUS_TYPE_BLUETOOTH: { - meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)arg; switch (bluetoothStatus->getConnectionState()) { case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: { ble_state = unpaired; @@ -62,19 +62,22 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) } return 0; }; - +#if !MESHTASTIC_EXCLUDE_INPUTBROKER int StatusLEDModule::handleInputEvent(const InputEvent *event) { lastUserbuttonTime = millis(); return 0; } +#endif int32_t StatusLEDModule::runOnce() { my_interval = 1000; if (power_state == charging) { +#ifndef POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING CHARGE_LED_state = !CHARGE_LED_state; +#endif } else if (power_state == charged) { CHARGE_LED_state = LED_STATE_ON; } else if (power_state == critical) { @@ -88,13 +91,19 @@ int32_t StatusLEDModule::runOnce() my_interval = 250; if (POWER_LED_starttime + 2000 < millis()) { doing_fast_blink = false; + CHARGE_LED_state = LED_STATE_OFF; } - } else { - CHARGE_LED_state = LED_STATE_OFF; } + } - } else { - CHARGE_LED_state = LED_STATE_OFF; + if (power_state != charging && power_state != charged && !doing_fast_blink) { + if (CHARGE_LED_state == LED_STATE_ON) { + CHARGE_LED_state = LED_STATE_OFF; + my_interval = 999; + } else { + CHARGE_LED_state = LED_STATE_ON; + my_interval = 1; + } } if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { @@ -112,6 +121,11 @@ int32_t StatusLEDModule::runOnce() PAIRING_LED_state = LED_STATE_ON; } + // Override if disabled in config + if (config.device.led_heartbeat_disabled) { + CHARGE_LED_state = LED_STATE_OFF; + } +#ifdef Battery_LED_1 bool chargeIndicatorLED1 = LED_STATE_OFF; bool chargeIndicatorLED2 = LED_STATE_OFF; bool chargeIndicatorLED3 = LED_STATE_OFF; @@ -126,15 +140,38 @@ int32_t StatusLEDModule::runOnce() if (powerStatus && powerStatus->getBatteryChargePercent() >= 75) chargeIndicatorLED4 = LED_STATE_ON; } - -#ifdef LED_CHARGE - digitalWrite(LED_CHARGE, CHARGE_LED_state); #endif - // digitalWrite(green_LED_PIN, LED_STATE_OFF); + +#if defined(HAS_PMU) + if (pmu_found && PMU) { + // blink the axp led + PMU->setChargingLedMode(CHARGE_LED_state ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); + } +#endif + +#ifdef PCA_LED_POWER + io.digitalWrite(PCA_LED_POWER, CHARGE_LED_state); +#endif +#ifdef PCA_LED_ENABLE + io.digitalWrite(PCA_LED_ENABLE, CHARGE_LED_state); +#endif +#ifdef LED_POWER + digitalWrite(LED_POWER, CHARGE_LED_state); +#endif #ifdef LED_PAIRING digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif +#ifdef RGB_LED_POWER + if (!config.device.led_heartbeat_disabled) { + if (CHARGE_LED_state == LED_STATE_ON) { + ambientLightingThread->setLighting(10, 255, 0, 0); + } else { + ambientLightingThread->setLighting(0, 0, 0, 0); + } + } +#endif + #ifdef Battery_LED_1 digitalWrite(Battery_LED_1, chargeIndicatorLED1); #endif @@ -150,3 +187,40 @@ int32_t StatusLEDModule::runOnce() return (my_interval); } + +void StatusLEDModule::setPowerLED(bool LEDon) +{ + +#if defined(HAS_PMU) + if (pmu_found && PMU) { + // blink the axp led + PMU->setChargingLedMode(LEDon ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); + } +#endif + uint8_t ledState = LEDon ? LED_STATE_ON : LED_STATE_OFF; +#ifdef PCA_LED_POWER + io.digitalWrite(PCA_LED_POWER, ledState); +#endif +#ifdef PCA_LED_ENABLE + io.digitalWrite(PCA_LED_ENABLE, ledState); +#endif +#ifdef LED_POWER + digitalWrite(LED_POWER, ledState); +#endif +#ifdef LED_PAIRING + digitalWrite(LED_PAIRING, ledState); +#endif + +#ifdef Battery_LED_1 + digitalWrite(Battery_LED_1, ledState); +#endif +#ifdef Battery_LED_2 + digitalWrite(Battery_LED_2, ledState); +#endif +#ifdef Battery_LED_3 + digitalWrite(Battery_LED_3, ledState); +#endif +#ifdef Battery_LED_4 + digitalWrite(Battery_LED_4, ledState); +#endif +} diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index 98020cb32..972e26737 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -5,10 +5,14 @@ #include "PowerStatus.h" #include "concurrency/OSThread.h" #include "configuration.h" -#include "input/InputBroker.h" +#include "main.h" #include #include +#if !MESHTASTIC_EXCLUDE_INPUTBROKER +#include "input/InputBroker.h" +#endif + class StatusLEDModule : private concurrency::OSThread { bool slowTrack = false; @@ -17,8 +21,11 @@ class StatusLEDModule : private concurrency::OSThread StatusLEDModule(); int handleStatusUpdate(const meshtastic::Status *); - +#if !MESHTASTIC_EXCLUDE_INPUTBROKER int handleInputEvent(const InputEvent *arg); +#endif + + void setPowerLED(bool); protected: unsigned int my_interval = 1000; // interval in millisconds @@ -28,8 +35,10 @@ class StatusLEDModule : private concurrency::OSThread CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); CallbackObserver powerStatusObserver = CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); +#if !MESHTASTIC_EXCLUDE_INPUTBROKER CallbackObserver inputObserver = CallbackObserver(this, &StatusLEDModule::handleInputEvent); +#endif private: bool CHARGE_LED_state = LED_STATE_OFF; @@ -50,3 +59,7 @@ class StatusLEDModule : private concurrency::OSThread }; extern StatusLEDModule *statusLEDModule; +#ifdef RGB_LED_POWER +#include "AmbientLightingThread.h" +extern AmbientLightingThread *ambientLightingThread; +#endif diff --git a/src/modules/StatusMessageModule.cpp b/src/modules/StatusMessageModule.cpp new file mode 100644 index 000000000..0707a4f7d --- /dev/null +++ b/src/modules/StatusMessageModule.cpp @@ -0,0 +1,54 @@ +#if !MESHTASTIC_EXCLUDE_STATUS + +#include "StatusMessageModule.h" +#include "MeshService.h" +#include "ProtobufModule.h" + +StatusMessageModule *statusMessageModule; + +int32_t StatusMessageModule::runOnce() +{ + if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') { + // create and send message with the status message set + meshtastic_StatusMessage ourStatus = meshtastic_StatusMessage_init_zero; + strncpy(ourStatus.status, moduleConfig.statusmessage.node_status, sizeof(ourStatus.status)); + ourStatus.status[sizeof(ourStatus.status) - 1] = '\0'; // ensure null termination + meshtastic_MeshPacket *p = allocDataPacket(); + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), + meshtastic_StatusMessage_fields, &ourStatus); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->channel = 0; + service->sendToMesh(p); + } + + return 1000 * 12 * 60 * 60; +} + +ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + 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; +} + +#endif \ No newline at end of file diff --git a/src/modules/StatusMessageModule.h b/src/modules/StatusMessageModule.h new file mode 100644 index 000000000..5090066e6 --- /dev/null +++ b/src/modules/StatusMessageModule.h @@ -0,0 +1,48 @@ +#pragma once +#if !MESHTASTIC_EXCLUDE_STATUS +#include "SinglePortModule.h" +#include "configuration.h" +#include +#include + +class StatusMessageModule : public SinglePortModule, private concurrency::OSThread +{ + public: + /** Constructor + * name is for debugging output + */ + StatusMessageModule() + : SinglePortModule("statusMessage", meshtastic_PortNum_NODE_STATUS_APP), concurrency::OSThread("StatusMessage") + { + if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') { + this->setInterval(2 * 60 * 1000); + } else { + 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 &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 recentReceived; +}; + +extern StatusMessageModule *statusMessageModule; +#endif \ No newline at end of file diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 023a1c798..6df0e18f0 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -131,9 +131,7 @@ void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to) uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time) { uint32_t count = 0; - if (lastRequest.find(dest) == lastRequest.end()) { - lastRequest.emplace(dest, 0); - } + lastRequest.emplace(dest, 0); for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { // Client is only interested in packets not from itself and only in broadcast packets or packets towards it. @@ -590,7 +588,8 @@ StoreForwardModule::StoreForwardModule() if (moduleConfig.store_forward.enabled) { // Router - if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || moduleConfig.store_forward.is_server)) { + if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE || moduleConfig.store_forward.is_server)) { LOG_INFO("Init Store & Forward Module in Server mode"); if (memGet.getPsramSize() > 0) { if (memGet.getFreePsram() >= 1024 * 1024) { diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 01f5da2c6..ca853d051 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,3 +1,4 @@ +#include "DebugConfiguration.h" #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR @@ -10,7 +11,7 @@ #include "PowerFSM.h" #include "RTC.h" #include "Router.h" -#include "Sensor/AddI2CSensorTemplate.h" +#include "TransmitHistory.h" #include "UnitConversions.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" @@ -19,8 +20,21 @@ #include "sleep.h" #include +static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004; + // Sensors +#include "Sensor/AddI2CSensorTemplate.h" #include "Sensor/PMSA003ISensor.h" +#include "Sensor/SEN5XSensor.h" +#if __has_include() +#include "Sensor/SCD4XSensor.h" +#endif +#if __has_include() +#include "Sensor/SFA30Sensor.h" +#endif +#if __has_include() +#include "Sensor/SCD30Sensor.h" +#endif void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -42,6 +56,16 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // order by priority of metrics/values (low top, high bottom) addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); + addSensor(i2cScanner, ScanI2C::DeviceType::SEN5X); +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::SCD4X); +#endif +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::SFA30); +#endif +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::SCD30); +#endif } int32_t AirQualityTelemetryModule::runOnce() @@ -63,7 +87,8 @@ int32_t AirQualityTelemetryModule::runOnce() } if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup + // This is the first time the OSThread library has called this function, so + // do some setup firstTime = false; if (moduleConfig.telemetry.air_quality_enabled) { @@ -85,21 +110,41 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it - LOG_INFO("Waking up sensors"); + LOG_INFO("Waking up sensors..."); + uint32_t lastTelemetry = + transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY) : 0; for (TelemetrySensor *sensor : sensors) { - if (!sensor->isActive()) { - return sensor->wakeUp(); + if (!sensor->canSleep()) { + LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName); + } else if (((lastTelemetry == 0) || + !Throttle::isWithinTimespanMs(lastTelemetry - sensor->wakeUpTimeMs(), + Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + if (!sensor->isActive()) { + LOG_DEBUG("Waking up: %s", sensor->sensorName); + return sensor->wakeUp(); + } else { + int32_t pendingForReadyMs = sensor->pendingForReadyMs(); + LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs); + if (pendingForReadyMs) { + return pendingForReadyMs; + } + } } } - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + if (((lastTelemetry == 0) || + !Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet @@ -109,9 +154,18 @@ int32_t AirQualityTelemetryModule::runOnce() } // Send to sleep sensors that consume power - LOG_INFO("Sending sensors to sleep"); + LOG_DEBUG("Sending sensors to sleep"); for (TelemetrySensor *sensor : sensors) { - sensor->sleep(); + if (sensor->isActive() && sensor->canSleep()) { + if (sensor->wakeUpTimeMs() < + (int32_t)Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes)) { + LOG_DEBUG("Disabling %s until next period", sensor->sensorName); + sensor->sleep(); + } else { + LOG_DEBUG("Sensor stays enabled due to warm up period"); + } + } } } return min(sendToPhoneIntervalMs, result); @@ -158,8 +212,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta const auto &m = telemetry.variant.air_quality_metrics; // Check if any telemetry field has valid data - bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || - m.has_pm25_environmental || m.has_pm100_environmental; + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_co2; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); @@ -186,6 +239,10 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); if (m.has_pm100_standard) entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3"); + if (m.has_co2) + entries.push_back("CO2: " + String(m.co2) + "ppm"); + if (m.has_form_formaldehyde) + entries.push_back("HCHO: " + String(m.form_formaldehyde) + "ppb"); // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -221,13 +278,19 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, - t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, - t->variant.air_quality_metrics.pm100_standard); + if (t->variant.air_quality_metrics.has_pm10_standard) + LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, " + "pm100_standard=%i", + sender, t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, + t->variant.air_quality_metrics.pm100_standard); - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, - t->variant.air_quality_metrics.pm100_environmental); + if (t->variant.air_quality_metrics.has_co2) + LOG_INFO("CO2=%i, CO2_T=%.2f, CO2_H=%.2f", t->variant.air_quality_metrics.co2, + t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity); + + if (t->variant.air_quality_metrics.has_form_formaldehyde) + LOG_INFO("HCHO=%.2f, HCHO_T=%.2f, HCHO_H=%.2f", t->variant.air_quality_metrics.form_formaldehyde, + t->variant.air_quality_metrics.form_temperature, t->variant.air_quality_metrics.form_humidity); #endif // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) @@ -241,17 +304,20 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - bool valid = true; + // Note: this is different to the case in EnvironmentTelemetryModule + // There, if any sensor fails to read - valid = false. + bool valid = false; bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - // TODO - Should we check for sensor state here? - // If a sensor is sleeping, we should know and check to wake it up + bool sensor_get = false; for (TelemetrySensor *sensor : sensors) { - LOG_INFO("Reading AQ sensors"); - valid = valid && sensor->getMetrics(m); + LOG_DEBUG("Reading %s", sensor->sensorName); + // Note - this function doesn't get properly called if within a conditional + sensor_get = sensor->getMetrics(m); + valid = valid || sensor_get; hasSensor = true; } @@ -261,6 +327,10 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; @@ -291,12 +361,38 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); + if (getAirQualityTelemetry(&m)) { - LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ - pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", - m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, - m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, - m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); + + bool hasAnyPM = + m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard || + m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental || + m.variant.air_quality_metrics.has_pm25_environmental || m.variant.air_quality_metrics.has_pm100_environmental; + + if (hasAnyPM) { + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard, + m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard); + if (m.variant.air_quality_metrics.has_pm10_environmental) + LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", + m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental, + m.variant.air_quality_metrics.pm100_environmental); + } + + bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || + m.variant.air_quality_metrics.has_co2_humidity; + + if (hasAnyCO2) { + LOG_INFO("Send: co2=%i, co2_t=%.2f, co2_rh=%.2f", m.variant.air_quality_metrics.co2, + m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity); + } + + bool hasAnyHCHO = m.variant.air_quality_metrics.has_form_formaldehyde || + m.variant.air_quality_metrics.has_form_temperature || m.variant.air_quality_metrics.has_form_humidity; + + if (hasAnyHCHO) { + LOG_INFO("Send: hcho=%.2f, hcho_t=%.2f, hcho_rh=%.2f", m.variant.air_quality_metrics.form_formaldehyde, + m.variant.air_quality_metrics.form_temperature, m.variant.air_quality_metrics.form_humidity); + } meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; @@ -331,6 +427,20 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_DEBUG("Start next execution in 5s, then sleep"); setIntervalFromNow(FIVE_SECONDS_MS); } + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } } return true; } @@ -352,4 +462,4 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( return result; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 2b88b74ba..04936d8c1 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -4,6 +4,8 @@ #pragma once +#include "BaseTelemetryModule.h" + #ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE #define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0 #endif @@ -17,6 +19,7 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, + public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = @@ -64,8 +67,8 @@ class AirQualityTelemetryModule : private concurrency::OSThread, bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; + // uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute uint32_t lastSentToPhone = 0; }; -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/BaseTelemetryModule.h b/src/modules/Telemetry/BaseTelemetryModule.h new file mode 100644 index 000000000..d986f41a9 --- /dev/null +++ b/src/modules/Telemetry/BaseTelemetryModule.h @@ -0,0 +1,15 @@ +#pragma once + +#include "NodeDB.h" +#include "configuration.h" + +class BaseTelemetryModule +{ + protected: + bool isSensorOrRouterRole() const + { + return config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE; + } +}; diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 066b9361d..1c2d18c71 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -7,6 +7,7 @@ #include "RTC.h" #include "RadioLibInterface.h" #include "Router.h" +#include "TransmitHistory.h" #include "configuration.h" #include "main.h" #include "memGet.h" @@ -15,21 +16,23 @@ #include #define MAGIC_USB_BATTERY_LEVEL 101 +static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001; int32_t DeviceTelemetryModule::runOnce() { refreshUptime(); - bool isImpoliteRole = - IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER); - if (((lastSentToMesh == 0) || - ((uptimeLastMs - lastSentToMesh) >= - Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0; + bool isImpoliteRole = isSensorOrRouterRole(); + if (((lastTelemetry == 0) || + ((uptimeLastMs - lastTelemetry) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, + default_telemetry_broadcast_interval_secs, + numOnlineNodes))) && airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN && moduleConfig.telemetry.device_telemetry_enabled) { sendTelemetry(); - lastSentToMesh = uptimeLastMs; + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_DEVICE_TELEMETRY); } else if (service->isToPhoneQueueEmpty()) { // Just send to phone when it's not our time to send to mesh yet // Only send while queue is empty (phone assumed connected) @@ -60,6 +63,10 @@ bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket & meshtastic_MeshPacket *DeviceTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; diff --git a/src/modules/Telemetry/DeviceTelemetry.h b/src/modules/Telemetry/DeviceTelemetry.h index a1d55a596..f37afee70 100644 --- a/src/modules/Telemetry/DeviceTelemetry.h +++ b/src/modules/Telemetry/DeviceTelemetry.h @@ -1,11 +1,14 @@ #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include -class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule +class DeviceTelemetryModule : private concurrency::OSThread, + public BaseTelemetryModule, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &DeviceTelemetryModule::handleStatusUpdate); @@ -48,7 +51,6 @@ class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModu uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes uint32_t lastSentStatsToPhone = 0; - uint32_t lastSentToMesh = 0; void refreshUptime() { diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 86a8606c2..684d408a1 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -10,6 +10,7 @@ #include "PowerFSM.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "UnitConversions.h" #include "buzz.h" #include "graphics/SharedUIDisplay.h" @@ -65,18 +66,10 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/MCP9808Sensor.h" #endif -#if __has_include() -#include "Sensor/SHT31Sensor.h" -#endif - #if __has_include() #include "Sensor/LPS22HBSensor.h" #endif -#if __has_include() -#include "Sensor/SHTC3Sensor.h" -#endif - #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 #include "Sensor/RAK12035Sensor.h" #endif @@ -93,8 +86,8 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/OPT3001Sensor.h" #endif -#if __has_include() -#include "Sensor/SHT4XSensor.h" +#if __has_include() +#include "Sensor/SHTXXSensor.h" #endif #if __has_include() @@ -145,6 +138,8 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "graphics/ScreenFonts.h" #include +static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002; + void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { @@ -152,6 +147,15 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) } LOG_INFO("Environment Telemetry adding I2C devices..."); + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ + + // moduleConfig.telemetry.environment_measurement_enabled = 1; + // moduleConfig.telemetry.environment_screen_enabled = 1; + // moduleConfig.telemetry.environment_update_interval = 15; + // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR @@ -199,15 +203,9 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::MCP9808); #endif -#if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHT31); -#endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::LPS22HB); #endif -#if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHTC3); -#endif #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 addSensor(i2cScanner, ScanI2C::DeviceType::RAK12035); #endif @@ -220,13 +218,9 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::OPT3001); #endif -#if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHT4X); -#endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::MLX90632); #endif - #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BMP_3XX); #endif @@ -242,7 +236,10 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BH1750); #endif - +#if __has_include() + // TODO Can we scan for multiple sensors connected on the same bus? + addSensor(i2cScanner, ScanI2C::DeviceType::SHTXX); +#endif #endif } @@ -257,14 +254,6 @@ int32_t EnvironmentTelemetryModule::runOnce() } uint32_t result = UINT32_MAX; - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ - - // moduleConfig.telemetry.environment_measurement_enabled = 1; - // moduleConfig.telemetry.environment_screen_enabled = 1; - // moduleConfig.telemetry.environment_update_interval = 15; if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) { @@ -297,7 +286,8 @@ int32_t EnvironmentTelemetryModule::runOnce() // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the // sensormap here. #ifdef HAS_RAKPROT - result = rak9154Sensor.runOnce(); + if (rak9154Sensor.hasSensor()) + result = rak9154Sensor.runOnce(); #endif #endif } @@ -317,14 +307,17 @@ int32_t EnvironmentTelemetryModule::runOnce() } } - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.environment_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + uint32_t lastTelemetry = + transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0; + if (((lastTelemetry == 0) || + !Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.environment_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet @@ -529,38 +522,49 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m) { - bool valid = true; + bool valid = false; bool hasSensor = false; + // getMetrics() doesn't always get evaluated because of + // short-circuit evaluation rules in c++ + bool get_metrics; m->time = getTime(); m->which_variant = meshtastic_Telemetry_environment_metrics_tag; m->variant.environment_metrics = meshtastic_EnvironmentMetrics_init_zero; for (TelemetrySensor *sensor : sensors) { - valid = valid && sensor->getMetrics(m); + get_metrics = sensor->getMetrics(m); // avoid short-circuit evaluation rules + valid = valid || get_metrics; hasSensor = true; } #ifndef T1000X_SENSOR_EN if (ina219Sensor.hasSensor()) { - valid = valid && ina219Sensor.getMetrics(m); + get_metrics = ina219Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } if (ina260Sensor.hasSensor()) { - valid = valid && ina260Sensor.getMetrics(m); + get_metrics = ina260Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } if (ina3221Sensor.hasSensor()) { - valid = valid && ina3221Sensor.getMetrics(m); + get_metrics = ina3221Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } if (max17048Sensor.hasSensor()) { - valid = valid && max17048Sensor.getMetrics(m); + get_metrics = max17048Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } #endif #ifdef HAS_RAKPROT - valid = valid && rak9154Sensor.getMetrics(m); - hasSensor = true; + if (rak9154Sensor.hasSensor()) { + get_metrics = rak9154Sensor.getMetrics(m); + valid = valid || get_metrics; + hasSensor = true; + } #endif return valid && hasSensor; } @@ -568,6 +572,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index 049ed6b77..0b7e0f4cb 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -4,6 +4,8 @@ #pragma once +#include "BaseTelemetryModule.h" + #ifndef ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE #define ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE 0 #endif @@ -17,6 +19,7 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, + public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = @@ -65,7 +68,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, bool firstTime = 1; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; }; diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index 572f0281a..ae6b366bd 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -10,6 +10,7 @@ #include "PowerFSM.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "UnitConversions.h" #include "main.h" #include "power.h" @@ -33,6 +34,8 @@ MLX90614Sensor mlx90614Sensor; #endif #include +static constexpr uint16_t TX_HISTORY_KEY_HEALTH_TELEMETRY = 0x8003; + int32_t HealthTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { @@ -69,14 +72,16 @@ int32_t HealthTelemetryModule::runOnce() return disable(); } - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.health_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_HEALTH_TELEMETRY) : 0; + if (((lastTelemetry == 0) || + !Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.health_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_HEALTH_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet @@ -168,18 +173,21 @@ bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket & bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m) { - bool valid = true; + bool valid = false; bool hasSensor = false; + bool get_metrics; m->time = getTime(); m->which_variant = meshtastic_Telemetry_health_metrics_tag; m->variant.health_metrics = meshtastic_HealthMetrics_init_zero; if (max30102Sensor.hasSensor()) { - valid = valid && max30102Sensor.getMetrics(m); + get_metrics = max30102Sensor.getMetrics(m); + valid = valid || get_metrics; // avoid short-circuit evaluation rules hasSensor = true; } if (mlx90614Sensor.hasSensor()) { - valid = valid && mlx90614Sensor.getMetrics(m); + get_metrics = mlx90614Sensor.getMetrics(m); + valid = valid || get_metrics; hasSensor = true; } @@ -189,6 +197,10 @@ bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m) meshtastic_MeshPacket *HealthTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; diff --git a/src/modules/Telemetry/HealthTelemetry.h b/src/modules/Telemetry/HealthTelemetry.h index 01e4c2372..4d0722201 100644 --- a/src/modules/Telemetry/HealthTelemetry.h +++ b/src/modules/Telemetry/HealthTelemetry.h @@ -4,12 +4,15 @@ #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include -class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule +class HealthTelemetryModule : private concurrency::OSThread, + public BaseTelemetryModule, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate); @@ -52,7 +55,6 @@ class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModu bool firstTime = 1; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; uint32_t sensor_read_error_count = 0; }; diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 9047c7cd4..d02aed9c2 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -10,6 +10,7 @@ #include "PowerTelemetry.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "graphics/SharedUIDisplay.h" #include "main.h" #include "power.h" @@ -22,6 +23,8 @@ #include "graphics/ScreenFonts.h" #include +static constexpr uint16_t TX_HISTORY_KEY_POWER_TELEMETRY = 0x8005; + namespace graphics { extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, @@ -88,10 +91,12 @@ int32_t PowerTelemetryModule::runOnce() if (!moduleConfig.telemetry.power_measurement_enabled) return disable(); - if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) && + uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_POWER_TELEMETRY) : 0; + if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry, sendToMeshIntervalMs)) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_POWER_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet @@ -217,6 +222,10 @@ bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m) meshtastic_MeshPacket *PowerTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; diff --git a/src/modules/Telemetry/PowerTelemetry.h b/src/modules/Telemetry/PowerTelemetry.h index b9ec6edc1..134b40b6b 100644 --- a/src/modules/Telemetry/PowerTelemetry.h +++ b/src/modules/Telemetry/PowerTelemetry.h @@ -5,12 +5,15 @@ #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include -class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule +class PowerTelemetryModule : private concurrency::OSThread, + public BaseTelemetryModule, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &PowerTelemetryModule::handleStatusUpdate); @@ -51,7 +54,6 @@ class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModul bool firstTime = 1; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; uint32_t sensor_read_error_count = 0; }; diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h index 37d909d71..b7029986b 100644 --- a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -8,7 +8,7 @@ static std::forward_list sensors; -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +template void addSensor(const ScanI2C *i2cScanner, ScanI2C::DeviceType type) { ScanI2C::FoundDevice dev = i2cScanner->find(type); if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 3a1eb9532..c202028e1 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -8,6 +8,10 @@ #include "SPILock.h" #include "TelemetrySensor.h" +#if __has_include() +#include +#endif + BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} #if __has_include() @@ -96,8 +100,28 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.environment_metrics.temperature = bme680->readTemperature(); measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity(); measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F; - measurement->variant.environment_metrics.gas_resistance = bme680->readGas() / 1000.0; + float gasRaw = bme680->readGas(); + measurement->variant.environment_metrics.gas_resistance = gasRaw / 1000.0; + + // IAQ approximation: humidity-compensated logarithmic mapping of gas resistance + // Gas sensor resistance drops with humidity; compensate to a 40% RH reference baseline + // Map compensated gas resistance (Ohms) to IAQ 0-500 using log-linear interpolation + // Clean air reference ~400 kOhm, polluted reference ~5 kOhm + if (gasRaw > 0.0f && !isfinite(gasRaw)) { + + static constexpr float LOG_UPPER = 12.899219f; // log(400k) + static constexpr float LOG_RANGE_INV = 1.0f / (12.899219f - 8.517193f); // 1 / (log(400k) - log(5k)) + measurement->variant.environment_metrics.has_iaq = true; + measurement->variant.environment_metrics.iaq = (uint16_t)(fminf( + fmaxf(((LOG_UPPER - + logf(fmaxf(gasRaw * expf(0.035f * (measurement->variant.environment_metrics.relative_humidity - 40.0f)), + 1.0f))) * + LOG_RANGE_INV) * + 500.0f, + 0.0f), + 500.0f)); + } #endif return true; } diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index eaeceb848..1134f04d9 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -28,7 +28,7 @@ class BME680Sensor : public TelemetrySensor #else using BME680Ptr = std::unique_ptr; - static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique(bus); } + static BME680Ptr makeBME680(TwoWire *bus) { return BME680Ptr(new Adafruit_BME680(bus)); } BME680Ptr bme680; #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 2225a4d87..e34b70a1f 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -21,26 +21,29 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) _bus = bus; _address = dev->address.address; -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); - if (!currentClock) { - LOG_WARN("PMSA003I can't be used at this clock speed"); - return false; - } -#endif +#ifdef PMSA003I_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* PMSA003I_I2C_CLOCK_SPEED */ _bus->beginTransmission(_address); if (_bus->endTransmission() != 0) { - LOG_WARN("PMSA003I not found on I2C at 0x12"); + LOG_WARN("%s not found on I2C at 0x12", sensorName); return false; } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - reClockI2C(currentClock, _bus); + reClockI2C(currentClock, _bus, false); #endif status = 1; - LOG_INFO("PMSA003I Enabled"); + LOG_INFO("%s Enabled", sensorName); initI2CSensor(); return true; @@ -49,34 +52,41 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { if (!isActive()) { - LOG_WARN("PMSA003I is not active"); + LOG_WARN("Can't get metrics. %s is not active", sensorName); return false; } -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); -#endif +#ifdef PMSA003I_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* PMSA003I_I2C_CLOCK_SPEED */ - _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); + _bus->requestFrom(_address, (uint8_t)PMSA003I_FRAME_LENGTH); if (_bus->available() < PMSA003I_FRAME_LENGTH) { - LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available()); + LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available()); return false; } -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - reClockI2C(currentClock, _bus); -#endif - for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { buffer[i] = _bus->read(); } +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + if (buffer[0] != 0x42 || buffer[1] != 0x4D) { - LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]); + LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]); return false; } - auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; + auto read16 = [](const uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; computedChecksum = 0; @@ -86,7 +96,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); if (computedChecksum != receivedChecksum) { - LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum); + LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum); return false; } @@ -128,6 +138,10 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_particles_100um = true; measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); + LOG_DEBUG("Got %s readings: pM1p0_standard=%u, pM2p5_standard=%u, pM10p0_standard=%u", sensorName, + measurement->variant.air_quality_metrics.pm10_standard, measurement->variant.air_quality_metrics.pm25_standard, + measurement->variant.air_quality_metrics.pm100_standard); + return true; } @@ -136,20 +150,58 @@ bool PMSA003ISensor::isActive() return state == State::ACTIVE; } +int32_t PMSA003ISensor::wakeUpTimeMs() +{ +#ifdef PMSA003I_ENABLE_PIN + return PMSA003I_WARMUP_MS; +#endif + return 0; +} + +int32_t PMSA003ISensor::pendingForReadyMs() +{ +#ifdef PMSA003I_ENABLE_PIN + + uint32_t now; + now = getTime(); + uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted); + + if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return PMSA003I_WARMUP_MS - sincePmMeasureStarted; + } + return 0; + +#endif + return 0; +} + +bool PMSA003ISensor::canSleep() +{ +#ifdef PMSA003I_ENABLE_PIN + return true; +#endif + return false; +} + void PMSA003ISensor::sleep() { #ifdef PMSA003I_ENABLE_PIN digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; + pmMeasureStarted = 0; #endif } uint32_t PMSA003ISensor::wakeUp() { #ifdef PMSA003I_ENABLE_PIN - LOG_INFO("Waking up PMSA003I"); + LOG_INFO("Waking up %s", sensorName); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; + pmMeasureStarted = getTime(); + return PMSA003I_WARMUP_MS; #endif // No need to wait for warmup if already active diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 09b43d620..3fe96888d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -3,6 +3,7 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" #include "TelemetrySensor.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 @@ -19,6 +20,9 @@ class PMSA003ISensor : public TelemetrySensor virtual bool isActive() override; virtual void sleep() override; virtual uint32_t wakeUp() override; + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; private: enum class State { IDLE, ACTIVE }; @@ -26,6 +30,7 @@ class PMSA003ISensor : public TelemetrySensor uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; + uint32_t pmMeasureStarted = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; TwoWire *_bus{}; diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp index 626cc0e87..4f3150b25 100644 --- a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp @@ -26,7 +26,7 @@ bool RAK12035Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) sensor.get_sensor_version(&data); if (data != 0) { LOG_INFO("Init sensor: %s", sensorName); - LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName); + LOG_INFO("RAK12035Sensor Init Succeed \nSensor Firmware version: %i, Sensor Name: %s", data, sensorName); status = true; sensor.sensor_sleep(); RESTORE_3V3_POWER(); @@ -49,33 +49,39 @@ void RAK12035Sensor::setup() // TODO:: Check for and run calibration check for up to 2 additional sensors if present. uint16_t zero_val = 0; uint16_t hundred_val = 0; - uint16_t default_zero_val = 550; - uint16_t default_hundred_val = 420; + const uint16_t default_zero_val = 510; + const uint16_t default_hundred_val = 390; + sensor.sensor_on(); + sensor.begin(); delay(200); sensor.get_dry_cal(&zero_val); + delay(200); sensor.get_wet_cal(&hundred_val); delay(200); - if (zero_val == 0 || zero_val <= hundred_val) { - LOG_INFO("Dry calibration value is %d", zero_val); - LOG_INFO("Wet calibration value is %d", hundred_val); - LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " - "https://github.com/RAKWireless/RAK12035_SoilMoisture."); - LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val); + + bool calibrationReset = false; + + if (zero_val == 0) { + LOG_INFO("Dry calibration not set, using default: %d", default_zero_val); sensor.set_dry_cal(default_zero_val); - sensor.get_dry_cal(&zero_val); - LOG_INFO("Dry calibration reset complete. New value is %d", zero_val); + delay(200); + zero_val = default_zero_val; + calibrationReset = true; } if (hundred_val == 0 || hundred_val >= zero_val) { - LOG_INFO("Dry calibration value is %d", zero_val); - LOG_INFO("Wet calibration value is %d", hundred_val); - LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " - "https://github.com/RAKWireless/RAK12035_SoilMoisture."); - LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val); + LOG_INFO("Wet calibration not set, using default: %d", default_hundred_val); sensor.set_wet_cal(default_hundred_val); - sensor.get_wet_cal(&hundred_val); - LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val); + delay(200); + hundred_val = default_hundred_val; + calibrationReset = true; } + if (calibrationReset) { + LOG_INFO("Default calibration values applied. Consider running the calibration sketch for better accuracy: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture"); + } + + LOG_INFO("Dry calibration value: %d, Wet calibration value: %d", zero_val, hundred_val); sensor.sensor_sleep(); RESTORE_3V3_POWER(); delay(200); diff --git a/src/modules/Telemetry/Sensor/RAK9154Sensor.h b/src/modules/Telemetry/Sensor/RAK9154Sensor.h index c96139f9c..34d0fba73 100644 --- a/src/modules/Telemetry/Sensor/RAK9154Sensor.h +++ b/src/modules/Telemetry/Sensor/RAK9154Sensor.h @@ -18,6 +18,7 @@ class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor public: RAK9154Sensor(); + bool hasSensor() { return true; } // Not an I2C sensor; always available when HAS_RAKPROT is defined virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual uint16_t getBusVoltageMv() override; diff --git a/src/modules/Telemetry/Sensor/SCD30Sensor.cpp b/src/modules/Telemetry/Sensor/SCD30Sensor.cpp new file mode 100644 index 000000000..0478b6651 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD30Sensor.cpp @@ -0,0 +1,511 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SCD30Sensor.h" + +#define SCD30_NO_ERROR 0 + +SCD30Sensor::SCD30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD30, "SCD30") {} + +bool SCD30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + scd30.begin(*_bus, _address); + + if (!startMeasurement()) { + LOG_ERROR("%s: Failed to start periodic measurement", sensorName); +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + if (!getASC(ascActive)) { + LOG_WARN("%s: Could not determine ASC state", sensorName); + } + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + if (state == SCD30_MEASUREMENT) { + status = 1; + } else { + status = 0; + } + + initI2CSensor(); + + return true; +} + +bool SCD30Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + float co2, temperature, humidity; + +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + if (scd30.readMeasurementData(co2, temperature, humidity) != SCD30_NO_ERROR) { + LOG_ERROR("SCD30: Failed to read measurement data."); +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + if (co2 == 0) { + LOG_ERROR("SCD30: Invalid CO₂ reading."); + return false; + } + + measurement->variant.air_quality_metrics.has_co2 = true; + measurement->variant.air_quality_metrics.has_co2_temperature = true; + measurement->variant.air_quality_metrics.has_co2_humidity = true; + measurement->variant.air_quality_metrics.co2 = (uint32_t)co2; + measurement->variant.air_quality_metrics.co2_temperature = temperature; + measurement->variant.air_quality_metrics.co2_humidity = humidity; + + LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum=%.2f", sensorName, (uint32_t)co2, temperature, humidity); + + return true; +} + +bool SCD30Sensor::setMeasurementInterval(uint16_t measInterval) +{ + uint16_t error; + + LOG_INFO("%s: setting measurement interval at %us", sensorName, measInterval); + error = scd30.setMeasurementInterval(measInterval); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to set measurement interval. Error code: %u", sensorName, error); + return false; + } + + // Restart measuring so we don't need to wait the current interval to finish + // (useful when you come from very long intervals) + scd30.stopPeriodicMeasurement(); + scd30.startPeriodicMeasurement(0); + + getMeasurementInterval(measurementInterval); + return true; +} + +bool SCD30Sensor::getMeasurementInterval(uint16_t &measInterval) +{ + uint16_t error; + + LOG_INFO("%s: getting measurement interval", sensorName); + error = scd30.getMeasurementInterval(measInterval); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to get measurement interval. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: measurement interval is %us", sensorName, measInterval); + + return true; +} + +/** + * @brief Start measurement mode + * @note This function should not change the clock + */ +bool SCD30Sensor::startMeasurement() +{ + uint16_t error; + + if (state == SCD30_MEASUREMENT) { + LOG_DEBUG("%s: Already in measurement mode", sensorName); + return true; + } + + error = scd30.startPeriodicMeasurement(0); + + if (error == SCD30_NO_ERROR) { + LOG_INFO("%s: Started measurement mode", sensorName); + + state = SCD30_MEASUREMENT; + return true; + } else { + LOG_ERROR("%s: Couldn't start measurement mode", sensorName); + return false; + } +} + +/** + * @brief Stop measurement mode + * @note This function should not change the clock + */ +bool SCD30Sensor::stopMeasurement() +{ + uint16_t error; + + error = scd30.stopPeriodicMeasurement(); + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to stop measurement", sensorName); + return false; + } + + state = SCD30_IDLE; + return true; +} + +bool SCD30Sensor::performFRC(uint16_t targetCO2) +{ + uint16_t error; + + LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); + + LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2); + error = scd30.forceRecalibration((uint16_t)targetCO2); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); + return false; + } + + LOG_INFO("%s: FRC Correction successful.", sensorName); + + return true; +} + +bool SCD30Sensor::setASC(bool ascEnabled) +{ + uint16_t error; + + LOG_INFO("%s: %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling"); + + error = scd30.activateAutoCalibration((uint16_t)ascEnabled); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + if (!getASC(ascActive)) { + LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); + return false; + } + + return true; +} + +bool SCD30Sensor::getASC(uint16_t &_ascActive) +{ + uint16_t error; + // LOG_INFO("%s: Getting ASC", sensorName); + + error = scd30.getAutoCalibrationStatus(_ascActive); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + LOG_INFO("%s: ASC is %s", sensorName, _ascActive ? "enabled" : "disabled"); + + return true; +} + +/** + * @brief Set the temperature reference. Unit ℃. + * + * The on-board RH/T sensor is influenced by thermal self-heating of SCD30 + * and other electrical components. Design-in alters the thermal properties + * of SCD30 such that temperature and humidity offsets may occur when + * operating the sensor in end-customer devices. Compensation of those + * effects is achievable by writing the temperature offset found in + * continuous operation of the device into the sensor. Temperature offset + * value is saved in non-volatile memory. The last set value will be used + * for temperature offset compensation after repowering. + * + * @param[in] tempReference + * @note this function is certainly confusing and it's not recommended + */ +bool SCD30Sensor::setTemperature(float tempReference) +{ + uint16_t error; + uint16_t updatedTempOffset; + float tempOffset; + uint16_t _tempOffset; + float co2; + float temperature; + float humidity; + + if (tempReference == 100) { + // Requesting the value of 100 will restore the temperature offset + LOG_INFO("%s: Setting reference temperature at 0degC", sensorName); + _tempOffset = 0; + } else { + + LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference); + + error = scd30.readMeasurementData(co2, temperature, humidity); + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature); + + tempOffset = (temperature - tempReference); + if (tempOffset < 0) { + LOG_ERROR("%s temperature offset is only positive", sensorName); + return false; + } + + tempOffset *= 100; + _tempOffset = static_cast(tempOffset); + } + + LOG_INFO("%s: Setting temperature offset: %u (*100)", sensorName, _tempOffset); + + error = scd30.setTemperatureOffset(_tempOffset); + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); + return false; + } + + scd30.getTemperatureOffset(updatedTempOffset); + LOG_INFO("%s: Updated sensor temperature offset: %u (*100)", sensorName, updatedTempOffset); + + return true; +} + +bool SCD30Sensor::setAltitude(uint16_t altitude) +{ + uint16_t error; + + LOG_INFO("%s: setting altitude at %um", sensorName, altitude); + + error = scd30.setAltitudeCompensation(altitude); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); + return false; + } + + uint16_t newAltitude; + getAltitude(newAltitude); + + return true; +} + +bool SCD30Sensor::getAltitude(uint16_t &altitude) +{ + uint16_t error; + // LOG_INFO("%s: Getting altitude", sensorName); + + error = scd30.getAltitudeCompensation(altitude); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); + return false; + } + LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude); + + return true; +} + +bool SCD30Sensor::softReset() +{ + uint16_t error; + + LOG_INFO("%s: Requesting soft reset", sensorName); + + error = scd30.softReset(); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to do soft reset. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: soft reset successful", sensorName); + + return true; +} + +/** + * @brief Check if sensor is in measurement mode + */ +bool SCD30Sensor::isActive() +{ + return state == SCD30_MEASUREMENT; +} + +/** + * @brief Start measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +uint32_t SCD30Sensor::wakeUp() +{ + +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return 0; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + startMeasurement(); + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return 0; +} + +/** + * @brief Stop measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +void SCD30Sensor::sleep() +{ +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + stopMeasurement(); + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif +} + +bool SCD30Sensor::canSleep() +{ + return false; +} + +int32_t SCD30Sensor::wakeUpTimeMs() +{ + return 0; +} + +int32_t SCD30Sensor::pendingForReadyMs() +{ + return 0; +} + +AdminMessageHandleResult SCD30Sensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return AdminMessageHandleResult::NOT_HANDLED; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + // Check for ASC-FRC request first + if (!request->sensor_config.has_scd30_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + if (request->sensor_config.scd30_config.has_soft_reset) { + LOG_DEBUG("%s: Requested soft reset", sensorName); + this->softReset(); + } else { + + if (request->sensor_config.scd30_config.has_set_asc) { + this->setASC(request->sensor_config.scd30_config.set_asc); + if (request->sensor_config.scd30_config.set_asc == false) { + LOG_DEBUG("%s: Request for FRC", sensorName); + if (request->sensor_config.scd30_config.has_set_target_co2_conc) { + this->performFRC(request->sensor_config.scd30_config.set_target_co2_conc); + } else { + // FRC requested but no target CO2 provided + LOG_ERROR("%s: target CO2 not provided", sensorName); + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + } + + // Check for temperature offset + // NOTE: this requires to have a sensor working on stable environment + // And to make it between readings + if (request->sensor_config.scd30_config.has_set_temperature) { + this->setTemperature(request->sensor_config.scd30_config.set_temperature); + } + + // Check for altitude + if (request->sensor_config.scd30_config.has_set_altitude) { + this->setAltitude(request->sensor_config.scd30_config.set_altitude); + } + + // Check for set measuremen interval + if (request->sensor_config.scd30_config.has_set_measurement_interval) { + this->setMeasurementInterval(request->sensor_config.scd30_config.set_measurement_interval); + } + } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return result; +} + +#endif diff --git a/src/modules/Telemetry/Sensor/SCD30Sensor.h b/src/modules/Telemetry/Sensor/SCD30Sensor.h new file mode 100644 index 000000000..6e03e2dda --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD30Sensor.h @@ -0,0 +1,53 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +#define SCD30_I2C_CLOCK_SPEED 100000 + +class SCD30Sensor : public TelemetrySensor +{ + private: + SensirionI2cScd30 scd30; + TwoWire *_bus{}; + uint8_t _address{}; + + bool performFRC(uint16_t targetCO2); + bool setASC(bool ascEnabled); + bool getASC(uint16_t &ascEnabled); + bool setTemperature(float tempReference); + bool getAltitude(uint16_t &altitude); + bool setAltitude(uint16_t altitude); + bool softReset(); // + bool setMeasurementInterval(uint16_t measInterval); + bool getMeasurementInterval(uint16_t &measInterval); + bool startMeasurement(); + bool stopMeasurement(); + + // Parameters + uint16_t ascActive = 1; + uint16_t measurementInterval = 2; + + public: + SCD30Sensor(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + enum SCD30State { SCD30_OFF, SCD30_IDLE, SCD30_MEASUREMENT }; + SCD30State state = SCD30_OFF; + + virtual bool isActive() override; + + virtual void sleep() override; // Stops measurement (measurement -> idle) + virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement) + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp new file mode 100644 index 000000000..c6ab7bb04 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -0,0 +1,917 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SCD4XSensor.h" + +#define SCD4X_NO_ERROR 0 + +SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} + +bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + scd4x.begin(*_bus, _address); + + // From SCD4X library + delay(30); + + // Stop periodic measurement + if (!stopMeasurement()) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + // Get sensor variant + scd4x.getSensorVariant(sensorVariant); + + if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41) { + LOG_INFO("%s: Found SCD41", sensorName); + if (!powerUp()) { + LOG_ERROR("%s: Error trying to execute powerUp()", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + } + + if (!getASC(ascActive)) { + LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + // Start measurement in selected power mode (low power by default) + if (!startMeasurement()) { + LOG_ERROR("%s: Couldn't start measurement", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + if (state == SCD4X_MEASUREMENT) { + status = 1; + } else { + status = 0; + } + + initI2CSensor(); + + return true; +} + +bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + + if (state != SCD4X_MEASUREMENT) { + LOG_ERROR("%s: Not in measurement mode", sensorName); + return false; + } + + uint16_t co2, error; + float temperature, humidity; + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + bool dataReady; + error = scd4x.getDataReadyStatus(dataReady); + if (error != SCD4X_NO_ERROR || !dataReady) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + LOG_ERROR("SCD4X: Data is not ready"); + return false; + } + + error = scd4x.readMeasurement(co2, temperature, humidity); + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum%.2f", sensorName, co2, temperature, humidity); + if (error != SCD4X_NO_ERROR) { + LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error); + if (co2 == 0) { + LOG_ERROR("%s: Skipping invalid measurement.", sensorName); + } + return false; + } else { + measurement->variant.air_quality_metrics.has_co2_temperature = true; + measurement->variant.air_quality_metrics.has_co2_humidity = true; + measurement->variant.air_quality_metrics.has_co2 = true; + measurement->variant.air_quality_metrics.co2_temperature = temperature; + measurement->variant.air_quality_metrics.co2_humidity = humidity; + measurement->variant.air_quality_metrics.co2 = co2; + return true; + } +} + +/** + * @brief Perform a forced recalibration (FRC) of the CO₂ concentration. + * + * From Sensirion SCD4X I2C Library + * + * 1. Operate the SCD4x in the operation mode later used for normal sensor + * operation (e.g. periodic measurement) for at least 3 minutes in an + * environment with a homogenous and constant CO2 concentration. The sensor + * must be operated at the voltage desired for the application when + * performing the FRC sequence. 2. Issue the stop_periodic_measurement + * command. 3. Issue the perform_forced_recalibration command. + * @note This function should not change the clock + */ +bool SCD4XSensor::performFRC(uint32_t targetCO2) +{ + uint16_t error, frcCorr; + + LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); + + if (!stopMeasurement()) { + return false; + } + + LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2); + error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr); + + // SCD4X Sensirion datasheet + delay(400); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); + return false; + } + + if (frcCorr == 0xFFFF) { + LOG_ERROR("%s: Error while performing forced recalibration.", sensorName); + return false; + } + + LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr - 0x8000)); + + return true; +} + +/** + * @brief Start measurement mode + * @note This function should not change the clock + */ +bool SCD4XSensor::startMeasurement() +{ + uint16_t error; + + if (state == SCD4X_MEASUREMENT) { + LOG_DEBUG("%s: Already in measurement mode", sensorName); + return true; + } + + if (lowPower) { + error = scd4x.startLowPowerPeriodicMeasurement(); + } else { + error = scd4x.startPeriodicMeasurement(); + } + + if (error == SCD4X_NO_ERROR) { + LOG_INFO("%s: Started measurement mode", sensorName); + if (lowPower) { + LOG_INFO("%s: Low power mode", sensorName); + } else { + LOG_INFO("%s: Normal power mode", sensorName); + } + + state = SCD4X_MEASUREMENT; + return true; + } else { + LOG_ERROR("%s: Unable to start measurement mode", sensorName); + return false; + } +} + +/** + * @brief Stop measurement mode + * @note This function should not change the clock + */ +bool SCD4XSensor::stopMeasurement() +{ + uint16_t error; + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to stop measurement.", sensorName); + return false; + } + + state = SCD4X_IDLE; + co2MeasureStarted = 0; + return true; +} + +/** + * @brief Set power mode + * Pass true to set low power mode + * @note This function should not change the clock + */ +bool SCD4XSensor::setPowerMode(bool _lowPower) +{ + lowPower = _lowPower; + + if (!stopMeasurement()) { + return false; + } + + if (lowPower) { + LOG_DEBUG("%s: Set low power mode", sensorName); + } else { + LOG_DEBUG("%s: Set normal power mode", sensorName); + } + + return true; +} + +/** + * @brief Check the current mode (ASC or FRC) + * From Sensirion SCD4X I2C Library + * @note This function should not change the clock + */ +bool SCD4XSensor::getASC(uint16_t &_ascActive) +{ + uint16_t error; + LOG_INFO("%s: Getting ASC", sensorName); + + if (!stopMeasurement()) { + return false; + } + error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + LOG_INFO("%s ASC is %s", sensorName, _ascActive ? "enabled" : "disabled"); + + return true; +} + +/** + * @brief Enable or disable automatic self calibration (ASC). + * + * From Sensirion SCD4X I2C Library + * + * Sets the current state (enabled / disabled) of the ASC. By default, ASC + * is enabled. + * @note This function should not change the clock + */ +bool SCD4XSensor::setASC(bool ascEnabled) +{ + uint16_t error; + + LOG_INFO("%s %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling"); + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to make settings persistent.", sensorName); + return false; + } + + if (!getASC(ascActive)) { + LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); + return false; + } + + return true; +} + +/** + * @brief Set the value of ASC baseline target in ppm. + * + * From Sensirion SCD4X I2C Library. + * + * Sets the value of the ASC baseline target, i.e. the CO₂ concentration in + * ppm which the ASC algorithm will assume as lower-bound background to + * which the SCD4x is exposed to regularly within one ASC period of + * operation. To save the setting to the EEPROM, the persist_settings + * command must be issued subsequently. The factory default value is 400 + * ppm. + * @note This function should not change the clock + */ +bool SCD4XSensor::setASCBaseline(uint32_t targetCO2) +{ + // Available in library, but not described in datasheet. + uint16_t error; + LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2); + + getASC(ascActive); + if (!ascActive) { + LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName); + return false; + } + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to make settings persistent.", sensorName); + return false; + } + + LOG_INFO("%s: Setting ASC baseline successful", sensorName); + + return true; +} + +/** + * @brief Set the temperature compensation reference. + * + * From Sensirion SCD4X I2C Library. + * + * Setting the temperature offset of the SCD4x inside the customer device + * allows the user to optimize the RH and T output signal. + * By default, the temperature offset is set to 4 °C. To save + * the setting to the EEPROM, the persist_settings command may be issued. + * Equation (1) details how the characteristic temperature offset can be + * calculated using the current temperature output of the sensor (TSCD4x), a + * reference temperature value (TReference), and the previous temperature + * offset (Toffset_pervious) obtained using the get_temperature_offset_raw + * command: + * + * Toffset_actual = TSCD4x - TReference + Toffset_pervious. + * + * Recommended temperature offset values are between 0 °C and 20 °C. The + * temperature offset does not impact the accuracy of the CO2 output. + * @note This function should not change the clock + */ +bool SCD4XSensor::setTemperature(float tempReference) +{ + uint16_t error; + float prevTempOffset; + float updatedTempOffset; + float tempOffset; + bool dataReady; + uint16_t co2; + float temperature; + float humidity; + + LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference); + + error = scd4x.getDataReadyStatus(dataReady); + if (error != SCD4X_NO_ERROR || !dataReady) { + LOG_ERROR("%s: Data is not ready", sensorName); + return false; + } + + error = scd4x.readMeasurement(co2, temperature, humidity); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature); + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.getTemperatureOffset(prevTempOffset); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error); + return false; + } + LOG_INFO("%s: Current sensor temperature offset: %.2f", sensorName, prevTempOffset); + + tempOffset = temperature - tempReference + prevTempOffset; + + LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset); + error = scd4x.setTemperatureOffset(tempOffset); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); + return false; + } + + scd4x.getTemperatureOffset(updatedTempOffset); + LOG_INFO("%s: Updated sensor temperature offset: %.2f", sensorName, updatedTempOffset); + + return true; +} + +/** + * @brief Get the sensor altitude. + * + * From Sensirion SCD4X I2C Library. + * + * Altitude in meters above sea level can be set after device installation. + * Valid value between 0 and 3000m. This overrides pressure offset. + * @note This function should not change the clock + */ +bool SCD4XSensor::getAltitude(uint16_t &altitude) +{ + uint16_t error; + LOG_INFO("%s: Requesting sensor altitude", sensorName); + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.getSensorAltitude(altitude); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); + return false; + } + LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude); + + return true; +} + +/** + * @brief Get the ambient pressure around the sensor. + * + * From Sensirion SCD4X I2C Library. + * + * Gets the ambient pressure in Pa. + * @note This function should not change the clock + */ +bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure) +{ + uint16_t error; + LOG_INFO("%s: Requesting sensor ambient pressure", sensorName); + + error = scd4x.getAmbientPressure(ambientPressure); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); + return false; + } + LOG_INFO("%s: Sensor ambient pressure: %u", sensorName, ambientPressure); + + return true; +} + +/** + * @brief Set the sensor altitude. + * + * From Sensirion SCD4X I2C Library. + * + * Altitude in meters above sea level can be set after device installation. + * Valid value between 0 and 3000m. This overrides pressure offset. + * @note This function should not change the clock + */ +bool SCD4XSensor::setAltitude(uint32_t altitude) +{ + uint16_t error; + + if (!stopMeasurement()) { + return false; + } + LOG_INFO("%s: setting altitude at %um", sensorName, altitude); + + error = scd4x.setSensorAltitude(altitude); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); + return false; + } + + // NOTE: this gives an error if issued. Sensirion's library + // doesn't indicate it's needed. + // error = scd4x.persistSettings(); + // if (error != SCD4X_NO_ERROR) { + // LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); + // return false; + // } + + LOG_INFO("%s: altitude set", sensorName); + + return true; +} + +/** + * @brief Set the ambient pressure around the sensor. + * + * From Sensirion SCD4X I2C Library. + * + * The set_ambient_pressure command can be sent during periodic measurements + * to enable continuous pressure compensation. Note that setting an ambient + * pressure overrides any pressure compensation based on a previously set + * sensor altitude. Use of this command is highly recommended for + * applications experiencing significant ambient pressure changes to ensure + * sensor accuracy. Valid input values are between 70000 - 120000 Pa. The + * default value is 101300 Pa. + * @note This function should not change the clock + */ +bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) +{ + uint16_t error; + + LOG_INFO("%s: setting ambient pressure at %u Pa", sensorName, ambientPressure); + + error = scd4x.setAmbientPressure(ambientPressure); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); + return false; + } + + // Sensirion doesn't indicate if this is necessary. We send it anyway + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: ambient pressure set set", sensorName); + + return true; +} + +/** + * @brief Perform factory reset to erase the settings stored in the EEPROM. + * + * From Sensirion SCD4X I2C Library. + * + * The perform_factory_reset command resets all configuration settings + * stored in the EEPROM and erases the FRC and ASC algorithm history. + * @note This function should not change the clock + */ +bool SCD4XSensor::factoryReset() +{ + uint16_t error; + + LOG_INFO("%s: Requesting factory reset", sensorName); + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.performFactoryReset(); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: Factory reset successful", sensorName); + + return true; +} + +/** + * @brief Put the sensor into sleep mode from idle mode. + * + * From Sensirion SCD4X I2C Library. + * + * Put the sensor from idle to sleep to reduce power consumption. Can be + * used to power down when operating the sensor in power-cycled single shot + * mode. + * @note This command is only available in idle mode. Only for SCD41. + */ +bool SCD4XSensor::powerDown() +{ + LOG_INFO("%s: Trying to send sensor to sleep", sensorName); + + if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) { + LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring"); + return true; + } + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + if (!stopMeasurement()) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + if (scd4x.powerDown() != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Error trying to execute sleep()", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + state = SCD4X_OFF; + return true; +} + +/** + * @brief Wake up sensor from sleep mode to idle mode (powerUp) + * + * From Sensirion SCD4X I2C Library. + * + * Wake up the sensor from sleep mode into idle mode. Note that the SCD4x + * does not acknowledge the wake_up command. The sensor's idle state after + * wake up can be verified by reading out the serial number. + * @note This command is only available for SCD41. + * @note This function can't change clock (used in init) + */ +bool SCD4XSensor::powerUp() +{ + LOG_INFO("%s: Waking up", sensorName); + + if (scd4x.wakeUp() != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName); + return false; + } + + state = SCD4X_IDLE; + + return true; +} + +/** + * @brief Check if sensor is in measurement mode + */ +bool SCD4XSensor::isActive() +{ + return state == SCD4X_MEASUREMENT; +} + +/** + * @brief Start measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +uint32_t SCD4XSensor::wakeUp() +{ + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return 0; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + if (startMeasurement()) { + co2MeasureStarted = getTime(); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return SCD4X_WARMUP_MS; + } + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return 0; +} + +/** + * @brief Stop measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +void SCD4XSensor::sleep() +{ +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + stopMeasurement(); + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif +} + +/** + * @brief Can sleep function + * + * Power consumption is very low on lowPower mode, modify this function if + * you still want to override this behaviour. Otherwise, sleep is disabled + * routinely in low power mode + */ +bool SCD4XSensor::canSleep() +{ + return lowPower ? false : true; +} + +int32_t SCD4XSensor::wakeUpTimeMs() +{ + return SCD4X_WARMUP_MS; +} + +int32_t SCD4XSensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted) * 1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted); + + if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return SCD4X_WARMUP_MS - sinceCO2MeasureStarted; + } + return 0; +} + +AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return AdminMessageHandleResult::NOT_HANDLED; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + // TODO: potentially add selftest command? + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + // Check for ASC-FRC request first + if (!request->sensor_config.has_scd4x_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + if (request->sensor_config.scd4x_config.has_factory_reset) { + LOG_DEBUG("%s: Requested factory reset", sensorName); + if (!this->factoryReset()) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else { + if (request->sensor_config.scd4x_config.has_set_asc) { + getASC(ascActive); + bool currentASC = ascActive; + if (request->sensor_config.scd4x_config.set_asc == false) { + LOG_DEBUG("%s: Request for FRC", sensorName); + if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { + if (this->setASC(request->sensor_config.scd4x_config.set_asc)) { + if (!this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc)) { + result = AdminMessageHandleResult::NOT_HANDLED; + // Set it back to ASC if failed + setASC(currentASC); + break; + }; + } else { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else { + // FRC requested but no target CO2 provided + LOG_ERROR("%s: target CO2 not provided", sensorName); + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else { + LOG_DEBUG("%s: Request for ASC", sensorName); + if (this->setASC(request->sensor_config.scd4x_config.set_asc)) { + if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { + LOG_DEBUG("%s: Request has target CO2", sensorName); + this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc); + // NOTE - in this situation, if we set ASC, but baseline set fails, we stay on ASC + } else { + LOG_DEBUG("%s: Request doesn't have target CO2", sensorName); + } + } else { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + } + + // Check for temperature offset + // NOTE: this requires to have a sensor working on stable environment + // And to make it between readings + if (request->sensor_config.scd4x_config.has_set_temperature) { + if (!this->setTemperature(request->sensor_config.scd4x_config.set_temperature)) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + + // Check for altitude or pressure offset + if (request->sensor_config.scd4x_config.has_set_altitude) { + if (!this->setAltitude(request->sensor_config.scd4x_config.set_altitude)) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) { + if (!this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure)) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + + // Check for low power mode + // NOTE: to switch from one mode to another do: + // setPowerMode -> startMeasurement + if (request->sensor_config.scd4x_config.has_set_power_mode) { + if (!this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode)) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + // Start measurement mode + this->startMeasurement(); + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return result; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h new file mode 100644 index 000000000..1ed86a183 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -0,0 +1,63 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" +#include "TelemetrySensor.h" +#include + +// Max speed 400kHz +#define SCD4X_I2C_CLOCK_SPEED 100000 +#define SCD4X_WARMUP_MS 5000 + +class SCD4XSensor : public TelemetrySensor +{ + private: + SensirionI2cScd4x scd4x; + TwoWire *_bus{}; + uint8_t _address{}; + + bool performFRC(uint32_t targetCO2); + bool setASCBaseline(uint32_t targetCO2); + bool getASC(uint16_t &ascEnabled); + bool setASC(bool ascEnabled); + bool setTemperature(float tempReference); + bool getAltitude(uint16_t &altitude); + bool setAltitude(uint32_t altitude); + bool getAmbientPressure(uint32_t &ambientPressure); + bool setAmbientPressure(uint32_t ambientPressure); + bool factoryReset(); + bool setPowerMode(bool _lowPower); + bool startMeasurement(); + bool stopMeasurement(); + + uint16_t ascActive = 1; + // low power measurement mode (on sensirion side). Disables sleep mode + // Improvement and testing needed for timings + bool lowPower = true; + uint32_t co2MeasureStarted = 0; + + public: + SCD4XSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + + enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT }; + SCD4XState state = SCD4X_OFF; + SCD4xSensorVariant sensorVariant{}; + + virtual bool isActive() override; + + virtual void sleep() override; // Stops measurement (measurement -> idle) + virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement) + bool powerDown(); // Powers down sensor (idle -> power-off) + bool powerUp(); // Powers the sensor (power-off -> idle) + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp new file mode 100644 index 000000000..2feac6d5f --- /dev/null +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -0,0 +1,969 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "FSCommon.h" +#include "SEN5XSensor.h" +#include "SPILock.h" +#include "SafeFile.h" +#include "TelemetrySensor.h" +#include // FLT_MAX +#include +#include + +SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} + +bool SEN5XSensor::getVersion() +{ + if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)) { + LOG_ERROR("SEN5X: Error sending version command"); + return false; + } + delay(20); // From Sensirion Datasheet + + uint8_t versionBuffer[12]{}; + size_t charNumber = readBuffer(&versionBuffer[0], 3); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting data ready flag value"); + return false; + } + + firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10); + hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10); + protocolVer = versionBuffer[5] + (versionBuffer[6] / 10); + + LOG_INFO("SEN5X Firmware Version: %0.2f", firmwareVer); + LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer); + LOG_INFO("SEN5X Protocol Version: %0.2f", protocolVer); + + return true; +} + +bool SEN5XSensor::findModel() +{ + if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) { + LOG_ERROR("SEN5X: Error asking for product name"); + return false; + } + delay(50); // From Sensirion Datasheet + + const uint8_t nameSize = 48; + uint8_t name[nameSize]; + size_t charNumber = readBuffer(&name[0], nameSize); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting device name"); + return false; + } + + // We only check the last character that defines the model SEN5X + switch (name[4]) { + case 48: + model = SEN50; + LOG_INFO("SEN5X: found sensor model SEN50"); + break; + case 52: + model = SEN54; + LOG_INFO("SEN5X: found sensor model SEN54"); + break; + case 53: + model = SEN55; + LOG_INFO("SEN5X: found sensor model SEN55"); + break; + } + + return true; +} + +bool SEN5XSensor::sendCommand(uint16_t command) +{ + uint8_t nothing; + return sendCommand(command, ¬hing, 0); +} + +bool SEN5XSensor::sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber) +{ + // At least we need two bytes for the command + uint8_t bufferSize = 2; + + // Add space for CRC bytes (one every two bytes) + if (byteNumber > 0) + bufferSize += byteNumber + (byteNumber / 2); + + uint8_t toSend[bufferSize]; + uint8_t i = 0; + toSend[i++] = static_cast((command & 0xFF00) >> 8); + toSend[i++] = static_cast((command & 0x00FF) >> 0); + + // Prepare buffer with CRC every third byte + uint8_t bi = 0; + if (byteNumber > 0) { + while (bi < byteNumber) { + toSend[i++] = buffer[bi++]; + toSend[i++] = buffer[bi++]; + uint8_t calcCRC = sen5xCRC(&buffer[bi - 2]); + toSend[i++] = calcCRC; + } + } + +#ifdef SEN5X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SEN5X_I2C_CLOCK_SPEED */ + + // Transmit the data + // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize); + // Note: this delay is necessary to allow for long-buffers + delay(20); + _bus->beginTransmission(_address); + size_t writtenBytes = _bus->write(toSend, bufferSize); + uint8_t i2c_error = _bus->endTransmission(); + +#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + if (writtenBytes != bufferSize) { + LOG_ERROR("SEN5X: Error writting on I2C bus"); + return false; + } + + if (i2c_error != 0) { + LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error); + return false; + } + return true; +} + +uint8_t SEN5XSensor::readBuffer(uint8_t *buffer, uint8_t byteNumber) +{ +#ifdef SEN5X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SEN5X_I2C_CLOCK_SPEED */ + + size_t readBytes = _bus->requestFrom(_address, byteNumber); + if (readBytes != byteNumber) { + LOG_ERROR("SEN5X: Error reading I2C bus"); + return 0; + } + + uint8_t i = 0; + uint8_t receivedBytes = 0; + while (readBytes > 0) { + buffer[i++] = _bus->read(); // Just as a reminder: i++ returns i and after that increments. + buffer[i++] = _bus->read(); + uint8_t recvCRC = _bus->read(); + uint8_t calcCRC = sen5xCRC(&buffer[i - 2]); + if (recvCRC != calcCRC) { + LOG_ERROR("SEN5X: Checksum error while receiving msg"); + return 0; + } + readBytes -= 3; + receivedBytes += 2; + } +#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return receivedBytes; +} + +uint8_t SEN5XSensor::sen5xCRC(const uint8_t *buffer) +{ + // This code is based on Sensirion's own implementation + // https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp + uint8_t crc = 0xff; + + for (uint8_t i = 0; i < 2; i++) { + + crc ^= buffer[i]; + + for (uint8_t bit = 8; bit > 0; bit--) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x31; + else + crc = (crc << 1); + } + } + + return crc; +} + +void SEN5XSensor::sleep() +{ + idle(true); +} + +bool SEN5XSensor::idle(bool checkState) +{ + // From the datasheet: + // By default, the VOC algorithm resets its state to initial + // values each time a measurement is started, + // even if the measurement was stopped only for a short + // time. So, the VOC index output value needs a long time + // until it is stable again. This can be avoided by + // restoring the previously memorized algorithm state before + // starting the measure mode + + if (checkState) { + // If the stabilisation period is not passed for SEN54 or SEN55, don't go to idle + if (model != SEN50) { + // Get VOC state before going to idle mode + vocValid = false; + if (vocStateFromSensor()) { + vocValid = vocStateValid(); + // Check if we have time, and store it + uint32_t now; // If time is RTCQualityNone, it will return zero + now = getValidTime(RTCQuality::RTCQualityDevice); + // Check if state is valid (non-zero) + if (now) { + vocTime = now; + } + } + + if (!(vocStateStable() && vocValid)) { + LOG_INFO("%s: Not stopping measurement, vocState is not stable yet!", sensorName); + return true; + } + } + // Save state and prefs (on all models) + saveState(); + } + + if (!oneShotMode) { + LOG_INFO("%s: Not stopping measurement, continuous mode!", sensorName); + return true; + } else { + LOG_INFO("%s: One shot mode enabled", sensorName); + } + + // Switch to low-power based on the model + if (model == SEN50) { + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { + LOG_ERROR("%s: Error stopping measurement", sensorName); + return false; + } + state = SEN5X_IDLE; + LOG_INFO("%s: Stop measurement mode", sensorName); + } else { + if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) { + LOG_ERROR("%s: Error switching to RHT/Gas measurement", sensorName); + return false; + } + state = SEN5X_RHTGAS_ONLY; + LOG_INFO("%s: Switch to RHT/Gas only measurement mode", sensorName); + } + + delay(200); // From Sensirion Datasheet + pmMeasureStarted = 0; + return true; +} + +bool SEN5XSensor::vocStateRecent(uint32_t now) +{ + if (now) { + uint32_t passed = now - vocTime; // in seconds + + // Check if state is recent, less than 10 minutes (600 seconds) + if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) { + return true; + } + } + return false; +} + +bool SEN5XSensor::vocStateValid() +{ + if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && !vocState[4] && !vocState[5] && !vocState[6] && + !vocState[7]) { + LOG_DEBUG("%s: VOC state is all 0, invalid", sensorName); + return false; + } else { + LOG_DEBUG("%s: VOC state is valid", sensorName); + return true; + } +} + +bool SEN5XSensor::vocStateToSensor() +{ + if (model == SEN50) { + return true; + } + + if (!vocStateValid()) { + LOG_INFO("SEN5X: VOC state is invalid, not sending"); + return true; + } + + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error stoping measurement"); + return false; + } + delay(200); // From Sensirion Datasheet + + LOG_DEBUG("SEN5X: Sending VOC state to sensor"); + LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], vocState[3], vocState[4], vocState[5], + vocState[6], vocState[7]); + + // Note: send command already takes into account the CRC + // buffer size increment needed + if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)) { + LOG_ERROR("SEN5X: Error sending VOC's state command'"); + return false; + } + + return true; +} + +bool SEN5XSensor::vocStateFromSensor() +{ + if (model == SEN50) { + return true; + } + + LOG_INFO("SEN5X: Getting VOC state from sensor"); + // Ask VOCs state from the sensor + if (!sendCommand(SEN5X_RW_VOCS_STATE)) { + LOG_ERROR("SEN5X: Error sending VOC's state command'"); + return false; + } + + delay(20); // From Sensirion Datasheet + + // Retrieve the data + // Allocate buffer to account for CRC + size_t receivedNumber = readBuffer(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)); + delay(20); // From Sensirion Datasheet + + if (receivedNumber == 0) { + LOG_DEBUG("SEN5X: Error getting VOC's state"); + return false; + } + + // Print the state (if debug is on) + LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], + vocState[3], vocState[4], vocState[5], vocState[6], vocState[7]); + + return true; +} + +bool SEN5XSensor::loadState() +{ +#ifdef FSCom + spiLock->lock(); + auto file = FSCom.open(sen5XStateFileName, FILE_O_READ); + bool okay = false; + if (file) { + LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName); + pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size}; + + if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { + LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); + } else { + lastCleaning = sen5xstate.last_cleaning_time; + lastCleaningValid = sen5xstate.last_cleaning_valid; + oneShotMode = sen5xstate.one_shot_mode; + + if (model != SEN50) { + vocTime = sen5xstate.voc_state_time; + vocValid = sen5xstate.voc_state_valid; + // Unpack state + vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56); + vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48); + vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40); + vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32); + vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24); + vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16); + vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8); + vocState[0] = (uint8_t)sen5xstate.voc_state_array; + } + + // LOG_DEBUG("Loaded lastCleaning %u", lastCleaning); + // LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid); + // LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false"); + // LOG_DEBUG("Loaded vocTime %u", vocTime); + // LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]", + // vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]); + // LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in"); + + okay = true; + } + file.close(); + } else { + LOG_INFO("No %s state found (File: %s)", sensorName, sen5XStateFileName); + } + spiLock->unlock(); + return okay; +#else + LOG_ERROR("SEN5X: ERROR - Filesystem not implemented"); +#endif +} + +bool SEN5XSensor::saveState() +{ +#ifdef FSCom + auto file = SafeFile(sen5XStateFileName); + + sen5xstate.last_cleaning_time = lastCleaning; + sen5xstate.last_cleaning_valid = lastCleaningValid; + sen5xstate.one_shot_mode = oneShotMode; + + if (model != SEN50) { + sen5xstate.has_voc_state_time = true; + sen5xstate.has_voc_state_valid = true; + sen5xstate.has_voc_state_array = true; + + sen5xstate.voc_state_time = vocTime; + sen5xstate.voc_state_valid = vocValid; + // Unpack state (8 bytes) + sen5xstate.voc_state_array = (((uint64_t)vocState[7]) << 56) | ((uint64_t)vocState[6] << 48) | + ((uint64_t)vocState[5] << 40) | ((uint64_t)vocState[4] << 32) | + ((uint64_t)vocState[3] << 24) | ((uint64_t)vocState[2] << 16) | + ((uint64_t)vocState[1] << 8) | ((uint64_t)vocState[0]); + } + + bool okay = false; + + LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName); + pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_SEN5XState_size}; + + if (!pb_encode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { + LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); + } else { + okay = true; + } + + okay &= file.close(); + + if (okay) + LOG_INFO("%s: state write to %s successful", sensorName, sen5XStateFileName); + + return okay; +#else + LOG_ERROR("%s: ERROR - Filesystem not implemented", sensorName); +#endif +} + +bool SEN5XSensor::isActive() +{ + return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2; +} + +uint32_t SEN5XSensor::wakeUp() +{ + + LOG_DEBUG("SEN5X: Waking up sensor"); + + if (!sendCommand(SEN5X_START_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error starting measurement"); + // TODO - what should this return?? Something actually on the default interval? + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + delay(50); // From Sensirion Datasheet + + // TODO - This is currently "problematic" + // If time is updated in between reads, there is no way to + // keep track of how long it has passed + pmMeasureStarted = getTime(); + state = SEN5X_MEASUREMENT; + if (state == SEN5X_MEASUREMENT) + LOG_INFO("SEN5X: Started measurement mode"); + return SEN5X_WARMUP_MS_1; +} + +bool SEN5XSensor::vocStateStable() +{ + uint32_t now; + now = getTime(); + uint32_t sinceFirstMeasureStarted = (now - rhtGasMeasureStarted); + LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted); + return sinceFirstMeasureStarted > SEN5X_VOC_STATE_WARMUP_S; +} + +bool SEN5XSensor::startCleaning() +{ + // Note: we only should enter here if we have a valid RTC with at least + // RTCQuality::RTCQualityDevice + state = SEN5X_CLEANING; + + // Note that cleaning command can only be run when the sensor is in measurement mode + if (!sendCommand(SEN5X_START_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error starting measurment mode"); + return false; + } + delay(50); // From Sensirion Datasheet + + if (!sendCommand(SEN5X_START_FAN_CLEANING)) { + LOG_ERROR("SEN5X: Error starting fan cleaning"); + return false; + } + delay(20); // From Sensirion Datasheet + + // This message will be always printed so the user knows the device it's not hung + LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds..."); + + uint16_t started = millis(); + while (millis() - started < 10500) { + delay(500); + } + LOG_INFO("SEN5X: Cleaning done!!"); + + // Save timestamp in flash so we know when a week has passed + uint32_t now; + now = getValidTime(RTCQuality::RTCQualityDevice); + // If time is not RTCQualityNone, it will return non-zero + lastCleaning = now; + lastCleaningValid = true; + saveState(); + + idle(); + return true; +} + +bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + state = SEN5X_NOT_DETECTED; + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + + delay(50); // without this there is an error on the deviceReset function + + if (!sendCommand(SEN5X_RESET)) { + LOG_ERROR("SEN5X: Error reseting device"); + return false; + } + delay(200); // From Sensirion Datasheet + + if (!findModel()) { + LOG_ERROR("SEN5X: error finding sensor model"); + return false; + } + + // Check the firmware version + if (!getVersion()) + return false; + if (firmwareVer < 2) { + LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation"); + return false; + } + delay(200); // From Sensirion Datasheet + + // Detection succeeded + state = SEN5X_IDLE; + status = 1; + + // Load state + loadState(); + + // Check if it is time to do a cleaning + uint32_t now; + int32_t passed = 0; + now = getValidTime(RTCQuality::RTCQualityDevice); + + // If time is not RTCQualityNone, it will return non-zero + if (now) { + if (lastCleaningValid) { + + passed = now - lastCleaning; // in seconds + + if (passed > ONE_WEEK_IN_SECONDS && (now > SEN5X_VOC_VALID_DATE)) { + // If current date greater than 01/01/2018 (validity check) + LOG_INFO("SEN5X: More than a week (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed, + lastCleaning); + startCleaning(); + } else { + LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning); + } + } else { + // We assume the device has just been updated or it is new, + // so no need to trigger a cleaning. + // Just save the timestamp to do a cleaning one week from now. + // Otherwise, we will never trigger cleaning in some cases + lastCleaning = now; + lastCleaningValid = true; + LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning); + saveState(); + } + + if (model != SEN50) { + if (!vocValid) { + LOG_INFO("SEN5X: No valid VOC's state found"); + } else { + // Check if state is recent + if (vocStateRecent(now)) { + // If current date greater than 01/01/2018 (validity check) + // Send it to the sensor + LOG_INFO("SEN5X: VOC state is valid and recent"); + vocStateToSensor(); + } else { + LOG_INFO("SEN5X: VOC state is too old or date is invalid"); + LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now); + } + } + } + } else { + // TODO - Should this actually ignore? We could end up never cleaning... + LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved cleaning and VOC state"); + } + + idle(false); + rhtGasMeasureStarted = now; + + initI2CSensor(); + return true; +} + +bool SEN5XSensor::readValues() +{ + if (!sendCommand(SEN5X_READ_VALUES)) { + LOG_ERROR("SEN5X: Error sending read command"); + return false; + } + LOG_DEBUG("SEN5X: Reading PM Values"); + delay(20); // From Sensirion Datasheet + + uint8_t dataBuffer[16]{}; + size_t receivedNumber = readBuffer(&dataBuffer[0], 24); + if (receivedNumber == 0) { + LOG_ERROR("SEN5X: Error getting values"); + return false; + } + + // Get the integers + uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); + uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); + uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); + uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); + + int16_t int_humidity = static_cast((dataBuffer[8] << 8) | dataBuffer[9]); + int16_t int_temperature = static_cast((dataBuffer[10] << 8) | dataBuffer[11]); + int16_t int_vocIndex = static_cast((dataBuffer[12] << 8) | dataBuffer[13]); + int16_t int_noxIndex = static_cast((dataBuffer[14] << 8) | dataBuffer[15]); + + // Convert values based on Sensirion Arduino lib + sen5xmeasurement.pM1p0 = !isnan(uint_pM1p0) ? uint_pM1p0 / 10 : UINT16_MAX; + sen5xmeasurement.pM2p5 = !isnan(uint_pM2p5) ? uint_pM2p5 / 10 : UINT16_MAX; + sen5xmeasurement.pM4p0 = !isnan(uint_pM4p0) ? uint_pM4p0 / 10 : UINT16_MAX; + sen5xmeasurement.pM10p0 = !isnan(uint_pM10p0) ? uint_pM10p0 / 10 : UINT16_MAX; + sen5xmeasurement.humidity = !isnan(int_humidity) ? int_humidity / 100.0f : FLT_MAX; + sen5xmeasurement.temperature = !isnan(int_temperature) ? int_temperature / 200.0f : FLT_MAX; + sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX; + sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX; + + LOG_DEBUG("Got %s readings: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sensorName, sen5xmeasurement.pM1p0, + sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); + + if (model != SEN50) { + LOG_DEBUG("Got %s readings: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sensorName, sen5xmeasurement.humidity, + sen5xmeasurement.temperature, sen5xmeasurement.vocIndex); + } + + if (model == SEN55) { + LOG_DEBUG("Got %s readings: noxIndex=%.2f", sensorName, sen5xmeasurement.noxIndex); + } + + return true; +} + +bool SEN5XSensor::readPNValues(bool cumulative) +{ + if (!sendCommand(SEN5X_READ_PM_VALUES)) { + LOG_ERROR("SEN5X: Error sending read command"); + return false; + } + + LOG_DEBUG("SEN5X: Reading PN Values"); + delay(20); // From Sensirion Datasheet + + uint8_t dataBuffer[20]{}; + size_t receivedNumber = readBuffer(&dataBuffer[0], 30); + if (receivedNumber == 0) { + LOG_ERROR("SEN5X: Error getting PN values"); + return false; + } + + // Get the integers + // uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); + // uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); + // uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); + // uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); + uint16_t uint_pN0p5 = static_cast((dataBuffer[8] << 8) | dataBuffer[9]); + uint16_t uint_pN1p0 = static_cast((dataBuffer[10] << 8) | dataBuffer[11]); + uint16_t uint_pN2p5 = static_cast((dataBuffer[12] << 8) | dataBuffer[13]); + uint16_t uint_pN4p0 = static_cast((dataBuffer[14] << 8) | dataBuffer[15]); + uint16_t uint_pN10p0 = static_cast((dataBuffer[16] << 8) | dataBuffer[17]); + uint16_t uint_tSize = static_cast((dataBuffer[18] << 8) | dataBuffer[19]); + + // Convert values based on Sensirion Arduino lib + // Multiply by 100 for converting from #/cm3 to #/0.1l for PN values + sen5xmeasurement.pN0p5 = !isnan(uint_pN0p5) ? uint_pN0p5 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.pN1p0 = !isnan(uint_pN1p0) ? uint_pN1p0 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.pN2p5 = !isnan(uint_pN2p5) ? uint_pN2p5 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.pN4p0 = !isnan(uint_pN4p0) ? uint_pN4p0 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.pN10p0 = !isnan(uint_pN10p0) ? uint_pN10p0 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.tSize = !isnan(uint_tSize) ? uint_tSize / 1000.0f : FLT_MAX; + + // Remove accumuluative values: + // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85 + if (!cumulative) { + sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0; + sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5; + sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0; + sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5; + } + + LOG_DEBUG("Got %s readings: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sensorName, + sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, + sen5xmeasurement.pN10p0, sen5xmeasurement.tSize); + + return true; +} + +uint8_t SEN5XSensor::getMeasurements() +{ + uint32_t now; + now = getTime(); + + // Try to get new data + if (!sendCommand(SEN5X_READ_DATA_READY)) { + LOG_ERROR("SEN5X: Error sending command data ready flag"); + return 2; + } + delay(20); // From Sensirion Datasheet + + uint8_t dataReadyBuffer[3]; + size_t charNumber = readBuffer(&dataReadyBuffer[0], 3); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting device version value"); + return 2; + } + + bool dataReady = dataReadyBuffer[1]; + uint32_t sinceLastDataPollMs = (now - lastDataPoll) * 1000; + // Check if data is ready, and if since last time we requested is less than SEN5X_POLL_INTERVAL + if (!dataReady && (sinceLastDataPollMs > SEN5X_POLL_INTERVAL)) { + LOG_INFO("SEN5X: Data is not ready"); + return 1; + } + + if (!readValues()) { + LOG_ERROR("SEN5X: Error getting readings"); + return 2; + } + + if (!readPNValues(false)) { + LOG_ERROR("SEN5X: Error getting PN readings"); + return 2; + } + + lastDataPoll = now; + + return 0; +} + +int32_t SEN5XSensor::wakeUpTimeMs() +{ + return SEN5X_WARMUP_MS_2; +} + +int32_t SEN5XSensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000; + LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted); + + switch (state) { + case SEN5X_MEASUREMENT: { + + if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) { + LOG_INFO("SEN5X: not enough time passed since starting measurement"); + return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted; + } + + if (!pmMeasureStarted) { + pmMeasureStarted = now; + } + + // Get PN values to check if we are above or below threshold + readPNValues(true); + lastDataPoll = now; + + // If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later + if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { + LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period"); + state = SEN5X_MEASUREMENT_2; + // Report how many seconds are pending to cover the first warm up period + return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; + } + return 0; + } + case SEN5X_MEASUREMENT_2: { + if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { + // Report how many seconds are pending to cover the first warm up period + return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; + } + return 0; + } + default: { + return -1; + } + } +} + +bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_INFO("SEN5X: Attempting to get metrics"); + if (!isActive()) { + LOG_INFO("SEN5X: not in measurement mode"); + return false; + } + + uint8_t response; + response = getMeasurements(); + + if (response == 0) { + if (sen5xmeasurement.pM1p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0; + } + if (sen5xmeasurement.pM2p5 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5; + } + if (sen5xmeasurement.pM4p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm40_standard = true; + measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0; + } + if (sen5xmeasurement.pM10p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0; + } + if (sen5xmeasurement.pN0p5 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5; + } + if (sen5xmeasurement.pN1p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0; + } + if (sen5xmeasurement.pN2p5 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5; + } + if (sen5xmeasurement.pN4p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_40um = true; + measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0; + } + if (sen5xmeasurement.pN10p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0; + } + if (sen5xmeasurement.tSize != FLT_MAX) { + measurement->variant.air_quality_metrics.has_particles_tps = true; + measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize; + } + + if (model != SEN50) { + if (sen5xmeasurement.humidity != FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_humidity = true; + measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity; + } + if (sen5xmeasurement.temperature != FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_temperature = true; + measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature; + } + if (sen5xmeasurement.noxIndex != FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_voc_idx = true; + measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex; + } + } + + if (model == SEN55) { + if (sen5xmeasurement.noxIndex != FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_nox_idx = true; + measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex; + } + } + + return true; + } else if (response == 1) { + // TODO return because data was not ready yet + // Should this return false? + idle(); + return false; + } else if (response == 2) { + // Return with error for non-existing data + idle(); + return false; + } + + return true; +} + +void SEN5XSensor::setMode(bool setOneShot) +{ + oneShotMode = setOneShot; + if (oneShotMode) { + LOG_INFO("%s setting mode to one shot mode", sensorName); + } else { + LOG_INFO("%s setting mode to continuous mode", sensorName); + } +} + +AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + result = AdminMessageHandleResult::NOT_HANDLED; + + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + if (!request->sensor_config.has_sen5x_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + // Check for one-shot/continuous mode request + if (request->sensor_config.sen5x_config.has_set_one_shot_mode) { + this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode); + } + + // TODO - Add admin command to set temperature offset? + // Check for temperature offset + // if (request->sensor_config.sen5x_config.has_set_temperature) { + // this->setTemperature(request->sensor_config.sen5x_config.set_temperature); + // } + + // TODO - Add admin command to trigger fan cleaning? + // Check for one-shot/continuous mode request + // if (request->sensor_config.sen5x_config.has_fan_cleaning && request->sensor_config.sen5x_config.fan_cleaning) { + // this->startCleaning(); + // } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + return result; +} +#endif diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h new file mode 100644 index 000000000..ef5ad5c29 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -0,0 +1,170 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" +#include "TelemetrySensor.h" +#include "Wire.h" + +// Warm up times for SEN5X from the datasheet +#ifndef SEN5X_WARMUP_MS_1 +#define SEN5X_WARMUP_MS_1 15000 +#endif + +#ifndef SEN5X_WARMUP_MS_2 +#define SEN5X_WARMUP_MS_2 30000 +#endif + +#ifndef SEN5X_POLL_INTERVAL +#define SEN5X_POLL_INTERVAL 1000 +#endif + +#ifndef SEN5X_I2C_CLOCK_SPEED +#define SEN5X_I2C_CLOCK_SPEED 100000 +#endif + +/* +Time after which the sensor can go to sleep, as the warmup period has passed +and the VOCs sensor will is allowed to stop (although needs to recover the state +each time) +*/ +#ifndef SEN5X_VOC_STATE_WARMUP_S +/* Note for Testing 5' is enough +Sensirion recommends 1h +This can be bypassed completely if switching to low-power RHT/Gas mode and setting +SEN5X_VOC_STATE_WARMUP_S 0 +*/ +#define SEN5X_VOC_STATE_WARMUP_S 3600 +#endif + +#define ONE_WEEK_IN_SECONDS 604800 + +struct _SEN5XMeasurements { + uint16_t pM1p0; + uint16_t pM2p5; + uint16_t pM4p0; + uint16_t pM10p0; + uint32_t pN0p5; + uint32_t pN1p0; + uint32_t pN2p5; + uint32_t pN4p0; + uint32_t pN10p0; + float tSize; + float humidity; + float temperature; + float vocIndex; + float noxIndex; +}; + +class SEN5XSensor : public TelemetrySensor +{ + private: + TwoWire *_bus{}; + uint8_t _address{}; + + bool getVersion(); + float firmwareVer = -1; + float hardwareVer = -1; + float protocolVer = -1; + bool findModel(); + +// Commands +#define SEN5X_RESET 0xD304 +#define SEN5X_GET_PRODUCT_NAME 0xD014 +#define SEN5X_GET_FIRMWARE_VERSION 0xD100 +#define SEN5X_START_MEASUREMENT 0x0021 +#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037 +#define SEN5X_STOP_MEASUREMENT 0x0104 +#define SEN5X_READ_DATA_READY 0x0202 +#define SEN5X_START_FAN_CLEANING 0x5607 +#define SEN5X_RW_VOCS_STATE 0x6181 + +#define SEN5X_READ_VALUES 0x03C4 +#define SEN5X_READ_RAW_VALUES 0x03D2 +#define SEN5X_READ_PM_VALUES 0x0413 + +#define SEN5X_VOC_VALID_TIME 600 +#define SEN5X_VOC_VALID_DATE 1514764800 + + enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 }; + SEN5Xmodel model = SEN5X_UNKNOWN; + + enum SEN5XState { + SEN5X_OFF, + SEN5X_IDLE, + SEN5X_RHTGAS_ONLY, + SEN5X_MEASUREMENT, + SEN5X_MEASUREMENT_2, + SEN5X_CLEANING, + SEN5X_NOT_DETECTED + }; + SEN5XState state = SEN5X_OFF; + // Flag to work on one-shot (read and sleep), or continuous mode + bool oneShotMode = true; + void setMode(bool setOneShot); + bool vocStateValid(); +/* Sensirion recommends taking a reading after 15 seconds, +if the Particle number reading is over 100#/cm3 the reading is OK, +but if it is lower wait until 30 seconds and take it again. +See: https://sensirion.com/resource/application_note/low_power_mode/sen5x +*/ +#define SEN5X_PN4P0_CONC_THD 100 + + bool sendCommand(uint16_t command); + bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0); + uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber); // Return number of bytes received + uint8_t sen5xCRC(const uint8_t *buffer); + bool startCleaning(); + uint8_t getMeasurements(); + // bool readRawValues(); + bool readPNValues(bool cumulative); + bool readValues(); + + uint32_t pmMeasureStarted = 0; + uint32_t rhtGasMeasureStarted = 0; + uint32_t lastDataPoll = 0; + _SEN5XMeasurements sen5xmeasurement{}; + + bool idle(bool checkState = true); + + protected: + // Store status of the sensor in this file + const char *sen5XStateFileName = "/prefs/sen5X.dat"; + meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero; + + bool loadState(); + bool saveState(); + + // Cleaning State + uint32_t lastCleaning = 0; + bool lastCleaningValid = false; + +// VOC State +#define SEN5X_VOC_STATE_BUFFER_SIZE 8 + uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{}; + uint32_t vocTime = 0; + bool vocValid = false; + + bool vocStateFromSensor(); + bool vocStateToSensor(); + bool vocStateStable(); + bool vocStateRecent(uint32_t now); + + public: + SEN5XSensor(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; + virtual bool canSleep() override { return true; } + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; + + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SFA30Sensor.cpp b/src/modules/Telemetry/Sensor/SFA30Sensor.cpp new file mode 100644 index 000000000..c5b5845d9 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SFA30Sensor.cpp @@ -0,0 +1,198 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SFA30Sensor.h" + +SFA30Sensor::SFA30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SFA30, "SFA30"){}; + +bool SFA30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + sfa30.begin(*_bus, _address); + delay(20); + + if (this->isError(sfa30.deviceReset())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + state = State::IDLE; + if (this->isError(sfa30.startContinuousMeasurement())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + LOG_INFO("%s starting measurement", sensorName); + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + status = 1; + state = State::ACTIVE; + measureStarted = getTime(); + LOG_INFO("%s Enabled", sensorName); + + initI2CSensor(); + return true; +}; + +bool SFA30Sensor::isError(uint16_t response) +{ + if (response == SFA30_NO_ERROR) + return false; + + // TODO - Check error to char conversion + LOG_ERROR("%s: %u", sensorName, response); + return true; +} + +void SFA30Sensor::sleep() +{ +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + // Note - not recommended for this sensor on a periodic basis + if (this->isError(sfa30.stopMeasurement())) { + LOG_ERROR("%s: can't stop measurement", sensorName); + }; + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + LOG_INFO("%s: stop measurement", sensorName); + state = State::IDLE; + measureStarted = 0; +} + +uint32_t SFA30Sensor::wakeUp() +{ +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + LOG_INFO("Waking up %s", sensorName); + if (this->isError(sfa30.startContinuousMeasurement())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return 0; + } + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + state = State::ACTIVE; + measureStarted = getTime(); + return SFA30_WARMUP_MS; +} + +int32_t SFA30Sensor::wakeUpTimeMs() +{ + return SFA30_WARMUP_MS; +} + +bool SFA30Sensor::canSleep() +{ + // Sleep is disabled in this sensor because readings are not tested with periodic sleep + // with such low power consumption, prefered to keep it active + return false; +} + +bool SFA30Sensor::isActive() +{ + return state == State::ACTIVE; +} + +int32_t SFA30Sensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sinceHchoMeasureStarted = (now - measureStarted) * 1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceHchoMeasureStarted); + + if (sinceHchoMeasureStarted < SFA30_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return SFA30_WARMUP_MS - sinceHchoMeasureStarted; + } + return 0; +} + +bool SFA30Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + float hcho = 0.0; + float humidity = 0.0; + float temperature = 0.0; + +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + if (this->isError(sfa30.readMeasuredValues(hcho, humidity, temperature))) { + LOG_WARN("%s: No values", sensorName); + return false; + } + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + measurement->variant.air_quality_metrics.has_form_temperature = true; + measurement->variant.air_quality_metrics.has_form_humidity = true; + measurement->variant.air_quality_metrics.has_form_formaldehyde = true; + + measurement->variant.air_quality_metrics.form_temperature = temperature; + measurement->variant.air_quality_metrics.form_humidity = humidity; + measurement->variant.air_quality_metrics.form_formaldehyde = hcho; + + LOG_DEBUG("Got %s readings: hcho=%.2f, hcho_temp=%.2f, hcho_hum=%.2f", sensorName, hcho, temperature, humidity); + + return true; +} +#endif diff --git a/src/modules/Telemetry/Sensor/SFA30Sensor.h b/src/modules/Telemetry/Sensor/SFA30Sensor.h new file mode 100644 index 000000000..9fa9c85fc --- /dev/null +++ b/src/modules/Telemetry/Sensor/SFA30Sensor.h @@ -0,0 +1,39 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" +#include "TelemetrySensor.h" +#include + +#define SFA30_I2C_CLOCK_SPEED 100000 +#define SFA30_WARMUP_MS 10000 +#define SFA30_NO_ERROR 0 + +class SFA30Sensor : public TelemetrySensor +{ + private: + enum class State { IDLE, ACTIVE }; + State state = State::IDLE; + uint32_t measureStarted = 0; + + SensirionI2cSfa3x sfa30; + TwoWire *_bus{}; + uint8_t _address{}; + bool isError(uint16_t response); + + public: + SFA30Sensor(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; +}; + +#endif diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.cpp b/src/modules/Telemetry/Sensor/SHT31Sensor.cpp deleted file mode 100644 index 67a36933d..000000000 --- a/src/modules/Telemetry/Sensor/SHT31Sensor.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "SHT31Sensor.h" -#include "TelemetrySensor.h" -#include - -SHT31Sensor::SHT31Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT31, "SHT31") {} - -bool SHT31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - sht31 = Adafruit_SHT31(bus); - status = sht31.begin(dev->address.address); - initI2CSensor(); - return status; -} - -bool SHT31Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.temperature = sht31.readTemperature(); - measurement->variant.environment_metrics.relative_humidity = sht31.readHumidity(); - - return true; -} - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.h b/src/modules/Telemetry/Sensor/SHT31Sensor.h deleted file mode 100644 index ecb7d63a6..000000000 --- a/src/modules/Telemetry/Sensor/SHT31Sensor.h +++ /dev/null @@ -1,20 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" -#include - -class SHT31Sensor : public TelemetrySensor -{ - private: - Adafruit_SHT31 sht31; - - public: - SHT31Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -}; - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.cpp b/src/modules/Telemetry/Sensor/SHT4XSensor.cpp deleted file mode 100644 index b11795d97..000000000 --- a/src/modules/Telemetry/Sensor/SHT4XSensor.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "SHT4XSensor.h" -#include "TelemetrySensor.h" -#include - -SHT4XSensor::SHT4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT4X, "SHT4X") {} - -bool SHT4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - - uint32_t serialNumber = 0; - - status = sht4x.begin(bus); - if (!status) { - return status; - } - - serialNumber = sht4x.readSerial(); - if (serialNumber != 0) { - LOG_DEBUG("serialNumber : %x", serialNumber); - status = 1; - } else { - LOG_DEBUG("Error trying to execute readSerial(): "); - status = 0; - } - - initI2CSensor(); - return status; -} - -bool SHT4XSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - - sensors_event_t humidity, temp; - sht4x.getEvent(&humidity, &temp); - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - return true; -} - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.h b/src/modules/Telemetry/Sensor/SHT4XSensor.h deleted file mode 100644 index 7311d2366..000000000 --- a/src/modules/Telemetry/Sensor/SHT4XSensor.h +++ /dev/null @@ -1,20 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" -#include - -class SHT4XSensor : public TelemetrySensor -{ - private: - Adafruit_SHT4x sht4x = Adafruit_SHT4x(); - - public: - SHT4XSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -}; - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp b/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp deleted file mode 100644 index fdab0b266..000000000 --- a/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "SHTC3Sensor.h" -#include "TelemetrySensor.h" -#include - -SHTC3Sensor::SHTC3Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTC3, "SHTC3") {} - -bool SHTC3Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = shtc3.begin(bus); - - initI2CSensor(); - return status; -} - -bool SHTC3Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - - sensors_event_t humidity, temp; - shtc3.getEvent(&humidity, &temp); - - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - - return true; -} - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTC3Sensor.h b/src/modules/Telemetry/Sensor/SHTC3Sensor.h deleted file mode 100644 index 51cee18f7..000000000 --- a/src/modules/Telemetry/Sensor/SHTC3Sensor.h +++ /dev/null @@ -1,20 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" -#include - -class SHTC3Sensor : public TelemetrySensor -{ - private: - Adafruit_SHTC3 shtc3 = Adafruit_SHTC3(); - - public: - SHTC3Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -}; - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTXXSensor.cpp b/src/modules/Telemetry/Sensor/SHTXXSensor.cpp new file mode 100644 index 000000000..92cac7f77 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHTXXSensor.cpp @@ -0,0 +1,145 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SHTXXSensor.h" +#include "TelemetrySensor.h" +#include + +SHTXXSensor::SHTXXSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTXX, "SHTXX") {} + +void SHTXXSensor::getSensorVariant(SHTSensor::SHTSensorType sensorType) +{ + switch (sensorType) { + case SHTSensor::SHTSensorType::SHT2X: + sensorVariant = "SHT2x"; + break; + + case SHTSensor::SHTSensorType::SHT3X: + case SHTSensor::SHTSensorType::SHT85: + sensorVariant = "SHT3x/SHT85"; + break; + + case SHTSensor::SHTSensorType::SHT3X_ALT: + sensorVariant = "SHT3x"; + break; + + case SHTSensor::SHTSensorType::SHTW1: + case SHTSensor::SHTSensorType::SHTW2: + case SHTSensor::SHTSensorType::SHTC1: + case SHTSensor::SHTSensorType::SHTC3: + sensorVariant = "SHTC1/SHTC3/SHTW1/SHTW2"; + break; + + case SHTSensor::SHTSensorType::SHT4X: + sensorVariant = "SHT4x"; + break; + + default: + sensorVariant = "Unknown"; + break; + } +} + +bool SHTXXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + + if (sht.init(*_bus)) { + LOG_INFO("%s: init(): success", sensorName); + getSensorVariant(sht.mSensorType); + LOG_INFO("%s Sensor detected: %s on 0x%x", sensorName, sensorVariant, _address); + status = 1; + } else { + LOG_ERROR("%s: init(): failed", sensorName); + } + + initI2CSensor(); + return status; +} + +/** + * Accuracy setting of measurement. + * Not all sensors support changing the sampling accuracy (only SHT3X and SHT4X) + * SHTAccuracy: + * - SHT_ACCURACY_HIGH: Highest repeatability at the cost of slower measurement + * - SHT_ACCURACY_MEDIUM: Balanced repeatability and speed of measurement + * - SHT_ACCURACY_LOW: Fastest measurement but lowest repeatability + */ +bool SHTXXSensor::setAccuracy(SHTSensor::SHTAccuracy newAccuracy) +{ + // Only SHT3X-family (including alternates) and SHT4X support changing accuracy + if (sht.mSensorType != SHTSensor::SHTSensorType::SHT3X && sht.mSensorType != SHTSensor::SHTSensorType::SHT3X_ALT && + sht.mSensorType != SHTSensor::SHTSensorType::SHT85 && sht.mSensorType != SHTSensor::SHTSensorType::SHT4X) { + LOG_WARN("%s doesn't support accuracy setting", sensorVariant); + return false; + } + LOG_INFO("%s: setting new accuracy setting", sensorVariant); + accuracy = newAccuracy; + return sht.setAccuracy(accuracy); +} + +bool SHTXXSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (sht.readSample()) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.temperature = sht.getTemperature(); + measurement->variant.environment_metrics.relative_humidity = sht.getHumidity(); + + LOG_INFO("%s (%s): Got: temp:%fdegC, hum:%f%%rh", sensorName, sensorVariant, + measurement->variant.environment_metrics.temperature, + measurement->variant.environment_metrics.relative_humidity); + + return true; + } else { + LOG_ERROR("%s (%s): read sample failed", sensorName, sensorVariant); + return false; + } +} + +AdminMessageHandleResult SHTXXSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + result = AdminMessageHandleResult::NOT_HANDLED; + + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + if (!request->sensor_config.has_shtxx_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + // Check for sensor accuracy setting + if (request->sensor_config.shtxx_config.has_set_accuracy) { + SHTSensor::SHTAccuracy newAccuracy; + if (request->sensor_config.shtxx_config.set_accuracy == 0) { + newAccuracy = SHTSensor::SHT_ACCURACY_LOW; + } else if (request->sensor_config.shtxx_config.set_accuracy == 1) { + newAccuracy = SHTSensor::SHT_ACCURACY_MEDIUM; + } else if (request->sensor_config.shtxx_config.set_accuracy == 2) { + newAccuracy = SHTSensor::SHT_ACCURACY_HIGH; + } else { + LOG_ERROR("%s: incorrect accuracy setting", sensorName); + result = AdminMessageHandleResult::HANDLED; + break; + } + this->setAccuracy(newAccuracy); + } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + return result; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTXXSensor.h b/src/modules/Telemetry/Sensor/SHTXXSensor.h new file mode 100644 index 000000000..e354e113f --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHTXXSensor.h @@ -0,0 +1,29 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class SHTXXSensor : public TelemetrySensor +{ + private: + SHTSensor sht; + TwoWire *_bus{}; + uint8_t _address{}; + SHTSensor::SHTAccuracy accuracy{}; + bool setAccuracy(SHTSensor::SHTAccuracy newAccuracy); + + public: + SHTXXSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + void getSensorVariant(SHTSensor::SHTSensorType); + const char *sensorVariant{}; + + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/T1000xSensor.cpp b/src/modules/Telemetry/Sensor/T1000xSensor.cpp index b123450ec..1e2a77e8b 100644 --- a/src/modules/Telemetry/Sensor/T1000xSensor.cpp +++ b/src/modules/Telemetry/Sensor/T1000xSensor.cpp @@ -73,11 +73,15 @@ float T1000xSensor::getTemp() float Vout = 0, Rt = 0, temp = 0; float Temp = 0; + // P0.4 is a sensor power enable GPIO, not a VCC ADC pin. + // Read BATTERY_PIN (with voltage divider) and cap at NTC_REF_VCC to estimate the sensor rail voltage. for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { - vcc_vot += analogRead(T1000X_VCC_PIN); + vcc_vot += analogRead(BATTERY_PIN); } vcc_vot = vcc_vot / T1000X_SENSE_SAMPLES; - vcc_vot = 2 * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; + vcc_vot = ADC_MULTIPLIER * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; + if (vcc_vot > NTC_REF_VCC) + vcc_vot = NTC_REF_VCC; for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { ntc_vot += analogRead(T1000X_NTC_PIN); diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index af51ddfad..47deaa936 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" @@ -26,7 +26,6 @@ class TelemetrySensor this->status = 0; } - const char *sensorName; meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; unsigned status; bool initialized = false; @@ -56,13 +55,18 @@ class TelemetrySensor return AdminMessageHandleResult::NOT_HANDLED; } + const char *sensorName; // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + // Functions to sleep / wakeup sensors that support it + // These functions can save power consumption in cases like AQ virtual void sleep(){}; virtual uint32_t wakeUp() { return 0; } - // Return active by default, override per sensor - virtual bool isActive() { return true; } + virtual bool isActive() { return true; } // Return true by default, override per sensor + virtual bool canSleep() { return false; } // Return false by default, override per sensor + virtual int32_t wakeUpTimeMs() { return 0; } + virtual int32_t pendingForReadyMs() { return 0; } #if WIRE_INTERFACES_COUNT > 1 // Set to true if Implementation only works first I2C port (Wire) diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 41dc02cd1..3371c405d 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -266,7 +266,7 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti } } -void TraceRouteModule::updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) +void TraceRouteModule::updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) { // E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D // Similarly, if we are C, we can set D as next-hop for D diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h index a40ed7733..db94b9d9b 100644 --- a/src/modules/TraceRouteModule.h +++ b/src/modules/TraceRouteModule.h @@ -62,7 +62,7 @@ class TraceRouteModule : public ProtobufModule, void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly); // Update next-hops in the routing table based on the returned route - void updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); + void updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); // Helper to update next-hop for a single node void maybeSetNextHop(NodeNum target, uint8_t nextHopByte); diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp new file mode 100644 index 000000000..1ecb68c4b --- /dev/null +++ b/src/modules/TrafficManagementModule.cpp @@ -0,0 +1,1415 @@ +#include "TrafficManagementModule.h" + +#if HAS_TRAFFIC_MANAGEMENT + +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "Router.h" +#include "TypeConversions.h" +#include "airtime.h" +#include "concurrency/LockGuard.h" +#include "configuration.h" +#include "mesh-pb-constants.h" +#include "meshUtils.h" +#include +#include + +#define TM_LOG_DEBUG(fmt, ...) LOG_DEBUG("[TM] " fmt, ##__VA_ARGS__) +#define TM_LOG_INFO(fmt, ...) LOG_INFO("[TM] " fmt, ##__VA_ARGS__) +#define TM_LOG_WARN(fmt, ...) LOG_WARN("[TM] " fmt, ##__VA_ARGS__) + +// ============================================================================= +// Anonymous Namespace - Internal Helpers +// ============================================================================= + +namespace +{ + +constexpr uint32_t kMaintenanceIntervalMs = 60 * 1000UL; // Cache cleanup interval +constexpr uint32_t kUnknownResetMs = 60 * 1000UL; // Unknown packet window +constexpr uint8_t kMaxCuckooKicks = 16; // Max displacement chain length + +// NodeInfo direct response: enforced maximum hops by device role +// Both use maxHops logic (respond when hopsAway <= threshold) +// Config value is clamped to these role-based limits +// Note: nodeinfo_direct_response must also be enabled for this to take effect +constexpr uint32_t kRouterDefaultMaxHops = 3; // Routers: max 3 hops (can set lower via config) +constexpr uint32_t kClientDefaultMaxHops = 0; // Clients: direct only (cannot increase) + +/** + * Convert seconds to milliseconds with overflow protection. + */ +uint32_t secsToMs(uint32_t secs) +{ + uint64_t ms = static_cast(secs) * 1000ULL; + if (ms > UINT32_MAX) + return UINT32_MAX; + return static_cast(ms); +} + +/** + * Clamp precision to a valid dedup range. + * Invalid values use the module default precision. + */ +uint8_t sanitizePositionPrecision(uint8_t precision) +{ + if (precision > 0 && precision <= 32) + return precision; + + const uint8_t defaultPrecision = static_cast(default_traffic_mgmt_position_precision_bits); + if (defaultPrecision > 0 && defaultPrecision <= 32) + return defaultPrecision; + + // Someone done messed up if we reach here + return 32; +} + +/** + * Check if a timestamp is within a time window. + * Handles wrap-around correctly using unsigned subtraction. + */ +bool isWithinWindow(uint32_t nowMs, uint32_t startMs, uint32_t intervalMs) +{ + if (intervalMs == 0 || startMs == 0) + return false; + return (nowMs - startMs) < intervalMs; +} + +/** + * Truncate lat/lon to specified precision for position deduplication. + * + * The truncation works by masking off lower bits and rounding to the center + * of the resulting grid cell. This creates a stable truncated value even + * when GPS jitter causes small coordinate changes. + * + * @param value Raw latitude_i or longitude_i from position + * @param precision Number of significant bits to keep (0-32) + * @return Truncated and centered coordinate value + */ +int32_t truncateLatLon(int32_t value, uint8_t precision) +{ + if (precision == 0 || precision >= 32) + return value; + + // Create mask to zero out lower bits + uint32_t mask = UINT32_MAX << (32 - precision); + uint32_t truncated = static_cast(value) & mask; + + // Add half the truncation step to center in the grid cell + truncated += (1u << (31 - precision)); + return static_cast(truncated); +} + +/** + * Saturating increment for uint8_t counters. + * Prevents overflow by capping at UINT8_MAX (255). + */ +inline void saturatingIncrement(uint8_t &counter) +{ + if (counter < UINT8_MAX) + counter++; +} + +/** + * Return a short human-readable name for common port numbers. + * Falls back to "port:" for unknown ports. + */ +const char *portName(int portnum) +{ + switch (portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: + return "text"; + case meshtastic_PortNum_POSITION_APP: + return "position"; + case meshtastic_PortNum_NODEINFO_APP: + return "nodeinfo"; + case meshtastic_PortNum_ROUTING_APP: + return "routing"; + case meshtastic_PortNum_ADMIN_APP: + return "admin"; + case meshtastic_PortNum_TELEMETRY_APP: + return "telemetry"; + case meshtastic_PortNum_TRACEROUTE_APP: + return "traceroute"; + case meshtastic_PortNum_NEIGHBORINFO_APP: + return "neighborinfo"; + case meshtastic_PortNum_STORE_FORWARD_APP: + return "store-forward"; + case meshtastic_PortNum_WAYPOINT_APP: + return "waypoint"; + default: + return nullptr; + } +} + +} // namespace + +// ============================================================================= +// Module Instance +// ============================================================================= + +TrafficManagementModule *trafficManagementModule; + +// ============================================================================= +// Constructor +// ============================================================================= + +TrafficManagementModule::TrafficManagementModule() : MeshModule("TrafficManagement"), concurrency::OSThread("TrafficManagement") +{ + // Module configuration + isPromiscuous = true; // See all packets, not just those addressed to us + encryptedOk = true; // Can process encrypted packets + stats = meshtastic_TrafficManagementStats_init_zero; + + // Initialize rolling epoch for relative timestamps + cacheEpochMs = millis(); + + // Calculate adaptive time resolutions from config (config changes require reboot) + // Resolution = max(60, min(339, interval/2)) for ~24 hour range with good precision + posTimeResolution = calcTimeResolution(Default::getConfiguredOrDefault( + moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); + rateTimeResolution = calcTimeResolution(moduleConfig.traffic_management.rate_limit_window_secs); + unknownTimeResolution = calcTimeResolution(kUnknownResetMs / 1000); // ~5 min default + + const auto &cfg = moduleConfig.traffic_management; + TM_LOG_INFO("Enabled: pos_dedup=%d nodeinfo_resp=%d rate_limit=%d drop_unknown=%d exhaust_telem=%d exhaust_pos=%d " + "preserve_hops=%d", + cfg.position_dedup_enabled, cfg.nodeinfo_direct_response, cfg.rate_limit_enabled, cfg.drop_unknown_enabled, + cfg.exhaust_hop_telemetry, cfg.exhaust_hop_position, cfg.router_preserve_hops); + TM_LOG_DEBUG("Time resolutions: pos=%us, rate=%us, unknown=%us", posTimeResolution, rateTimeResolution, + unknownTimeResolution); + +// Allocate unified cache (10 bytes/entry for all platforms) +#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + const uint16_t allocSize = cacheSize(); + TM_LOG_INFO("Allocating unified cache: %u entries (%u bytes)", allocSize, + static_cast(allocSize * sizeof(UnifiedCacheEntry))); + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + // ESP32 with PSRAM: prefer PSRAM for large allocations + cache = static_cast(ps_calloc(allocSize, sizeof(UnifiedCacheEntry))); + if (cache) { + cacheFromPsram = true; + } else { + TM_LOG_WARN("PSRAM allocation failed, falling back to heap"); + cache = new UnifiedCacheEntry[allocSize](); + } +#else + // All other platforms: heap allocation + cache = new UnifiedCacheEntry[allocSize](); +#endif + +#endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + TM_LOG_INFO("Allocating NodeInfo cache: target=%u occupancy=%u%% payload=%u bytes (PSRAM) tags=%u bytes (%u-bit, %u slots, " + "%u buckets x %u)", + static_cast(nodeInfoTargetEntries()), static_cast(nodeInfoTargetOccupancyPercent()), + static_cast(nodeInfoTargetEntries() * sizeof(NodeInfoPayloadEntry)), + static_cast(nodeInfoIndexMetadataBudgetBytes()), static_cast(nodeInfoTagBits()), + static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoBucketCount()), + static_cast(nodeInfoBucketSize())); + + nodeInfoIndex = static_cast(calloc(nodeInfoIndexMetadataBudgetBytes(), sizeof(uint8_t))); + if (!nodeInfoIndex) { + TM_LOG_WARN("NodeInfo index allocation failed; direct responses will fall back to NodeDB"); + } else { + nodeInfoPayload = static_cast(ps_calloc(nodeInfoTargetEntries(), sizeof(NodeInfoPayloadEntry))); + if (nodeInfoPayload) { + nodeInfoPayloadFromPsram = true; + TM_LOG_INFO("NodeInfo bucketed cuckoo cache ready"); + } else { + TM_LOG_WARN("NodeInfo PSRAM payload allocation failed; direct responses will fall back to NodeDB"); + free(nodeInfoIndex); + nodeInfoIndex = nullptr; + } + } +#else + TM_LOG_DEBUG("NodeInfo PSRAM cache not available on this target"); +#endif + + setIntervalFromNow(kMaintenanceIntervalMs); +} + +// Cache may have been allocated via ps_calloc (PSRAM, C allocator) or new[] (heap). +// Must use the matching deallocator: free() for ps_calloc, delete[] for new[]. +TrafficManagementModule::~TrafficManagementModule() +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + if (cache) { + // Cache may be from ps_calloc (PSRAM, C allocator) or new[] (heap). + // Use the matching deallocator for the allocation source. + if (cacheFromPsram) + free(cache); + else + delete[] cache; + cache = nullptr; + } +#endif + + if (nodeInfoPayload) { + if (nodeInfoPayloadFromPsram) + free(nodeInfoPayload); + else + delete[] nodeInfoPayload; + nodeInfoPayload = nullptr; + } + + if (nodeInfoIndex) { + free(nodeInfoIndex); + nodeInfoIndex = nullptr; + } +} + +// ============================================================================= +// Statistics +// ============================================================================= + +meshtastic_TrafficManagementStats TrafficManagementModule::getStats() const +{ + concurrency::LockGuard guard(&cacheLock); + return stats; +} + +void TrafficManagementModule::resetStats() +{ + concurrency::LockGuard guard(&cacheLock); + stats = meshtastic_TrafficManagementStats_init_zero; +} + +void TrafficManagementModule::recordRouterHopPreserved() +{ + if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) + return; + incrementStat(&stats.router_hops_preserved); +} + +void TrafficManagementModule::incrementStat(uint32_t *field) +{ + concurrency::LockGuard guard(&cacheLock); + (*field)++; +} + +// ============================================================================= +// Cuckoo Hash Table Operations +// ============================================================================= + +/** + * Find an existing entry for the given node. + * + * Cuckoo hashing guarantees that if an entry exists, it's in one of exactly + * two locations: hash1(node) or hash2(node). This provides O(1) lookup. + * + * @param node NodeNum to search for + * @return Pointer to entry if found, nullptr otherwise + */ +TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findEntry(NodeNum node) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)node; + return nullptr; +#else + if (!cache || node == 0) + return nullptr; + + // Check primary location + uint16_t h1 = cuckooHash1(node); + if (cache[h1].node == node) + return &cache[h1]; + + // Check alternate location + uint16_t h2 = cuckooHash2(node); + if (cache[h2].node == node) + return &cache[h2]; + + return nullptr; +#endif +} + +/** + * Find or create an entry for the given node using cuckoo hashing. + * + * If the node exists, returns the existing entry. Otherwise, attempts to + * insert a new entry using cuckoo displacement: + * + * 1. Try to insert at h1(node) - if empty, done + * 2. Try to insert at h2(node) - if empty, done + * 3. Kick existing entry from h1 to its alternate location + * 4. Repeat up to kMaxCuckooKicks times + * 5. If cycle detected or max kicks exceeded, evict oldest entry + * + * @param node NodeNum to find or create + * @param isNew Set to true if a new entry was created + * @return Pointer to entry, or nullptr if allocation failed + */ +TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findOrCreateEntry(NodeNum node, bool *isNew) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)node; + if (isNew) + *isNew = false; + return nullptr; +#else + if (!cache || node == 0) { + if (isNew) + *isNew = false; + return nullptr; + } + + // Check if entry already exists (O(1) lookup) + uint16_t h1 = cuckooHash1(node); + if (cache[h1].node == node) { + if (isNew) + *isNew = false; + return &cache[h1]; + } + + uint16_t h2 = cuckooHash2(node); + if (cache[h2].node == node) { + if (isNew) + *isNew = false; + return &cache[h2]; + } + + // Entry doesn't exist - try to insert + + // Prefer empty slot at h1 + if (cache[h1].node == 0) { + memset(&cache[h1], 0, sizeof(UnifiedCacheEntry)); + cache[h1].node = node; + if (isNew) + *isNew = true; + return &cache[h1]; + } + + // Try empty slot at h2 + if (cache[h2].node == 0) { + memset(&cache[h2], 0, sizeof(UnifiedCacheEntry)); + cache[h2].node = node; + if (isNew) + *isNew = true; + return &cache[h2]; + } + + // Both slots occupied - perform cuckoo displacement + // Start by kicking entry at h1 to its alternate location + UnifiedCacheEntry displaced = cache[h1]; + memset(&cache[h1], 0, sizeof(UnifiedCacheEntry)); + cache[h1].node = node; + + for (uint8_t kicks = 0; kicks < kMaxCuckooKicks; kicks++) { + // Find alternate location for displaced entry + uint16_t altH1 = cuckooHash1(displaced.node); + uint16_t altH2 = cuckooHash2(displaced.node); + uint16_t altSlot = (altH1 == h1) ? altH2 : altH1; + + if (cache[altSlot].node == 0) { + // Found empty slot - insert displaced entry + cache[altSlot] = displaced; + if (isNew) + *isNew = true; + return &cache[h1]; + } + + // Kick entry from alternate slot + UnifiedCacheEntry temp = cache[altSlot]; + cache[altSlot] = displaced; + displaced = temp; + h1 = altSlot; + } + + // Cuckoo cycle detected or max kicks exceeded. + // The displaced entry has no valid cuckoo slot — drop it to preserve cache integrity. + // Placing it at an arbitrary slot would make it unreachable by findEntry(). + TM_LOG_DEBUG("Cuckoo cycle, evicting node 0x%08x", displaced.node); + + if (isNew) + *isNew = true; + return &cache[cuckooHash1(node)]; +#endif +} + +const TrafficManagementModule::NodeInfoPayloadEntry *TrafficManagementModule::findNodeInfoEntry(NodeNum node) const +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex || node == 0) + return nullptr; + + uint16_t payloadIndex = findNodeInfoPayloadIndex(node); + if (payloadIndex >= nodeInfoTargetEntries()) + return nullptr; + + return &nodeInfoPayload[payloadIndex]; +#else + (void)node; + return nullptr; +#endif +} + +uint16_t TrafficManagementModule::encodeNodeInfoTag(uint16_t payloadIndex) const +{ + if (payloadIndex >= nodeInfoTargetEntries()) + return 0; + return static_cast(payloadIndex + 1u); +} + +uint16_t TrafficManagementModule::decodeNodeInfoPayloadIndex(uint16_t tag) const +{ + if (tag == 0 || tag > nodeInfoTargetEntries()) + return UINT16_MAX; + return static_cast(tag - 1u); +} + +uint16_t TrafficManagementModule::getNodeInfoTag(uint16_t slot) const +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex || slot >= nodeInfoIndexSlots()) + return 0; + + const uint32_t bitOffset = static_cast(slot) * nodeInfoTagBits(); + const uint16_t byteOffset = static_cast(bitOffset >> 3); + const uint8_t shift = static_cast(bitOffset & 7u); + uint32_t packed = 0; + + if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset]); + if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset + 1u]) << 8; + if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset + 2u]) << 16; + + return static_cast((packed >> shift) & nodeInfoTagMask()); +#else + (void)slot; + return 0; +#endif +} + +void TrafficManagementModule::setNodeInfoTag(uint16_t slot, uint16_t tag) +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex || slot >= nodeInfoIndexSlots()) + return; + + const uint16_t normalizedTag = static_cast(tag & nodeInfoTagMask()); + const uint32_t bitOffset = static_cast(slot) * nodeInfoTagBits(); + const uint16_t byteOffset = static_cast(bitOffset >> 3); + const uint8_t shift = static_cast(bitOffset & 7u); + uint32_t packed = 0; + + if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset]); + if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset + 1u]) << 8; + if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset + 2u]) << 16; + + const uint32_t mask = static_cast(nodeInfoTagMask()) << shift; + packed = (packed & ~mask) | ((static_cast(normalizedTag) << shift) & mask); + + if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) + nodeInfoIndex[byteOffset] = static_cast(packed & 0xFFu); + if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) + nodeInfoIndex[byteOffset + 1u] = static_cast((packed >> 8) & 0xFFu); + if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) + nodeInfoIndex[byteOffset + 2u] = static_cast((packed >> 16) & 0xFFu); +#else + (void)slot; + (void)tag; +#endif +} + +uint16_t TrafficManagementModule::findNodeInfoPayloadIndex(NodeNum node) const +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex || node == 0) + return UINT16_MAX; + + const uint16_t buckets[2] = {nodeInfoHash1(node), nodeInfoHash2(node)}; + + for (uint8_t b = 0; b < 2; b++) { + const uint16_t base = static_cast(buckets[b] * nodeInfoBucketSize()); + for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { + uint16_t tag = getNodeInfoTag(static_cast(base + slot)); + if (tag == 0) + continue; + + uint16_t payloadIndex = decodeNodeInfoPayloadIndex(tag); + if (payloadIndex >= nodeInfoTargetEntries()) + continue; + + if (nodeInfoPayload[payloadIndex].node == node) + return payloadIndex; + } + } + + return UINT16_MAX; +#else + (void)node; + return UINT16_MAX; +#endif +} + +bool TrafficManagementModule::removeNodeInfoIndexEntry(NodeNum node, uint16_t payloadIndex) +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex || node == 0 || payloadIndex >= nodeInfoTargetEntries()) + return false; + + const uint16_t payloadTag = encodeNodeInfoTag(payloadIndex); + if (payloadTag == 0) + return false; + const uint16_t buckets[2] = {nodeInfoHash1(node), nodeInfoHash2(node)}; + + for (uint8_t b = 0; b < 2; b++) { + const uint16_t base = static_cast(buckets[b] * nodeInfoBucketSize()); + for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { + const uint16_t indexSlot = static_cast(base + slot); + if (getNodeInfoTag(indexSlot) == payloadTag) { + setNodeInfoTag(indexSlot, 0); + return true; + } + } + } + + return false; +#else + (void)node; + (void)payloadIndex; + return false; +#endif +} + +uint16_t TrafficManagementModule::allocateNodeInfoPayloadSlot() +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload) + return UINT16_MAX; + + for (uint16_t tries = 0; tries < nodeInfoTargetEntries(); tries++) { + uint16_t idx = static_cast((nodeInfoAllocHint + tries) % nodeInfoTargetEntries()); + if (nodeInfoPayload[idx].node == 0) { + nodeInfoAllocHint = static_cast((idx + 1u) % nodeInfoTargetEntries()); + return idx; + } + } +#endif + return UINT16_MAX; +} + +uint16_t TrafficManagementModule::evictNodeInfoPayloadSlot() +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex) + return UINT16_MAX; + + for (uint16_t tries = 0; tries < nodeInfoTargetEntries(); tries++) { + uint16_t idx = static_cast(nodeInfoEvictCursor % nodeInfoTargetEntries()); + nodeInfoEvictCursor = static_cast((nodeInfoEvictCursor + 1u) % nodeInfoTargetEntries()); + + NodeNum oldNode = nodeInfoPayload[idx].node; + if (oldNode == 0) + continue; + + removeNodeInfoIndexEntry(oldNode, idx); // best effort; cache tolerates occasional stale miss + nodeInfoPayload[idx].node = 0; + return idx; + } +#endif + return UINT16_MAX; +} + +bool TrafficManagementModule::tryInsertNodeInfoEntryInBucket(uint16_t bucket, uint16_t tag) +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex || !nodeInfoPayload || bucket >= nodeInfoBucketCount() || tag == 0) + return false; + + const uint16_t base = static_cast(bucket * nodeInfoBucketSize()); + for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { + const uint16_t indexSlot = static_cast(base + slot); + const uint16_t existingTag = getNodeInfoTag(indexSlot); + if (existingTag == 0) { + setNodeInfoTag(indexSlot, tag); + return true; + } + + // Opportunistically reuse stale tags that point at empty/invalid payload slots. + const uint16_t payloadIndex = decodeNodeInfoPayloadIndex(existingTag); + if (payloadIndex >= nodeInfoTargetEntries() || nodeInfoPayload[payloadIndex].node == 0) { + setNodeInfoTag(indexSlot, tag); + return true; + } + } +#else + (void)bucket; + (void)tag; +#endif + return false; +} + +TrafficManagementModule::NodeInfoPayloadEntry *TrafficManagementModule::findOrCreateNodeInfoEntry(NodeNum node, + bool *usedEmptySlot) +{ + if (usedEmptySlot) + *usedEmptySlot = false; + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex || node == 0) + return nullptr; + + uint16_t existing = findNodeInfoPayloadIndex(node); + if (existing < nodeInfoTargetEntries()) + return &nodeInfoPayload[existing]; + + const uint16_t beforeCount = countNodeInfoEntriesLocked(); + + uint16_t payloadIndex = allocateNodeInfoPayloadSlot(); + if (payloadIndex == UINT16_MAX) { + payloadIndex = evictNodeInfoPayloadSlot(); + if (payloadIndex == UINT16_MAX) + return nullptr; + } + + nodeInfoPayload[payloadIndex].node = node; + + // 4-way bucketed cuckoo insertion mirrors Cuckoo Filter practice from + // Fan et al. (CoNEXT 2014): high occupancy with short relocation chains. + uint16_t pending = encodeNodeInfoTag(payloadIndex); + uint16_t h1 = nodeInfoHash1(node); + uint16_t h2 = nodeInfoHash2(node); + + if (!tryInsertNodeInfoEntryInBucket(h1, pending) && !tryInsertNodeInfoEntryInBucket(h2, pending)) { + uint16_t currentBucket = h1; + for (uint8_t kicks = 0; kicks < kMaxCuckooKicks; kicks++) { + const uint16_t base = static_cast(currentBucket * nodeInfoBucketSize()); + const uint16_t kickSlot = static_cast((node + kicks) & (nodeInfoBucketSize() - 1u)); + const uint16_t pos = static_cast(base + kickSlot); + + uint16_t displaced = getNodeInfoTag(pos); + setNodeInfoTag(pos, pending); + pending = displaced; + + uint16_t displacedPayload = decodeNodeInfoPayloadIndex(pending); + if (displacedPayload >= nodeInfoTargetEntries()) { + pending = 0; + break; + } + + NodeNum displacedNode = nodeInfoPayload[displacedPayload].node; + if (displacedNode == 0) { + pending = 0; + break; + } + + uint16_t altH1 = nodeInfoHash1(displacedNode); + uint16_t altH2 = nodeInfoHash2(displacedNode); + uint16_t altBucket = (altH1 == currentBucket) ? altH2 : altH1; + + if (tryInsertNodeInfoEntryInBucket(altBucket, pending)) { + pending = 0; + break; + } + + currentBucket = altBucket; + } + + if (pending != 0) { + uint16_t droppedPayload = decodeNodeInfoPayloadIndex(pending); + if (droppedPayload < nodeInfoTargetEntries()) + nodeInfoPayload[droppedPayload].node = 0; + TM_LOG_DEBUG("NodeInfo bucketed cuckoo overflow, dropped payload idx=%u", + static_cast(droppedPayload < nodeInfoTargetEntries() ? droppedPayload : UINT16_MAX)); + } + } + + uint16_t finalIndex = findNodeInfoPayloadIndex(node); + if (finalIndex >= nodeInfoTargetEntries()) { + // New entry did not survive insertion chain. + if (payloadIndex < nodeInfoTargetEntries() && nodeInfoPayload[payloadIndex].node == node) + nodeInfoPayload[payloadIndex].node = 0; + return nullptr; + } + + if (usedEmptySlot) { + const uint16_t afterCount = countNodeInfoEntriesLocked(); + *usedEmptySlot = afterCount > beforeCount; + } + + return &nodeInfoPayload[finalIndex]; +#else + (void)node; + return nullptr; +#endif +} + +uint16_t TrafficManagementModule::countNodeInfoEntriesLocked() const +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex) + return 0; + + uint16_t count = 0; + for (uint16_t i = 0; i < nodeInfoIndexSlots(); i++) { + if (getNodeInfoTag(i) != 0) + count++; + } + return count; +#else + return 0; +#endif +} + +void TrafficManagementModule::cacheNodeInfoPacket(const meshtastic_MeshPacket &mp) +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex || mp.decoded.payload.size == 0) + return; + + meshtastic_User user = meshtastic_User_init_zero; + if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &user)) + return; + + // Normalize user.id to the packet sender's node number. + snprintf(user.id, sizeof(user.id), "!%08x", getFrom(&mp)); + + bool usedEmptySlot = false; + uint16_t cachedCount = 0; + { + concurrency::LockGuard guard(&cacheLock); + NodeInfoPayloadEntry *entry = findOrCreateNodeInfoEntry(getFrom(&mp), &usedEmptySlot); + if (!entry) + return; + + // Cache both payload and response metadata so direct replies can use + // richer context than "just the user protobuf" when PSRAM is present. + // This path is intentionally independent from NodeInfoModule/NodeDB. + entry->user = user; + entry->lastObservedMs = millis(); + entry->lastObservedRxTime = mp.rx_time; + entry->sourceChannel = mp.channel; + entry->hasDecodedBitfield = mp.decoded.has_bitfield; + entry->decodedBitfield = mp.decoded.bitfield; + + if (usedEmptySlot) + cachedCount = countNodeInfoEntriesLocked(); + } + + if (usedEmptySlot) { + TM_LOG_INFO("NodeInfo PSRAM cache entries: %u/%u target (%u packed slots, %u-bit tags, %u-byte DRAM index)", + static_cast(cachedCount), static_cast(nodeInfoTargetEntries()), + static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoTagBits()), + static_cast(nodeInfoIndexMetadataBudgetBytes())); + } +#else + (void)mp; +#endif +} + +// ============================================================================= +// Epoch Management +// ============================================================================= + +/** + * Reset the timestamp epoch when relative offsets approach overflow. + * + * Called when epoch age exceeds ~19 hours (approaching 8-bit minute overflow). + * Invalidates all cached per-node traffic state. + */ +void TrafficManagementModule::resetEpoch(uint32_t nowMs) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + TM_LOG_DEBUG("Resetting cache epoch"); + cacheEpochMs = nowMs; + + // Full flush avoids stale dedup identity/counters surviving epoch rollover. + memset(cache, 0, static_cast(cacheSize()) * sizeof(UnifiedCacheEntry)); +#else + (void)nowMs; +#endif +} + +// ============================================================================= +// Position Hash (Compact Mode) +// ============================================================================= + +/** + * Compute 8-bit position fingerprint from truncated lat/lon coordinates. + * + * Unlike a hash, this is deterministic: adjacent grid cells have sequential + * fingerprints, so nearby positions never collide. The fingerprint extracts + * the lower 4 significant bits from each truncated coordinate. + * + * Example with precision=16: + * lat_truncated = 0x12340000 (top 16 bits significant) + * Significant portion = 0x1234, lower 4 bits = 0x4 + * + * fingerprint = (lat_low4 << 4) | lon_low4 = 8 bits total + * + * Collision: Two positions collide only if they differ by a multiple of 16 + * grid cells in BOTH lat and lon dimensions simultaneously - very unlikely + * for typical position update patterns. + * + * @param lat_truncated Precision-truncated latitude + * @param lon_truncated Precision-truncated longitude + * @param precision Number of significant bits (1-32) + * @return 8-bit fingerprint (4 bits lat + 4 bits lon) + */ +uint8_t TrafficManagementModule::computePositionFingerprint(int32_t lat_truncated, int32_t lon_truncated, uint8_t precision) +{ + precision = sanitizePositionPrecision(precision); + + // Guard: if precision < 4, we have fewer bits to work with + // Take min(precision, 4) bits from each coordinate + uint8_t bitsToTake = (precision < 4) ? precision : 4; + + // Shift to move significant bits to bottom, then mask lower bits + // For precision=16: shift by 16 to get the 16 significant bits at bottom + uint8_t shift = 32 - precision; + uint8_t latBits = (static_cast(lat_truncated) >> shift) & ((1u << bitsToTake) - 1); + uint8_t lonBits = (static_cast(lon_truncated) >> shift) & ((1u << bitsToTake) - 1); + + return static_cast((latBits << 4) | lonBits); +} + +// ============================================================================= +// Packet Handling +// ============================================================================= + +// Processing order matters: this module runs BEFORE RoutingModule in the callModules() loop. +// - STOP prevents RoutingModule from calling sniffReceived() → perhapsRebroadcast(), +// so the packet is fully consumed (not forwarded). +// - ignoreRequest suppresses the default "no one responded" NAK for want_response packets. +// - exhaustRequested is set by alterReceived() and checked by perhapsRebroadcast() to +// force hop_limit=0 on the rebroadcast copy, allowing one final relay hop. +ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) + return ProcessMessage::CONTINUE; + + ignoreRequest = false; + exhaustRequested = false; // Reset per-packet; may be set by alterReceived() below + exhaustRequestedFrom = 0; + exhaustRequestedId = 0; + incrementStat(&stats.packets_inspected); + + const auto &cfg = moduleConfig.traffic_management; + const uint32_t nowMs = millis(); + + // ------------------------------------------------------------------------- + // Undecoded Packet Handling + // ------------------------------------------------------------------------- + // Packets we can't decode (wrong key, corruption, etc.) may indicate + // a misbehaving node. Track and optionally drop repeat offenders. + + if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { + if (cfg.drop_unknown_enabled && cfg.unknown_packet_threshold > 0) { + if (shouldDropUnknown(&mp, nowMs)) { + logAction("drop", &mp, "unknown"); + incrementStat(&stats.unknown_packet_drops); + ignoreRequest = true; // Suppress NAK for want_response packets + return ProcessMessage::STOP; // Consumed — will not be rebroadcast + } + } + return ProcessMessage::CONTINUE; + } + + // Learn NodeInfo payloads into the dedicated PSRAM cache. + if (mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) + cacheNodeInfoPacket(mp); + + // ------------------------------------------------------------------------- + // NodeInfo Direct Response + // ------------------------------------------------------------------------- + // When we see a unicast NodeInfo request for a node we know about, + // respond directly from cache instead of forwarding the request. + // STOP prevents the request from being rebroadcast toward the target node, + // and our cached response is sent back to the requestor with hop_limit=0. + + if (cfg.nodeinfo_direct_response && mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP && mp.decoded.want_response && + !isBroadcast(mp.to) && !isToUs(&mp) && !isFromUs(&mp)) { + if (shouldRespondToNodeInfo(&mp, true)) { + meshtastic_User requester = meshtastic_User_init_zero; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) { + nodeDB->updateUser(getFrom(&mp), requester, mp.channel); + } + logAction("respond", &mp, "nodeinfo-cache"); + incrementStat(&stats.nodeinfo_cache_hits); + ignoreRequest = true; // We responded; suppress default NAK + return ProcessMessage::STOP; // Consumed — request will not be forwarded + } + } + + // ------------------------------------------------------------------------- + // Position Deduplication + // ------------------------------------------------------------------------- + // Drop position broadcasts that haven't moved significantly since the + // last broadcast from this node. Uses truncated coordinates to ignore + // GPS jitter within the configured precision. + + if (!isFromUs(&mp) && !isToUs(&mp)) { + if (cfg.position_dedup_enabled && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { + meshtastic_Position pos = meshtastic_Position_init_zero; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &pos)) { + if (shouldDropPosition(&mp, &pos, nowMs)) { + logAction("drop", &mp, "position-dedup"); + incrementStat(&stats.position_dedup_drops); + ignoreRequest = true; // Suppress NAK + return ProcessMessage::STOP; // Consumed — duplicate will not be rebroadcast + } + } + } + + // --------------------------------------------------------------------- + // Rate Limiting + // --------------------------------------------------------------------- + // Throttle nodes sending too many packets within a time window. + // Excludes routing and admin packets which are essential for mesh operation. + + if (cfg.rate_limit_enabled && cfg.rate_limit_window_secs > 0 && cfg.rate_limit_max_packets > 0) { + if (mp.decoded.portnum != meshtastic_PortNum_ROUTING_APP && mp.decoded.portnum != meshtastic_PortNum_ADMIN_APP) { + if (isRateLimited(mp.from, nowMs)) { + logAction("drop", &mp, "rate-limit"); + incrementStat(&stats.rate_limit_drops); + ignoreRequest = true; // Suppress NAK + return ProcessMessage::STOP; // Consumed — throttled packet will not be rebroadcast + } + } + } + } + + return ProcessMessage::CONTINUE; +} + +void TrafficManagementModule::alterReceived(meshtastic_MeshPacket &mp) +{ + if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) + return; + + if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) + return; + + if (isFromUs(&mp)) + return; + + // ------------------------------------------------------------------------- + // Relayed Broadcast Hop Exhaustion + // ------------------------------------------------------------------------- + // For relayed telemetry or position broadcasts from other nodes, optionally + // set hop_limit=0 so they don't propagate further through the mesh. + + const auto &cfg = moduleConfig.traffic_management; + const bool isTelemetry = mp.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP; + const bool isPosition = mp.decoded.portnum == meshtastic_PortNum_POSITION_APP; + // Only exhaust telemetry hops when channel is actually congested, mirroring the same + // airtime checks that gate self-generated telemetry in the telemetry modules. + const bool channelBusy = airTime && (!airTime->isTxAllowedChannelUtil(true) || !airTime->isTxAllowedAirUtil()); + const bool shouldExhaust = + ((channelBusy && isTelemetry && cfg.exhaust_hop_telemetry) || (isPosition && cfg.exhaust_hop_position)); + + if (!shouldExhaust || !isBroadcast(mp.to)) + return; + + if (mp.hop_limit > 0) { + const char *reason = isTelemetry ? "exhaust-hop-telemetry" : "exhaust-hop-position"; + logAction("exhaust", &mp, reason); + // Adjust hop_start so downstream nodes compute correct hopsAway (hop_start - hop_limit). + // Without this, hop_limit=0 with original hop_start would show inflated hopsAway. + mp.hop_start = mp.hop_start - mp.hop_limit + 1; + mp.hop_limit = 0; + // Signal perhapsRebroadcast() to allow one final relay with hop_limit=0. + // Without this flag, perhapsRebroadcast() would skip the packet since hop_limit==0. + // The packet-scoped flag is checked in NextHopRouter::perhapsRebroadcast() + // and forces tosend->hop_limit=0, ensuring no further propagation beyond the + // next node. + exhaustRequested = true; + exhaustRequestedFrom = getFrom(&mp); + exhaustRequestedId = mp.id; + incrementStat(&stats.hop_exhausted_packets); + } +} + +// ============================================================================= +// Periodic Maintenance +// ============================================================================= + +int32_t TrafficManagementModule::runOnce() +{ + if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) + return INT32_MAX; + +#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + const uint32_t nowMs = millis(); + + // Check if epoch reset needed (~3.5 hours approaching 8-bit minute overflow) + if (needsEpochReset(nowMs)) { + concurrency::LockGuard guard(&cacheLock); + resetEpoch(nowMs); + return kMaintenanceIntervalMs; + } + + // Calculate TTLs for cache expiration + const uint32_t positionIntervalMs = secsToMs(Default::getConfiguredOrDefault( + moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); + const uint32_t positionTtlMs = positionIntervalMs * 4; + + const uint32_t rateIntervalMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); + const uint32_t rateTtlMs = (rateIntervalMs > 0) ? rateIntervalMs * 2 : (10 * 60 * 1000UL); + + const uint32_t unknownTtlMs = kUnknownResetMs * 5; + + // Sweep cache and clear expired entries + uint16_t activeEntries = 0; + uint16_t expiredEntries = 0; + const uint32_t sweepStartMs = millis(); + + concurrency::LockGuard guard(&cacheLock); + for (uint16_t i = 0; i < cacheSize(); i++) { + if (cache[i].node == 0) + continue; + + bool anyValid = false; + + // Check and clear expired position data + if (cache[i].pos_time != 0) { + uint32_t posTimeMs = fromRelativePosTime(cache[i].pos_time); + if (!isWithinWindow(nowMs, posTimeMs, positionTtlMs)) { + cache[i].pos_fingerprint = 0; + cache[i].pos_time = 0; + } else { + anyValid = true; + } + } + + // Check and clear expired rate limit data + if (cache[i].rate_time != 0) { + uint32_t rateTimeMs = fromRelativeRateTime(cache[i].rate_time); + if (!isWithinWindow(nowMs, rateTimeMs, rateTtlMs)) { + cache[i].rate_count = 0; + cache[i].rate_time = 0; + } else { + anyValid = true; + } + } + + // Check and clear expired unknown tracking data + if (cache[i].unknown_time != 0) { + uint32_t unknownTimeMs = fromRelativeUnknownTime(cache[i].unknown_time); + if (!isWithinWindow(nowMs, unknownTimeMs, unknownTtlMs)) { + cache[i].unknown_count = 0; + cache[i].unknown_time = 0; + } else { + anyValid = true; + } + } + + // If all data expired, free the slot entirely + if (!anyValid) { + memset(&cache[i], 0, sizeof(UnifiedCacheEntry)); + expiredEntries++; + } else { + activeEntries++; + } + } + + TM_LOG_DEBUG("Maintenance: %u active, %u expired, %u/%u slots, %lums elapsed", activeEntries, expiredEntries, + static_cast(activeEntries), static_cast(cacheSize()), + static_cast(millis() - sweepStartMs)); + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (nodeInfoPayload && nodeInfoIndex) { + TM_LOG_DEBUG("NodeInfo PSRAM cache: %u/%u target (%u packed slots, %u buckets, %u-bit tags, %u-byte index)", + static_cast(countNodeInfoEntriesLocked()), static_cast(nodeInfoTargetEntries()), + static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoBucketCount()), + static_cast(nodeInfoTagBits()), static_cast(nodeInfoIndexMetadataBudgetBytes())); + } +#endif + +#endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + + return kMaintenanceIntervalMs; +} + +// ============================================================================= +// Traffic Management Logic +// ============================================================================= + +bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)p; + (void)pos; + (void)nowMs; + return false; +#else + if (!pos->has_latitude_i || !pos->has_longitude_i) + return false; + + uint8_t precision = Default::getConfiguredOrDefault(moduleConfig.traffic_management.position_precision_bits, + default_traffic_mgmt_position_precision_bits); + precision = sanitizePositionPrecision(precision); + + const int32_t lat_truncated = truncateLatLon(pos->latitude_i, precision); + const int32_t lon_truncated = truncateLatLon(pos->longitude_i, precision); + const uint8_t fingerprint = computePositionFingerprint(lat_truncated, lon_truncated, precision); + const uint32_t minIntervalMs = secsToMs(Default::getConfiguredOrDefault( + moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); + + bool isNew = false; + concurrency::LockGuard guard(&cacheLock); + UnifiedCacheEntry *entry = findOrCreateEntry(p->from, &isNew); + if (!entry) + return false; + + // Compare fingerprint and check time window + // When minIntervalMs == 0, deduplication is disabled (withinInterval = false means never drop) + const bool hasPositionState = !isNew && entry->pos_time != 0; + const bool samePosition = hasPositionState && entry->pos_fingerprint == fingerprint; + const bool withinInterval = + hasPositionState && (minIntervalMs != 0) && isWithinWindow(nowMs, fromRelativePosTime(entry->pos_time), minIntervalMs); + + TM_LOG_DEBUG("Position dedup 0x%08x: fp=0x%02x prev=0x%02x same=%d within=%d new=%d", p->from, fingerprint, + entry->pos_fingerprint, samePosition, withinInterval, isNew); + + // Update cache entry + entry->pos_fingerprint = fingerprint; + entry->pos_time = toRelativePosTime(nowMs); + + // Drop only if same position AND within the minimum interval + return samePosition && withinInterval; +#endif +} + +bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse) +{ + // Caller already verified: nodeinfo_direct_response, portnum, want_response, + // !isBroadcast, !isToUs, !isFromUs + + if (!isMinHopsFromRequestor(p)) + return false; + + meshtastic_User cachedUser = meshtastic_User_init_zero; + bool hasCachedUser = false; + + // Extra metadata consumed only by the PSRAM-backed cache path. + // Defaults preserve previous behavior when cache metadata is unavailable. + bool cachedHasDecodedBitfield = false; + uint8_t cachedDecodedBitfield = 0; + uint8_t cachedSourceChannel = 0; + uint32_t cachedLastObservedMs = 0; + uint32_t cachedLastObservedRxTime = 0; + + { + concurrency::LockGuard guard(&cacheLock); + const NodeInfoPayloadEntry *entry = findNodeInfoEntry(p->to); + if (entry) { + cachedUser = entry->user; + hasCachedUser = true; + cachedHasDecodedBitfield = entry->hasDecodedBitfield; + cachedDecodedBitfield = entry->decodedBitfield; + cachedSourceChannel = entry->sourceChannel; + cachedLastObservedMs = entry->lastObservedMs; + cachedLastObservedRxTime = entry->lastObservedRxTime; + } + } + + if (!hasCachedUser) { + // If the PSRAM cache exists but misses, we intentionally do not fall back + // to the node-wide table. This keeps the PSRAM direct-reply path separate + // from NodeInfoModule/NodeDB behavior when PSRAM is available. + if (nodeInfoPayload && nodeInfoIndex) { + TM_LOG_DEBUG("NodeInfo PSRAM cache miss for node=0x%08x", p->to); + return false; + } + + // Fallback only when PSRAM cache is unavailable on this target. + // In this mode we use the node-wide table maintained by NodeInfoModule. + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); + if (!node || !node->has_user) + return false; + cachedUser = TypeConversions::ConvertToUser(node->num, node->user); + } + + if (!sendResponse) + return true; + + meshtastic_MeshPacket *reply = router->allocForSending(); + if (!reply) { + TM_LOG_WARN("NodeInfo direct response dropped: no packet buffer"); + return false; + } + + reply->decoded.portnum = meshtastic_PortNum_NODEINFO_APP; + reply->decoded.payload.size = + pb_encode_to_bytes(reply->decoded.payload.bytes, sizeof(reply->decoded.payload.bytes), &meshtastic_User_msg, &cachedUser); + reply->decoded.want_response = false; + + // Start from cached bitfield metadata when available. This lets direct + // responses preserve more of the original packet semantics (PSRAM path), + // while still enforcing local policy for OK_TO_MQTT below. + if (cachedHasDecodedBitfield) + reply->decoded.bitfield = cachedDecodedBitfield; + else + reply->decoded.bitfield = 0; + + // Respect the node-wide config_ok_to_mqtt setting for direct NodeInfo replies. + // This response is spoofed from another node, so Router::perhapsEncode() + // will not auto-populate the bitfield via config_ok_to_mqtt for us. + reply->decoded.has_bitfield = true; + // Update only the OK_TO_MQTT bit; keep any other cached bits intact. + reply->decoded.bitfield &= ~BITFIELD_OK_TO_MQTT_MASK; + if (config.lora.config_ok_to_mqtt) + reply->decoded.bitfield |= BITFIELD_OK_TO_MQTT_MASK; + + if (hasCachedUser && cachedLastObservedMs != 0) { + uint32_t ageMs = millis() - cachedLastObservedMs; + TM_LOG_DEBUG("NodeInfo PSRAM hit node=0x%08x age=%lu ms src_ch=%u req_ch=%u rx_time=%lu", p->to, + static_cast(ageMs), static_cast(cachedSourceChannel), + static_cast(p->channel), static_cast(cachedLastObservedRxTime)); + } + + // Spoof the sender as the target node so the requestor sees a valid NodeInfo response. + // hop_limit=0 ensures this reply travels only one hop (direct to requestor). + reply->from = p->to; + reply->to = getFrom(p); + reply->channel = p->channel; + reply->decoded.request_id = p->id; + reply->hop_limit = 0; + // hop_start=0 is set explicitly because Router::send() only sets it for isFromUs(), + // and our spoofed from means isFromUs() is false. + reply->hop_start = 0; + reply->next_hop = nodeDB->getLastByteOfNodeNum(getFrom(p)); + reply->priority = meshtastic_MeshPacket_Priority_DEFAULT; + + service->sendToMesh(reply); + return true; +} + +bool TrafficManagementModule::isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const +{ + int8_t hopsAway = getHopsAway(*p, -1); + if (hopsAway < 0) + return false; + + // Both routers and clients use maxHops logic (respond when hopsAway <= threshold) + // Role determines the maximum allowed value (enforced limit, not just default) + bool isRouter = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); + + uint32_t roleLimit = isRouter ? kRouterDefaultMaxHops : kClientDefaultMaxHops; + uint32_t configValue = moduleConfig.traffic_management.nodeinfo_direct_response_max_hops; + + // Use config value if set, otherwise use role default, but always clamp to role limit + uint32_t maxHops = (configValue > 0) ? configValue : roleLimit; + if (maxHops > roleLimit) + maxHops = roleLimit; + + bool result = static_cast(hopsAway) <= maxHops; + TM_LOG_DEBUG("NodeInfo hops check: hopsAway=%d maxHops=%u roleLimit=%u isRouter=%d -> %s", hopsAway, maxHops, roleLimit, + isRouter, result ? "respond" : "skip"); + return result; +} + +bool TrafficManagementModule::isRateLimited(NodeNum from, uint32_t nowMs) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)from; + (void)nowMs; + return false; +#else + const uint32_t windowMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); + if (windowMs == 0 || moduleConfig.traffic_management.rate_limit_max_packets == 0) + return false; + + bool isNew = false; + concurrency::LockGuard guard(&cacheLock); + UnifiedCacheEntry *entry = findOrCreateEntry(from, &isNew); + if (!entry) + return false; + + // Check if window has expired + if (isNew || !isWithinWindow(nowMs, fromRelativeRateTime(entry->rate_time), windowMs)) { + entry->rate_time = toRelativeRateTime(nowMs); + entry->rate_count = 1; + return false; + } + + // Increment counter (saturates at 255) + saturatingIncrement(entry->rate_count); + + // Check against threshold (uint8_t max is 255, but config is uint32_t) + uint32_t threshold = moduleConfig.traffic_management.rate_limit_max_packets; + if (threshold > 255) + threshold = 255; + + bool limited = entry->rate_count > threshold; + if (limited || entry->rate_count == threshold) { + TM_LOG_DEBUG("Rate limit 0x%08x: count=%u threshold=%u -> %s", from, entry->rate_count, threshold, + limited ? "DROP" : "at-limit"); + } + return limited; +#endif +} + +bool TrafficManagementModule::shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)p; + (void)nowMs; + return false; +#else + if (!moduleConfig.traffic_management.drop_unknown_enabled || moduleConfig.traffic_management.unknown_packet_threshold == 0) + return false; + + uint32_t windowMs = kUnknownResetMs; + if (moduleConfig.traffic_management.rate_limit_window_secs > 0) + windowMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); + + bool isNew = false; + concurrency::LockGuard guard(&cacheLock); + UnifiedCacheEntry *entry = findOrCreateEntry(p->from, &isNew); + if (!entry) + return false; + + // Check if window has expired + if (isNew || !isWithinWindow(nowMs, fromRelativeUnknownTime(entry->unknown_time), windowMs)) { + entry->unknown_time = toRelativeUnknownTime(nowMs); + entry->unknown_count = 0; + } + + // Increment counter (saturates at 255) + saturatingIncrement(entry->unknown_count); + + // Check against threshold + uint32_t threshold = moduleConfig.traffic_management.unknown_packet_threshold; + if (threshold > 255) + threshold = 255; + + bool drop = entry->unknown_count > threshold; + if (drop || entry->unknown_count == threshold) { + TM_LOG_DEBUG("Unknown packets 0x%08x: count=%u threshold=%u -> %s", p->from, entry->unknown_count, threshold, + drop ? "DROP" : "at-limit"); + } + return drop; +#endif +} + +void TrafficManagementModule::logAction(const char *action, const meshtastic_MeshPacket *p, const char *reason) const +{ + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + const char *name = portName(p->decoded.portnum); + if (name) { + TM_LOG_INFO("%s %s from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, name, getFrom(p), p->to, p->hop_limit, + p->hop_start, reason); + } else { + TM_LOG_INFO("%s port=%d from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, p->decoded.portnum, getFrom(p), p->to, + p->hop_limit, p->hop_start, reason); + } + } else { + TM_LOG_INFO("%s encrypted from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, getFrom(p), p->to, p->hop_limit, + p->hop_start, reason); + } +} + +#endif diff --git a/src/modules/TrafficManagementModule.h b/src/modules/TrafficManagementModule.h new file mode 100644 index 000000000..fe3483a8e --- /dev/null +++ b/src/modules/TrafficManagementModule.h @@ -0,0 +1,434 @@ +#pragma once + +#include "MeshModule.h" +#include "concurrency/Lock.h" +#include "concurrency/OSThread.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include "mesh/generated/meshtastic/telemetry.pb.h" + +#if HAS_TRAFFIC_MANAGEMENT + +/** + * TrafficManagementModule - Packet inspection and traffic shaping for mesh networks. + * + * This module provides: + * - Position deduplication (drop redundant position broadcasts) + * - Per-node rate limiting (throttle chatty nodes) + * - Unknown packet filtering (drop undecoded packets from repeat offenders) + * - NodeInfo direct response (answer queries from cache to reduce mesh chatter) + * - Local-only telemetry/position (exhaust hop_limit for local broadcasts) + * - Router hop preservation (maintain hop_limit for router-to-router traffic) + * + * Memory Optimization: + * Uses a unified cache with cuckoo hashing for O(1) lookups and 56% memory reduction + * compared to separate per-feature caches. Timestamps are stored as 8-bit relative + * offsets from a rolling epoch to further reduce memory footprint. + */ +class TrafficManagementModule : public MeshModule, private concurrency::OSThread +{ + public: + TrafficManagementModule(); + ~TrafficManagementModule(); + + // Singleton — no copying or moving + TrafficManagementModule(const TrafficManagementModule &) = delete; + TrafficManagementModule &operator=(const TrafficManagementModule &) = delete; + + meshtastic_TrafficManagementStats getStats() const; + void resetStats(); + void recordRouterHopPreserved(); + + /** + * Check if this packet should have its hops exhausted. + * Called from perhapsRebroadcast() to force hop_limit = 0 regardless of + * router_preserve_hops or favorite node logic. + */ + bool shouldExhaustHops(const meshtastic_MeshPacket &mp) const + { + return exhaustRequested && exhaustRequestedFrom == getFrom(&mp) && exhaustRequestedId == mp.id; + } + + protected: + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } + void alterReceived(meshtastic_MeshPacket &mp) override; + int32_t runOnce() override; + // Protected so test shims can force epoch rollover behavior. + void resetEpoch(uint32_t nowMs); + + private: + // ========================================================================= + // Unified Cache Entry (10 bytes) - Same for ALL platforms + // ========================================================================= + // + // A single compact structure used across ESP32, NRF52, and all other platforms. + // Memory: 10 bytes × 2048 entries = 20KB + // + // Position Fingerprinting: + // Instead of storing full coordinates (8 bytes) or a computed hash, + // we store an 8-bit fingerprint derived deterministically from the + // truncated lat/lon. This extracts the lower 4 significant bits from + // each coordinate: fingerprint = (lat_low4 << 4) | lon_low4 + // + // Benefits over hash: + // - Adjacent grid cells have sequential fingerprints (no collision) + // - Two positions only collide if 16+ grid cells apart in BOTH dimensions + // - Deterministic: same input always produces same output + // + // Adaptive Timestamp Resolution: + // All timestamps use 8-bit values with adaptive resolution calculated + // from config at startup. Resolution = max(60, min(339, interval/2)). + // - Min 60 seconds ensures reasonable precision + // - Max 339 seconds allows ~24 hour range (255 * 339 = 86445 sec) + // - interval/2 ensures at least 2 ticks per configured interval + // + // Layout: + // [0-3] node - NodeNum (4 bytes) + // [4] pos_fingerprint - 4 bits lat + 4 bits lon (1 byte) + // [5] rate_count - Packets in current window (1 byte) + // [6] unknown_count - Unknown packets count (1 byte) + // [7] pos_time - Position timestamp (1 byte, adaptive resolution) + // [8] rate_time - Rate window start (1 byte, adaptive resolution) + // [9] unknown_time - Unknown tracking start (1 byte, adaptive resolution) + // + struct __attribute__((packed)) UnifiedCacheEntry { + NodeNum node; // 4 bytes - Node identifier (0 = empty slot) + uint8_t pos_fingerprint; // 1 byte - Lower 4 bits of lat + lon + uint8_t rate_count; // 1 byte - Packet count (saturates at 255) + uint8_t unknown_count; // 1 byte - Unknown packet count (saturates at 255) + uint8_t pos_time; // 1 byte - Position timestamp (adaptive resolution) + uint8_t rate_time; // 1 byte - Rate window start (adaptive resolution) + uint8_t unknown_time; // 1 byte - Unknown tracking start (adaptive resolution) + }; + static_assert(sizeof(UnifiedCacheEntry) == 10, "UnifiedCacheEntry should be 10 bytes"); + + // ========================================================================= + // Cuckoo Hash Table Implementation + // ========================================================================= + // + // Cuckoo hashing provides O(1) worst-case lookup time using two hash functions. + // Each key can be in one of two possible locations (h1 or h2). On collision, + // the existing entry is "kicked" to its alternate location. + // + // Benefits over linear scan: + // - O(1) lookup vs O(n) - critical at packet processing rates + // - O(1) insertion (amortized) with simple eviction on cycles + // - ~95% load factor achievable + // + // Cache size rounds to power-of-2 for fast modulo via bitmask. + // TRAFFIC_MANAGEMENT_CACHE_SIZE=2000 → cacheSize()=2048 + // + static constexpr uint16_t cacheSize(); + static constexpr uint16_t cacheMask(); + + // Hash functions for cuckoo hashing + inline uint16_t cuckooHash1(NodeNum node) const { return node & cacheMask(); } + inline uint16_t cuckooHash2(NodeNum node) const { return ((node * 2654435769u) >> (32 - cuckooHashBits())) & cacheMask(); } + static constexpr uint8_t cuckooHashBits(); + + // NodeInfo cache configuration (PSRAM path): + // - Payload lives in PSRAM + // - DRAM keeps packed 12-bit tags with 4-way bucketed cuckoo hashing + // (Fan et al., CoNEXT 2014). Tag value 0 is reserved as "empty". + static constexpr uint16_t kNodeInfoIndexMetadataBudgetBytes = 3072; // 3KB DRAM tag store + static constexpr uint8_t kNodeInfoTargetOccupancyPercent = 95; + static constexpr uint8_t kNodeInfoBucketSize = 4; + static constexpr uint8_t kNodeInfoTagBits = 12; + static constexpr uint16_t kNodeInfoTagMask = static_cast((1u << kNodeInfoTagBits) - 1u); + static constexpr uint16_t kNodeInfoIndexSlotsRaw = + static_cast((kNodeInfoIndexMetadataBudgetBytes * 8u) / kNodeInfoTagBits); + static constexpr uint16_t kNodeInfoIndexSlots = + static_cast(kNodeInfoIndexSlotsRaw - (kNodeInfoIndexSlotsRaw % kNodeInfoBucketSize)); + static constexpr uint16_t kNodeInfoTargetEntries = + static_cast((kNodeInfoIndexSlots * kNodeInfoTargetOccupancyPercent) / 100u); + static_assert((kNodeInfoIndexSlots % kNodeInfoBucketSize) == 0, "NodeInfo slot count must align to bucket size"); + static_assert(kNodeInfoTargetEntries < (1u << kNodeInfoTagBits), "NodeInfo tag bits must encode payload index"); + + static constexpr uint16_t nodeInfoTargetEntries(); + static constexpr uint16_t nodeInfoIndexMetadataBudgetBytes(); + static constexpr uint8_t nodeInfoTargetOccupancyPercent(); + static constexpr uint8_t nodeInfoBucketSize(); + static constexpr uint8_t nodeInfoTagBits(); + static constexpr uint16_t nodeInfoTagMask(); + static constexpr uint16_t nodeInfoIndexSlots(); + static constexpr uint16_t nodeInfoBucketCount(); + static constexpr uint16_t nodeInfoBucketMask(); + static constexpr uint8_t nodeInfoBucketHashBits(); + inline uint16_t nodeInfoHash1(NodeNum node) const { return node & nodeInfoBucketMask(); } + inline uint16_t nodeInfoHash2(NodeNum node) const + { + return ((node * 2246822519u) >> (32 - nodeInfoBucketHashBits())) & nodeInfoBucketMask(); + } + + // ========================================================================= + // Adaptive Timestamp Resolution + // ========================================================================= + // + // All timestamps use 8-bit values with adaptive resolution calculated from + // config at startup. This allows ~24 hour range while maintaining precision. + // + // Resolution formula: max(60, min(339, interval/2)) + // - 60 sec minimum ensures reasonable precision + // - 339 sec maximum allows 24 hour range (255 * 339 ≈ 86400 sec) + // - interval/2 ensures at least 2 ticks per configured interval + // + // Since config changes require reboot, resolution is calculated once. + // + uint32_t cacheEpochMs = 0; + uint16_t posTimeResolution = 60; // Seconds per tick for position + uint16_t rateTimeResolution = 60; // Seconds per tick for rate limiting + uint16_t unknownTimeResolution = 60; // Seconds per tick for unknown tracking + + // Calculate resolution from configured interval (called once at startup) + static uint16_t calcTimeResolution(uint32_t intervalSecs) + { + // Resolution = interval/2 to ensure at least 2 ticks per interval + // Clamped to [60, 339] for min precision and max 24h range + uint32_t res = (intervalSecs > 0) ? (intervalSecs / 2) : 60; + if (res < 60) + res = 60; + if (res > 339) + res = 339; + return static_cast(res); + } + + // Convert to/from 8-bit relative timestamps with given resolution + uint8_t toRelativeTime(uint32_t nowMs, uint16_t resolutionSecs) const + { + uint32_t ticks = (nowMs - cacheEpochMs) / (resolutionSecs * 1000UL); + return (ticks > UINT8_MAX) ? UINT8_MAX : static_cast(ticks); + } + uint32_t fromRelativeTime(uint8_t ticks, uint16_t resolutionSecs) const + { + return cacheEpochMs + (static_cast(ticks) * resolutionSecs * 1000UL); + } + + // Convenience wrappers for each timestamp type + uint8_t toRelativePosTime(uint32_t nowMs) const { return toRelativeTime(nowMs, posTimeResolution); } + uint32_t fromRelativePosTime(uint8_t t) const { return fromRelativeTime(t, posTimeResolution); } + + uint8_t toRelativeRateTime(uint32_t nowMs) const { return toRelativeTime(nowMs, rateTimeResolution); } + uint32_t fromRelativeRateTime(uint8_t t) const { return fromRelativeTime(t, rateTimeResolution); } + + uint8_t toRelativeUnknownTime(uint32_t nowMs) const { return toRelativeTime(nowMs, unknownTimeResolution); } + uint32_t fromRelativeUnknownTime(uint8_t t) const { return fromRelativeTime(t, unknownTimeResolution); } + + // Epoch reset when any timestamp approaches overflow + // With max resolution of 339 sec, 200 ticks = ~19 hours (safe margin for 24h max) + bool needsEpochReset(uint32_t nowMs) const + { + uint16_t maxRes = posTimeResolution; + if (rateTimeResolution > maxRes) + maxRes = rateTimeResolution; + if (unknownTimeResolution > maxRes) + maxRes = unknownTimeResolution; + return (nowMs - cacheEpochMs) > (200UL * maxRes * 1000UL); + } + // ========================================================================= + // Position Fingerprint + // ========================================================================= + // + // Computes 8-bit fingerprint from truncated lat/lon coordinates. + // Extracts lower 4 significant bits from each coordinate. + // + // fingerprint = (lat_low4 << 4) | lon_low4 + // + // Unlike a hash, adjacent grid cells have sequential fingerprints, + // so nearby positions never collide. Collisions only occur for + // positions 16+ grid cells apart in both dimensions. + // + // Guards: If precision < 4 bits, uses min(precision, 4) bits. + // + static uint8_t computePositionFingerprint(int32_t lat_truncated, int32_t lon_truncated, uint8_t precision); + + // ========================================================================= + // Cache Storage + // ========================================================================= + + mutable concurrency::Lock cacheLock; // Protects all cache access + UnifiedCacheEntry *cache = nullptr; // Cuckoo hash table (unified for all platforms) + bool cacheFromPsram = false; // Tracks allocator for correct deallocation + + struct NodeInfoPayloadEntry { + // Node identifier associated with this payload slot. + // 0 means the slot is currently unused. + NodeNum node; + + // Cached NODEINFO_APP payload body. This is separate from NodeDB and is only + // used by the PSRAM-backed direct-response path in this module. + meshtastic_User user; + + // Extra response metadata captured from the latest observed NODEINFO_APP + // packet for this node. shouldRespondToNodeInfo() uses this metadata when + // building spoofed replies for requesting clients. + + // Last local uptime tick (millis) when this entry was refreshed. + uint32_t lastObservedMs; + + // Last RTC/packet timestamp (seconds) observed for this NodeInfo frame. + // If unavailable in packet, remains 0. + uint32_t lastObservedRxTime; + + // Channel where we most recently heard this node's NodeInfo. + uint8_t sourceChannel; + + // Cached decoded bitfield metadata from the source packet. + // We preserve non-OK_TO_MQTT bits in direct replies when available. + bool hasDecodedBitfield; + uint8_t decodedBitfield; + }; + + NodeInfoPayloadEntry *nodeInfoPayload = nullptr; // NodeInfo payloads in PSRAM + bool nodeInfoPayloadFromPsram = false; // Tracks allocator for correct deallocation + uint8_t *nodeInfoIndex = nullptr; // Packed 12-bit NodeInfo tags in DRAM + uint16_t nodeInfoAllocHint = 0; + uint16_t nodeInfoEvictCursor = 0; + + meshtastic_TrafficManagementStats stats; + + // Flag set during alterReceived() when packet should be exhausted. + // Checked by perhapsRebroadcast() to force hop_limit = 0 only for the + // matching packet key (from + id). Reset at start of handleReceived(). + bool exhaustRequested = false; + NodeNum exhaustRequestedFrom = 0; + PacketId exhaustRequestedId = 0; + + // ========================================================================= + // Cache Operations + // ========================================================================= + + // Find or create entry for node using cuckoo hashing + // Returns nullptr if cache is full and eviction fails + UnifiedCacheEntry *findOrCreateEntry(NodeNum node, bool *isNew); + + // Find existing entry (no creation) + UnifiedCacheEntry *findEntry(NodeNum node); + + // NodeInfo cache operations (bucketed cuckoo index + PSRAM payloads) + const NodeInfoPayloadEntry *findNodeInfoEntry(NodeNum node) const; + NodeInfoPayloadEntry *findOrCreateNodeInfoEntry(NodeNum node, bool *usedEmptySlot); + uint16_t findNodeInfoPayloadIndex(NodeNum node) const; + bool removeNodeInfoIndexEntry(NodeNum node, uint16_t payloadIndex); + uint16_t allocateNodeInfoPayloadSlot(); + uint16_t evictNodeInfoPayloadSlot(); + bool tryInsertNodeInfoEntryInBucket(uint16_t bucket, uint16_t tag); + uint16_t encodeNodeInfoTag(uint16_t payloadIndex) const; + uint16_t decodeNodeInfoPayloadIndex(uint16_t tag) const; + uint16_t getNodeInfoTag(uint16_t slot) const; + void setNodeInfoTag(uint16_t slot, uint16_t tag); + uint16_t countNodeInfoEntriesLocked() const; + void cacheNodeInfoPacket(const meshtastic_MeshPacket &mp); + + // ========================================================================= + // Traffic Management Logic + // ========================================================================= + + bool shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs); + bool shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse); + bool isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const; + bool isRateLimited(NodeNum from, uint32_t nowMs); + bool shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs); + + void logAction(const char *action, const meshtastic_MeshPacket *p, const char *reason) const; + void incrementStat(uint32_t *field); +}; + +// ========================================================================= +// Compile-time Cache Size Calculations +// ========================================================================= +// +// Round TRAFFIC_MANAGEMENT_CACHE_SIZE up to next power of 2 for efficient +// cuckoo hash indexing (allows bitmask instead of modulo). +// +// These use C++11-compatible constexpr (single return statement). +// + +namespace detail +{ +// Helper: round up to next power of 2 using bit manipulation +constexpr uint16_t nextPow2(uint16_t n) +{ + return n == 0 ? 0 : (((n - 1) | ((n - 1) >> 1) | ((n - 1) >> 2) | ((n - 1) >> 4) | ((n - 1) >> 8)) + 1); +} + +// Helper: floor(log2(n)) for n >= 0, C++11-compatible constexpr. +constexpr uint8_t log2Floor(uint16_t n) +{ + return n <= 1 ? 0 : static_cast(1 + log2Floor(static_cast(n >> 1))); +} + +// Helper: ceil(log2(n)) for n >= 1, C++11-compatible constexpr. +constexpr uint8_t log2Ceil(uint16_t n) +{ + return n <= 1 ? 0 : static_cast(1 + log2Floor(static_cast(n - 1))); +} +} // namespace detail + +constexpr uint16_t TrafficManagementModule::cacheSize() +{ + return detail::nextPow2(TRAFFIC_MANAGEMENT_CACHE_SIZE); +} + +constexpr uint16_t TrafficManagementModule::cacheMask() +{ + return cacheSize() > 0 ? cacheSize() - 1 : 0; +} + +constexpr uint8_t TrafficManagementModule::cuckooHashBits() +{ + return detail::log2Floor(cacheSize()); +} + +constexpr uint16_t TrafficManagementModule::nodeInfoTargetEntries() +{ + return kNodeInfoTargetEntries; +} + +constexpr uint16_t TrafficManagementModule::nodeInfoIndexMetadataBudgetBytes() +{ + return kNodeInfoIndexMetadataBudgetBytes; +} + +constexpr uint8_t TrafficManagementModule::nodeInfoTargetOccupancyPercent() +{ + return kNodeInfoTargetOccupancyPercent; +} + +constexpr uint8_t TrafficManagementModule::nodeInfoBucketSize() +{ + return kNodeInfoBucketSize; +} + +constexpr uint8_t TrafficManagementModule::nodeInfoTagBits() +{ + return kNodeInfoTagBits; +} + +constexpr uint16_t TrafficManagementModule::nodeInfoTagMask() +{ + return kNodeInfoTagMask; +} + +constexpr uint16_t TrafficManagementModule::nodeInfoIndexSlots() +{ + return kNodeInfoIndexSlots; +} + +constexpr uint16_t TrafficManagementModule::nodeInfoBucketCount() +{ + return static_cast(nodeInfoIndexSlots() / nodeInfoBucketSize()); +} + +constexpr uint16_t TrafficManagementModule::nodeInfoBucketMask() +{ + return nodeInfoBucketCount() > 0 ? nodeInfoBucketCount() - 1 : 0; +} + +constexpr uint8_t TrafficManagementModule::nodeInfoBucketHashBits() +{ + return detail::log2Floor(nodeInfoBucketCount()); +} + +extern TrafficManagementModule *trafficManagementModule; + +#endif diff --git a/src/modules/esp32/AudioModule.cpp b/src/modules/esp32/AudioModule.cpp index 77cc94359..37e3e9184 100644 --- a/src/modules/esp32/AudioModule.cpp +++ b/src/modules/esp32/AudioModule.cpp @@ -100,7 +100,7 @@ AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_ // moduleConfig.audio.i2s_sck = 14; // moduleConfig.audio.ptt_pin = 39; - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) { LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); memcpy(tx_header.magic, c2_magic, sizeof(c2_magic)); @@ -143,7 +143,7 @@ void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int int32_t AudioModule::runOnce() { - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) { esp_err_t res; if (firstTime) { // Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC @@ -270,7 +270,7 @@ void AudioModule::sendPayload(NodeNum dest, bool wantReplies) ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp) { - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) { auto &p = mp.decoded; if (!isFromUs(&mp)) { memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h old mode 100755 new mode 100644 index f08ee00f9..d2205fd2a --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -4,12 +4,15 @@ #include "configuration.h" -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "../concurrency/OSThread.h" #ifdef HAS_BMA423 #include "BMA423Sensor.h" #endif +#ifdef HAS_BMI270 +#include "BMI270Sensor.h" +#endif #include "BMM150Sensor.h" #include "BMX160Sensor.h" #include "ICM20948Sensor.h" @@ -111,6 +114,11 @@ class AccelerometerThread : public concurrency::OSThread case ScanI2C::DeviceType::BMM150: sensor = new BMM150Sensor(device); break; +#ifdef HAS_BMI270 + case ScanI2C::DeviceType::BMI270: + sensor = new BMI270Sensor(device); + break; +#endif #ifdef HAS_QMA6100P case ScanI2C::DeviceType::QMA6100P: sensor = new QMA6100PSensor(device); diff --git a/src/motion/BMA423Sensor.cpp b/src/motion/BMA423Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/BMA423Sensor.h b/src/motion/BMA423Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/BMI270Sensor.cpp b/src/motion/BMI270Sensor.cpp new file mode 100644 index 000000000..bc547529d --- /dev/null +++ b/src/motion/BMI270Sensor.cpp @@ -0,0 +1,605 @@ +#include "BMI270Sensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMI270) + +#include + +// BMI270 registers used +#define BMI270_REG_ACC_X_LSB 0x0C +#define BMI270_REG_INTERNAL_STATUS 0x21 +#define BMI270_REG_ACC_CONF 0x40 +#define BMI270_REG_ACC_RANGE 0x41 +#define BMI270_REG_INIT_CTRL 0x59 +#define BMI270_REG_INIT_ADDR_0 0x5B +#define BMI270_REG_INIT_ADDR_1 0x5C +#define BMI270_REG_INIT_DATA 0x5E +#define BMI270_REG_PWR_CONF 0x7C +#define BMI270_REG_PWR_CTRL 0x7D +#define BMI270_REG_CMD 0x7E + +// Commands and configuration values +#define BMI270_CMD_SOFTRESET 0xB6 +#define BMI270_PWR_CONF_ADV_POWER_SAVE_DISABLED 0x00 +#define BMI270_PWR_CTRL_ACC_EN 0x04 +#define BMI270_ACC_ODR_50HZ 0x07 +#define BMI270_ACC_BWP_NORMAL 0x20 +#define BMI270_ACC_FILTER_PERF 0x80 +#define BMI270_ACC_RANGE_2G 0x00 +#define BMI270_INIT_OK 0x01 + +// BMI270 config file - 8192 bytes from official Bosch BMI270 API +static const uint8_t bmi270_config_file[] PROGMEM = { + 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x3d, 0xb1, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x91, 0x03, 0x80, 0x2e, 0xbc, 0xb0, 0x80, + 0x2e, 0xa3, 0x03, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x00, 0xb0, 0x50, 0x30, 0x21, 0x2e, 0x59, 0xf5, 0x10, 0x30, 0x21, 0x2e, + 0x6a, 0xf5, 0x80, 0x2e, 0x3b, 0x03, 0x00, 0x00, 0x00, 0x00, 0x08, 0x19, 0x01, 0x00, 0x22, 0x00, 0x75, 0x00, 0x00, 0x10, 0x00, + 0x10, 0xd1, 0x00, 0xb3, 0x43, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0xe0, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x19, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xe0, 0xaa, 0x38, 0x05, 0xe0, 0x90, 0x30, 0xfa, 0x00, + 0x96, 0x00, 0x4b, 0x09, 0x11, 0x00, 0x11, 0x00, 0x02, 0x00, 0x2d, 0x01, 0xd4, 0x7b, 0x3b, 0x01, 0xdb, 0x7a, 0x04, 0x00, 0x3f, + 0x7b, 0xcd, 0x6c, 0xc3, 0x04, 0x85, 0x09, 0xc3, 0x04, 0xec, 0xe6, 0x0c, 0x46, 0x01, 0x00, 0x27, 0x00, 0x19, 0x00, 0x96, 0x00, + 0xa0, 0x00, 0x01, 0x00, 0x0c, 0x00, 0xf0, 0x3c, 0x00, 0x01, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x32, + 0x00, 0x05, 0x00, 0xee, 0x06, 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x04, 0x00, 0xa8, 0x05, 0xee, 0x06, 0x00, 0x04, 0xbc, 0x02, + 0xb3, 0x00, 0x85, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xb4, 0x00, 0x01, 0x00, 0xb9, 0x00, 0x01, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x80, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0xde, 0x00, 0xeb, 0x00, 0xda, 0x00, 0x00, 0x0c, 0xff, 0x0f, 0x00, 0x04, 0xc0, + 0x00, 0x5b, 0xf5, 0xc9, 0x01, 0x1e, 0xf2, 0x80, 0x00, 0x3f, 0xff, 0x19, 0xf4, 0x58, 0xf5, 0x66, 0xf5, 0x64, 0xf5, 0xc0, 0xf1, + 0xf0, 0x00, 0xe0, 0x00, 0xcd, 0x01, 0xd3, 0x01, 0xdb, 0x01, 0xff, 0x7f, 0xff, 0x01, 0xe4, 0x00, 0x74, 0xf7, 0xf3, 0x00, 0xfa, + 0x00, 0xff, 0x3f, 0xca, 0x03, 0x6c, 0x38, 0x56, 0xfe, 0x44, 0xfd, 0xbc, 0x02, 0xf9, 0x06, 0x00, 0xfc, 0x12, 0x02, 0xae, 0x01, + 0x58, 0xfa, 0x9a, 0xfd, 0x77, 0x05, 0xbb, 0x02, 0x96, 0x01, 0x95, 0x01, 0x7f, 0x01, 0x82, 0x01, 0x89, 0x01, 0x87, 0x01, 0x88, + 0x01, 0x8a, 0x01, 0x8c, 0x01, 0x8f, 0x01, 0x8d, 0x01, 0x92, 0x01, 0x91, 0x01, 0xdd, 0x00, 0x9f, 0x01, 0x7e, 0x01, 0xdb, 0x00, + 0xb6, 0x01, 0x70, 0x69, 0x26, 0xd3, 0x9c, 0x07, 0x1f, 0x05, 0x9d, 0x00, 0x00, 0x08, 0xbc, 0x05, 0x37, 0xfa, 0xa2, 0x01, 0xaa, + 0x01, 0xa1, 0x01, 0xa8, 0x01, 0xa0, 0x01, 0xa8, 0x05, 0xb4, 0x01, 0xb4, 0x01, 0xce, 0x00, 0xd0, 0x00, 0xfc, 0x00, 0xc5, 0x01, + 0xff, 0xfb, 0xb1, 0x00, 0x00, 0x38, 0x00, 0x30, 0xfd, 0xf5, 0xfc, 0xf5, 0xcd, 0x01, 0xa0, 0x00, 0x5f, 0xff, 0x00, 0x40, 0xff, + 0x00, 0x00, 0x80, 0x6d, 0x0f, 0xeb, 0x00, 0x7f, 0xff, 0xc2, 0xf5, 0x68, 0xf7, 0xb3, 0xf1, 0x67, 0x0f, 0x5b, 0x0f, 0x61, 0x0f, + 0x80, 0x0f, 0x58, 0xf7, 0x5b, 0xf7, 0x83, 0x0f, 0x86, 0x00, 0x72, 0x0f, 0x85, 0x0f, 0xc6, 0xf1, 0x7f, 0x0f, 0x6c, 0xf7, 0x00, + 0xe0, 0x00, 0xff, 0xd1, 0xf5, 0x87, 0x0f, 0x8a, 0x0f, 0xff, 0x03, 0xf0, 0x3f, 0x8b, 0x00, 0x8e, 0x00, 0x90, 0x00, 0xb9, 0x00, + 0x2d, 0xf5, 0xca, 0xf5, 0xcb, 0x01, 0x20, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x50, 0x98, + 0x2e, 0xd7, 0x0e, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30, 0xf0, 0x7f, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x00, 0x2e, + 0x01, 0x80, 0x08, 0xa2, 0xfb, 0x2f, 0x98, 0x2e, 0xba, 0x03, 0x21, 0x2e, 0x19, 0x00, 0x01, 0x2e, 0xee, 0x00, 0x00, 0xb2, 0x07, + 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x03, 0x2f, 0x01, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x07, 0xcc, 0x01, 0x2e, 0xdd, 0x00, + 0x00, 0xb2, 0x27, 0x2f, 0x05, 0x2e, 0x8a, 0x00, 0x05, 0x52, 0x98, 0x2e, 0xc7, 0xc1, 0x03, 0x2e, 0xe9, 0x00, 0x40, 0xb2, 0xf0, + 0x7f, 0x08, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0xe9, 0x00, 0x98, 0x2e, 0xb4, 0xb1, + 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x10, 0x2f, 0x05, 0x50, 0x98, 0x2e, 0x4d, 0xc3, 0x05, 0x50, 0x98, 0x2e, 0x5a, 0xc7, 0x98, + 0x2e, 0xf9, 0xb4, 0x98, 0x2e, 0x54, 0xb2, 0x98, 0x2e, 0x67, 0xb6, 0x98, 0x2e, 0x17, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, + 0x01, 0x2e, 0xef, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x7a, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0xef, 0x00, 0x01, 0x2e, 0xd4, + 0x00, 0x04, 0xae, 0x0b, 0x2f, 0x01, 0x2e, 0xdd, 0x00, 0x00, 0xb2, 0x07, 0x2f, 0x05, 0x52, 0x98, 0x2e, 0x8e, 0x0e, 0x00, 0xb2, + 0x02, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x01, 0x2e, 0x7d, 0x00, 0x00, 0x90, 0x90, 0x2e, 0xf1, 0x02, 0x01, 0x2e, 0xd7, + 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x2f, 0x0e, 0x00, 0x30, 0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, 0x7b, 0x00, 0x00, 0xb2, + 0x12, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x00, 0x90, 0x02, 0x2f, 0x98, 0x2e, 0x1f, 0x0e, 0x09, 0x2d, 0x98, 0x2e, 0x81, 0x0d, 0x01, + 0x2e, 0xd4, 0x00, 0x04, 0x90, 0x02, 0x2f, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30, 0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, + 0x7c, 0x00, 0x00, 0xb2, 0x90, 0x2e, 0x09, 0x03, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x31, 0x01, 0x08, 0x00, 0xb2, 0x04, 0x2f, 0x98, + 0x2e, 0x47, 0xcb, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x81, 0x30, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x08, 0x00, 0xb2, 0x61, 0x2f, + 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x98, 0xbc, 0x98, 0xb8, 0x05, 0xb2, 0x0f, 0x58, 0x23, 0x2f, 0x07, 0x90, 0x09, + 0x54, 0x00, 0x30, 0x37, 0x2f, 0x15, 0x41, 0x04, 0x41, 0xdc, 0xbe, 0x44, 0xbe, 0xdc, 0xba, 0x2c, 0x01, 0x61, 0x00, 0x0f, 0x56, + 0x4a, 0x0f, 0x0c, 0x2f, 0xd1, 0x42, 0x94, 0xb8, 0xc1, 0x42, 0x11, 0x30, 0x05, 0x2e, 0x6a, 0xf7, 0x2c, 0xbd, 0x2f, 0xb9, 0x80, + 0xb2, 0x08, 0x22, 0x98, 0x2e, 0xc3, 0xb7, 0x21, 0x2d, 0x61, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x98, 0x2e, 0xc3, 0xb7, 0x00, 0x30, + 0x21, 0x2e, 0x5a, 0xf5, 0x18, 0x2d, 0xe1, 0x7f, 0x50, 0x30, 0x98, 0x2e, 0xfa, 0x03, 0x0f, 0x52, 0x07, 0x50, 0x50, 0x42, 0x70, + 0x30, 0x0d, 0x54, 0x42, 0x42, 0x7e, 0x82, 0xe2, 0x6f, 0x80, 0xb2, 0x42, 0x42, 0x05, 0x2f, 0x21, 0x2e, 0xd4, 0x00, 0x10, 0x30, + 0x98, 0x2e, 0xc3, 0xb7, 0x03, 0x2d, 0x60, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x06, 0x90, 0x18, 0x2f, 0x01, + 0x2e, 0x76, 0x00, 0x0b, 0x54, 0x07, 0x52, 0xe0, 0x7f, 0x98, 0x2e, 0x7a, 0xc1, 0xe1, 0x6f, 0x08, 0x1a, 0x40, 0x30, 0x08, 0x2f, + 0x21, 0x2e, 0xd4, 0x00, 0x20, 0x30, 0x98, 0x2e, 0xaf, 0xb7, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x05, 0x2d, 0x98, 0x2e, 0x38, + 0x0e, 0x00, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x00, 0x30, 0x21, 0x2e, 0x7c, 0x00, 0x18, 0x2d, 0x01, 0x2e, 0xd4, 0x00, 0x03, 0xaa, + 0x01, 0x2f, 0x98, 0x2e, 0x45, 0x0e, 0x01, 0x2e, 0xd4, 0x00, 0x3f, 0x80, 0x03, 0xa2, 0x01, 0x2f, 0x00, 0x2e, 0x02, 0x2d, 0x98, + 0x2e, 0x5b, 0x0e, 0x30, 0x30, 0x98, 0x2e, 0xce, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, + 0x01, 0x2e, 0x77, 0x00, 0x00, 0xb2, 0x24, 0x2f, 0x98, 0x2e, 0xf5, 0xcb, 0x03, 0x2e, 0xd5, 0x00, 0x11, 0x54, 0x01, 0x0a, 0xbc, + 0x84, 0x83, 0x86, 0x21, 0x2e, 0xc9, 0x01, 0xe0, 0x40, 0x13, 0x52, 0xc4, 0x40, 0x82, 0x40, 0xa8, 0xb9, 0x52, 0x42, 0x43, 0xbe, + 0x53, 0x42, 0x04, 0x0a, 0x50, 0x42, 0xe1, 0x7f, 0xf0, 0x31, 0x41, 0x40, 0xf2, 0x6f, 0x25, 0xbd, 0x08, 0x08, 0x02, 0x0a, 0xd0, + 0x7f, 0x98, 0x2e, 0xa8, 0xcf, 0x06, 0xbc, 0xd1, 0x6f, 0xe2, 0x6f, 0x08, 0x0a, 0x80, 0x42, 0x98, 0x2e, 0x58, 0xb7, 0x00, 0x30, + 0x21, 0x2e, 0xee, 0x00, 0x21, 0x2e, 0x77, 0x00, 0x21, 0x2e, 0xdd, 0x00, 0x80, 0x2e, 0xf4, 0x01, 0x1a, 0x24, 0x22, 0x00, 0x80, + 0x2e, 0xec, 0x01, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50, 0xfb, 0x6f, 0x01, 0x30, 0x71, 0x54, 0x11, 0x42, + 0x42, 0x0e, 0xfc, 0x2f, 0xc0, 0x2e, 0x01, 0x42, 0xf0, 0x5f, 0x80, 0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0x01, 0x34, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x06, 0x32, 0x0f, 0x2e, 0x61, 0xf5, 0xfe, 0x09, 0xc0, 0xb3, 0x04, 0x2f, 0x17, 0x30, 0x2f, 0x2e, + 0xef, 0x00, 0x2d, 0x2e, 0x61, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e, 0x20, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x46, + 0x30, 0x0f, 0x2e, 0xa4, 0xf1, 0xbe, 0x09, 0x80, 0xb3, 0x06, 0x2f, 0x0d, 0x2e, 0xd4, 0x00, 0x84, 0xaf, 0x02, 0x2f, 0x16, 0x30, + 0x2d, 0x2e, 0x7b, 0x00, 0x86, 0x30, 0x2d, 0x2e, 0x60, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e, 0x01, 0x2e, 0x77, + 0xf7, 0x09, 0xbc, 0x0f, 0xb8, 0x00, 0xb2, 0x10, 0x50, 0xfb, 0x7f, 0x10, 0x30, 0x0b, 0x2f, 0x03, 0x2e, 0x8a, 0x00, 0x96, 0xbc, + 0x9f, 0xb8, 0x40, 0xb2, 0x05, 0x2f, 0x03, 0x2e, 0x68, 0xf7, 0x9e, 0xbc, 0x9f, 0xb8, 0x40, 0xb2, 0x07, 0x2f, 0x03, 0x2e, 0x7e, + 0x00, 0x41, 0x90, 0x01, 0x2f, 0x98, 0x2e, 0xdc, 0x03, 0x03, 0x2c, 0x00, 0x30, 0x21, 0x2e, 0x7e, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, + 0xb8, 0x2e, 0x20, 0x50, 0xe0, 0x7f, 0xfb, 0x7f, 0x00, 0x2e, 0x27, 0x50, 0x98, 0x2e, 0x3b, 0xc8, 0x29, 0x50, 0x98, 0x2e, 0xa7, + 0xc8, 0x01, 0x50, 0x98, 0x2e, 0x55, 0xcc, 0xe1, 0x6f, 0x2b, 0x50, 0x98, 0x2e, 0xe0, 0xc9, 0xfb, 0x6f, 0x00, 0x30, 0xe0, 0x5f, + 0x21, 0x2e, 0x7e, 0x00, 0xb8, 0x2e, 0x73, 0x50, 0x01, 0x30, 0x57, 0x54, 0x11, 0x42, 0x42, 0x0e, 0xfc, 0x2f, 0xb8, 0x2e, 0x21, + 0x2e, 0x59, 0xf5, 0x10, 0x30, 0xc0, 0x2e, 0x21, 0x2e, 0x4a, 0xf1, 0x90, 0x50, 0xf7, 0x7f, 0xe6, 0x7f, 0xd5, 0x7f, 0xc4, 0x7f, + 0xb3, 0x7f, 0xa1, 0x7f, 0x90, 0x7f, 0x82, 0x7f, 0x7b, 0x7f, 0x98, 0x2e, 0x35, 0xb7, 0x00, 0xb2, 0x90, 0x2e, 0x97, 0xb0, 0x03, + 0x2e, 0x8f, 0x00, 0x07, 0x2e, 0x91, 0x00, 0x05, 0x2e, 0xb1, 0x00, 0x3f, 0xba, 0x9f, 0xb8, 0x01, 0x2e, 0xb1, 0x00, 0xa3, 0xbd, + 0x4c, 0x0a, 0x05, 0x2e, 0xb1, 0x00, 0x04, 0xbe, 0xbf, 0xb9, 0xcb, 0x0a, 0x4f, 0xba, 0x22, 0xbd, 0x01, 0x2e, 0xb3, 0x00, 0xdc, + 0x0a, 0x2f, 0xb9, 0x03, 0x2e, 0xb8, 0x00, 0x0a, 0xbe, 0x9a, 0x0a, 0xcf, 0xb9, 0x9b, 0xbc, 0x01, 0x2e, 0x97, 0x00, 0x9f, 0xb8, + 0x93, 0x0a, 0x0f, 0xbc, 0x91, 0x0a, 0x0f, 0xb8, 0x90, 0x0a, 0x25, 0x2e, 0x18, 0x00, 0x05, 0x2e, 0xc1, 0xf5, 0x2e, 0xbd, 0x2e, + 0xb9, 0x01, 0x2e, 0x19, 0x00, 0x31, 0x30, 0x8a, 0x04, 0x00, 0x90, 0x07, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0xa2, 0x03, 0x2f, + 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x0c, 0x2f, 0x19, 0x50, 0x05, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x05, 0x2e, 0x78, 0x00, 0x80, + 0x90, 0x10, 0x30, 0x01, 0x2f, 0x21, 0x2e, 0x78, 0x00, 0x25, 0x2e, 0xdd, 0x00, 0x98, 0x2e, 0x3e, 0xb7, 0x00, 0xb2, 0x02, 0x30, + 0x01, 0x30, 0x04, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x00, 0x2f, 0x21, 0x30, 0x01, 0x2e, 0xea, 0x00, 0x08, 0x1a, 0x0e, + 0x2f, 0x23, 0x2e, 0xea, 0x00, 0x33, 0x30, 0x1b, 0x50, 0x0b, 0x09, 0x01, 0x40, 0x17, 0x56, 0x46, 0xbe, 0x4b, 0x08, 0x4c, 0x0a, + 0x01, 0x42, 0x0a, 0x80, 0x15, 0x52, 0x01, 0x42, 0x00, 0x2e, 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x1f, 0x2f, 0x03, 0x2e, 0xc0, + 0xf5, 0xf0, 0x30, 0x48, 0x08, 0x47, 0xaa, 0x74, 0x30, 0x07, 0x2e, 0x7a, 0x00, 0x61, 0x22, 0x4b, 0x1a, 0x05, 0x2f, 0x07, 0x2e, + 0x66, 0xf5, 0xbf, 0xbd, 0xbf, 0xb9, 0xc0, 0x90, 0x0b, 0x2f, 0x1d, 0x56, 0x2b, 0x30, 0xd2, 0x42, 0xdb, 0x42, 0x01, 0x04, 0xc2, + 0x42, 0x04, 0xbd, 0xfe, 0x80, 0x81, 0x84, 0x23, 0x2e, 0x7a, 0x00, 0x02, 0x42, 0x02, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x05, 0x2e, + 0xd6, 0x00, 0x81, 0x84, 0x25, 0x2e, 0xd6, 0x00, 0x02, 0x31, 0x25, 0x2e, 0x60, 0xf5, 0x05, 0x2e, 0x8a, 0x00, 0x0b, 0x50, 0x90, + 0x08, 0x80, 0xb2, 0x0b, 0x2f, 0x05, 0x2e, 0xca, 0xf5, 0xf0, 0x3e, 0x90, 0x08, 0x25, 0x2e, 0xca, 0xf5, 0x05, 0x2e, 0x59, 0xf5, + 0xe0, 0x3f, 0x90, 0x08, 0x25, 0x2e, 0x59, 0xf5, 0x90, 0x6f, 0xa1, 0x6f, 0xb3, 0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0xe6, 0x6f, 0xf7, + 0x6f, 0x7b, 0x6f, 0x82, 0x6f, 0x70, 0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0x90, 0x7f, 0xe5, 0x7f, 0xd4, 0x7f, 0xc3, 0x7f, 0xb1, 0x7f, + 0xa2, 0x7f, 0x87, 0x7f, 0xf6, 0x7f, 0x7b, 0x7f, 0x00, 0x2e, 0x01, 0x2e, 0x60, 0xf5, 0x60, 0x7f, 0x98, 0x2e, 0x35, 0xb7, 0x02, + 0x30, 0x63, 0x6f, 0x15, 0x52, 0x50, 0x7f, 0x62, 0x7f, 0x5a, 0x2c, 0x02, 0x32, 0x1a, 0x09, 0x00, 0xb3, 0x14, 0x2f, 0x00, 0xb2, + 0x03, 0x2f, 0x09, 0x2e, 0x18, 0x00, 0x00, 0x91, 0x0c, 0x2f, 0x43, 0x7f, 0x98, 0x2e, 0x97, 0xb7, 0x1f, 0x50, 0x02, 0x8a, 0x02, + 0x32, 0x04, 0x30, 0x25, 0x2e, 0x64, 0xf5, 0x15, 0x52, 0x50, 0x6f, 0x43, 0x6f, 0x44, 0x43, 0x25, 0x2e, 0x60, 0xf5, 0xd9, 0x08, + 0xc0, 0xb2, 0x36, 0x2f, 0x98, 0x2e, 0x3e, 0xb7, 0x00, 0xb2, 0x06, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x02, 0x2f, 0x50, + 0x6f, 0x00, 0x90, 0x0a, 0x2f, 0x01, 0x2e, 0x79, 0x00, 0x00, 0x90, 0x19, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x79, 0x00, 0x00, 0x30, + 0x98, 0x2e, 0xdc, 0x03, 0x13, 0x2d, 0x01, 0x2e, 0xc3, 0xf5, 0x0c, 0xbc, 0x0f, 0xb8, 0x12, 0x30, 0x10, 0x04, 0x03, 0xb0, 0x26, + 0x25, 0x21, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x10, 0x30, 0x21, 0x2e, 0xee, 0x00, 0x02, 0x30, 0x60, 0x7f, 0x25, 0x2e, + 0x79, 0x00, 0x60, 0x6f, 0x00, 0x90, 0x05, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0xea, 0x00, 0x15, 0x50, 0x21, 0x2e, 0x64, 0xf5, 0x15, + 0x52, 0x23, 0x2e, 0x60, 0xf5, 0x02, 0x32, 0x50, 0x6f, 0x00, 0x90, 0x02, 0x2f, 0x03, 0x30, 0x27, 0x2e, 0x78, 0x00, 0x07, 0x2e, + 0x60, 0xf5, 0x1a, 0x09, 0x00, 0x91, 0xa3, 0x2f, 0x19, 0x09, 0x00, 0x91, 0xa0, 0x2f, 0x90, 0x6f, 0xa2, 0x6f, 0xb1, 0x6f, 0xc3, + 0x6f, 0xd4, 0x6f, 0xe5, 0x6f, 0x7b, 0x6f, 0xf6, 0x6f, 0x87, 0x6f, 0x40, 0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, + 0x26, 0x30, 0x0f, 0x2e, 0x61, 0xf5, 0x2f, 0x2e, 0x7c, 0x00, 0x0f, 0x2e, 0x7c, 0x00, 0xbe, 0x09, 0xa2, 0x7f, 0x80, 0x7f, 0x80, + 0xb3, 0xd5, 0x7f, 0xc4, 0x7f, 0xb3, 0x7f, 0x91, 0x7f, 0x7b, 0x7f, 0x0b, 0x2f, 0x23, 0x50, 0x1a, 0x25, 0x12, 0x40, 0x42, 0x7f, + 0x74, 0x82, 0x12, 0x40, 0x52, 0x7f, 0x00, 0x2e, 0x00, 0x40, 0x60, 0x7f, 0x98, 0x2e, 0x6a, 0xd6, 0x81, 0x30, 0x01, 0x2e, 0x7c, + 0x00, 0x01, 0x08, 0x00, 0xb2, 0x42, 0x2f, 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0x89, 0x00, 0x97, 0xbc, 0x06, 0xbc, 0x9f, 0xb8, + 0x0f, 0xb8, 0x00, 0x90, 0x23, 0x2e, 0xd8, 0x00, 0x10, 0x30, 0x01, 0x30, 0x2a, 0x2f, 0x03, 0x2e, 0xd4, 0x00, 0x44, 0xb2, 0x05, + 0x2f, 0x47, 0xb2, 0x00, 0x30, 0x2d, 0x2f, 0x21, 0x2e, 0x7c, 0x00, 0x2b, 0x2d, 0x03, 0x2e, 0xfd, 0xf5, 0x9e, 0xbc, 0x9f, 0xb8, + 0x40, 0x90, 0x14, 0x2f, 0x03, 0x2e, 0xfc, 0xf5, 0x99, 0xbc, 0x9f, 0xb8, 0x40, 0x90, 0x0e, 0x2f, 0x03, 0x2e, 0x49, 0xf1, 0x25, + 0x54, 0x4a, 0x08, 0x40, 0x90, 0x08, 0x2f, 0x98, 0x2e, 0x35, 0xb7, 0x00, 0xb2, 0x10, 0x30, 0x03, 0x2f, 0x50, 0x30, 0x21, 0x2e, + 0xd4, 0x00, 0x10, 0x2d, 0x98, 0x2e, 0xaf, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7c, 0x00, 0x0a, 0x2d, 0x05, 0x2e, 0x69, 0xf7, 0x2d, + 0xbd, 0x2f, 0xb9, 0x80, 0xb2, 0x01, 0x2f, 0x21, 0x2e, 0x7d, 0x00, 0x23, 0x2e, 0x7c, 0x00, 0xe0, 0x31, 0x21, 0x2e, 0x61, 0xf5, + 0xf6, 0x6f, 0xe7, 0x6f, 0x80, 0x6f, 0xa2, 0x6f, 0xb3, 0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0x7b, 0x6f, 0x91, 0x6f, 0x40, 0x5f, 0xc8, + 0x2e, 0x60, 0x51, 0x0a, 0x25, 0x36, 0x88, 0xf4, 0x7f, 0xeb, 0x7f, 0x00, 0x32, 0x31, 0x52, 0x32, 0x30, 0x13, 0x30, 0x98, 0x2e, + 0x15, 0xcb, 0x0a, 0x25, 0x33, 0x84, 0xd2, 0x7f, 0x43, 0x30, 0x05, 0x50, 0x2d, 0x52, 0x98, 0x2e, 0x95, 0xc1, 0xd2, 0x6f, 0x27, + 0x52, 0x98, 0x2e, 0xd7, 0xc7, 0x2a, 0x25, 0xb0, 0x86, 0xc0, 0x7f, 0xd3, 0x7f, 0xaf, 0x84, 0x29, 0x50, 0xf1, 0x6f, 0x98, 0x2e, + 0x4d, 0xc8, 0x2a, 0x25, 0xae, 0x8a, 0xaa, 0x88, 0xf2, 0x6e, 0x2b, 0x50, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x7f, 0x98, 0x2e, 0xb6, + 0xc8, 0xe0, 0x6e, 0x00, 0xb2, 0x32, 0x2f, 0x33, 0x54, 0x83, 0x86, 0xf1, 0x6f, 0xc3, 0x7f, 0x04, 0x30, 0x30, 0x30, 0xf4, 0x7f, + 0xd0, 0x7f, 0xb2, 0x7f, 0xe3, 0x30, 0xc5, 0x6f, 0x56, 0x40, 0x45, 0x41, 0x28, 0x08, 0x03, 0x14, 0x0e, 0xb4, 0x08, 0xbc, 0x82, + 0x40, 0x10, 0x0a, 0x2f, 0x54, 0x26, 0x05, 0x91, 0x7f, 0x44, 0x28, 0xa3, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0x08, 0xb9, 0x33, 0x30, + 0x53, 0x09, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x6f, 0x83, 0x17, 0x47, 0x40, 0x6c, 0x15, 0xb2, 0x6f, 0xbe, 0x09, 0x75, 0x0b, 0x90, + 0x42, 0x45, 0x42, 0x51, 0x0e, 0x32, 0xbc, 0x02, 0x89, 0xa1, 0x6f, 0x7e, 0x86, 0xf4, 0x7f, 0xd0, 0x7f, 0xb2, 0x7f, 0x04, 0x30, + 0x91, 0x6f, 0xd6, 0x2f, 0xeb, 0x6f, 0xa0, 0x5e, 0xb8, 0x2e, 0x03, 0x2e, 0x97, 0x00, 0x1b, 0xbc, 0x60, 0x50, 0x9f, 0xbc, 0x0c, + 0xb8, 0xf0, 0x7f, 0x40, 0xb2, 0xeb, 0x7f, 0x2b, 0x2f, 0x03, 0x2e, 0x7f, 0x00, 0x41, 0x40, 0x01, 0x2e, 0xc8, 0x00, 0x01, 0x1a, + 0x11, 0x2f, 0x37, 0x58, 0x23, 0x2e, 0xc8, 0x00, 0x10, 0x41, 0xa0, 0x7f, 0x38, 0x81, 0x01, 0x41, 0xd0, 0x7f, 0xb1, 0x7f, 0x98, + 0x2e, 0x64, 0xcf, 0xd0, 0x6f, 0x07, 0x80, 0xa1, 0x6f, 0x11, 0x42, 0x00, 0x2e, 0xb1, 0x6f, 0x01, 0x42, 0x11, 0x30, 0x01, 0x2e, + 0xfc, 0x00, 0x00, 0xa8, 0x03, 0x30, 0xcb, 0x22, 0x4a, 0x25, 0x01, 0x2e, 0x7f, 0x00, 0x3c, 0x89, 0x35, 0x52, 0x05, 0x54, 0x98, + 0x2e, 0xc4, 0xce, 0xc1, 0x6f, 0xf0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x04, 0x2d, 0x01, 0x30, 0xf0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, + 0xeb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e, 0x03, 0x2e, 0xb3, 0x00, 0x02, 0x32, 0xf0, 0x30, 0x03, 0x31, 0x30, 0x50, 0x8a, 0x08, 0x08, + 0x08, 0xcb, 0x08, 0xe0, 0x7f, 0x80, 0xb2, 0xf3, 0x7f, 0xdb, 0x7f, 0x25, 0x2f, 0x03, 0x2e, 0xca, 0x00, 0x41, 0x90, 0x04, 0x2f, + 0x01, 0x30, 0x23, 0x2e, 0xca, 0x00, 0x98, 0x2e, 0x3f, 0x03, 0xc0, 0xb2, 0x05, 0x2f, 0x03, 0x2e, 0xda, 0x00, 0x00, 0x30, 0x41, + 0x04, 0x23, 0x2e, 0xda, 0x00, 0x98, 0x2e, 0x92, 0xb2, 0x10, 0x25, 0xf0, 0x6f, 0x00, 0xb2, 0x05, 0x2f, 0x01, 0x2e, 0xda, 0x00, + 0x02, 0x30, 0x10, 0x04, 0x21, 0x2e, 0xda, 0x00, 0x40, 0xb2, 0x01, 0x2f, 0x23, 0x2e, 0xc8, 0x01, 0xdb, 0x6f, 0xe0, 0x6f, 0xd0, + 0x5f, 0x80, 0x2e, 0x95, 0xcf, 0x01, 0x30, 0xe0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x11, 0x30, 0x23, 0x2e, 0xca, 0x00, 0xdb, 0x6f, + 0xd0, 0x5f, 0xb8, 0x2e, 0xd0, 0x50, 0x0a, 0x25, 0x33, 0x84, 0x55, 0x50, 0xd2, 0x7f, 0xe2, 0x7f, 0x03, 0x8c, 0xc0, 0x7f, 0xbb, + 0x7f, 0x00, 0x30, 0x05, 0x5a, 0x39, 0x54, 0x51, 0x41, 0xa5, 0x7f, 0x96, 0x7f, 0x80, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0x05, 0x30, + 0xf5, 0x7f, 0x20, 0x25, 0x91, 0x6f, 0x3b, 0x58, 0x3d, 0x5c, 0x3b, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xc1, 0x6f, 0xd5, 0x6f, 0x52, + 0x40, 0x50, 0x43, 0xc1, 0x7f, 0xd5, 0x7f, 0x10, 0x25, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x86, 0x6f, + 0x30, 0x28, 0x92, 0x6f, 0x82, 0x8c, 0xa5, 0x6f, 0x6f, 0x52, 0x69, 0x0e, 0x39, 0x54, 0xdb, 0x2f, 0x19, 0xa0, 0x15, 0x30, 0x03, + 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x81, 0x01, 0x0a, 0x2d, 0x01, 0x2e, 0x81, 0x01, 0x05, 0x28, 0x42, 0x36, 0x21, 0x2e, 0x81, 0x01, + 0x02, 0x0e, 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50, 0x12, 0x30, 0x01, 0x40, 0x98, 0x2e, 0xfe, 0xc9, 0x51, 0x6f, 0x0b, + 0x5c, 0x8e, 0x0e, 0x3b, 0x6f, 0x57, 0x58, 0x02, 0x30, 0x21, 0x2e, 0x95, 0x01, 0x45, 0x6f, 0x2a, 0x8d, 0xd2, 0x7f, 0xcb, 0x7f, + 0x13, 0x2f, 0x02, 0x30, 0x3f, 0x50, 0xd2, 0x7f, 0xa8, 0x0e, 0x0e, 0x2f, 0xc0, 0x6f, 0x53, 0x54, 0x02, 0x00, 0x51, 0x54, 0x42, + 0x0e, 0x10, 0x30, 0x59, 0x52, 0x02, 0x30, 0x01, 0x2f, 0x00, 0x2e, 0x03, 0x2d, 0x50, 0x42, 0x42, 0x42, 0x12, 0x30, 0xd2, 0x7f, + 0x80, 0xb2, 0x03, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x80, 0x01, 0x12, 0x2d, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x80, 0x05, 0x2e, 0x80, + 0x01, 0x11, 0x30, 0x91, 0x28, 0x00, 0x40, 0x25, 0x2e, 0x80, 0x01, 0x10, 0x0e, 0x05, 0x2f, 0x01, 0x2e, 0x7f, 0x01, 0x01, 0x90, + 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x00, 0x2e, 0xa0, 0x41, 0x01, 0x90, 0xa6, 0x7f, 0x90, 0x2e, 0xe3, 0xb4, 0x01, 0x2e, 0x95, + 0x01, 0x00, 0xa8, 0x90, 0x2e, 0xe3, 0xb4, 0x5b, 0x54, 0x95, 0x80, 0x82, 0x40, 0x80, 0xb2, 0x02, 0x40, 0x2d, 0x8c, 0x3f, 0x52, + 0x96, 0x7f, 0x90, 0x2e, 0xc2, 0xb3, 0x29, 0x0e, 0x76, 0x2f, 0x01, 0x2e, 0xc9, 0x00, 0x00, 0x40, 0x81, 0x28, 0x45, 0x52, 0xb3, + 0x30, 0x98, 0x2e, 0x0f, 0xca, 0x5d, 0x54, 0x80, 0x7f, 0x00, 0x2e, 0xa1, 0x40, 0x72, 0x7f, 0x82, 0x80, 0x82, 0x40, 0x60, 0x7f, + 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x62, 0x6f, 0x05, 0x30, 0x87, 0x40, 0xc0, 0x91, 0x04, 0x30, 0x05, + 0x2f, 0x05, 0x2e, 0x83, 0x01, 0x80, 0xb2, 0x14, 0x30, 0x00, 0x2f, 0x04, 0x30, 0x05, 0x2e, 0xc9, 0x00, 0x73, 0x6f, 0x81, 0x40, + 0xe2, 0x40, 0x69, 0x04, 0x11, 0x0f, 0xe1, 0x40, 0x16, 0x30, 0xfe, 0x29, 0xcb, 0x40, 0x02, 0x2f, 0x83, 0x6f, 0x83, 0x0f, 0x22, + 0x2f, 0x47, 0x56, 0x13, 0x0f, 0x12, 0x30, 0x77, 0x2f, 0x49, 0x54, 0x42, 0x0e, 0x12, 0x30, 0x73, 0x2f, 0x00, 0x91, 0x0a, 0x2f, + 0x01, 0x2e, 0x8b, 0x01, 0x19, 0xa8, 0x02, 0x30, 0x6c, 0x2f, 0x63, 0x50, 0x00, 0x2e, 0x17, 0x42, 0x05, 0x42, 0x68, 0x2c, 0x12, + 0x30, 0x0b, 0x25, 0x08, 0x0f, 0x50, 0x30, 0x02, 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x03, 0x2d, 0x40, 0x30, 0x21, 0x2e, 0x83, 0x01, + 0x2b, 0x2e, 0x85, 0x01, 0x5a, 0x2c, 0x12, 0x30, 0x00, 0x91, 0x2b, 0x25, 0x04, 0x2f, 0x63, 0x50, 0x02, 0x30, 0x17, 0x42, 0x17, + 0x2c, 0x02, 0x42, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x05, 0x2e, 0xc9, 0x00, 0x81, 0x84, 0x5b, 0x30, + 0x82, 0x40, 0x37, 0x2e, 0x83, 0x01, 0x02, 0x0e, 0x07, 0x2f, 0x5f, 0x52, 0x40, 0x30, 0x62, 0x40, 0x41, 0x40, 0x91, 0x0e, 0x01, + 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x05, 0x30, 0x2b, 0x2e, 0x85, 0x01, 0x12, 0x30, 0x36, 0x2c, 0x16, 0x30, 0x15, 0x25, 0x81, 0x7f, + 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x19, 0xa2, 0x16, 0x30, 0x15, 0x2f, 0x05, 0x2e, 0x97, 0x01, 0x80, + 0x6f, 0x82, 0x0e, 0x05, 0x2f, 0x01, 0x2e, 0x86, 0x01, 0x06, 0x28, 0x21, 0x2e, 0x86, 0x01, 0x0b, 0x2d, 0x03, 0x2e, 0x87, 0x01, + 0x5f, 0x54, 0x4e, 0x28, 0x91, 0x42, 0x00, 0x2e, 0x82, 0x40, 0x90, 0x0e, 0x01, 0x2f, 0x21, 0x2e, 0x88, 0x01, 0x02, 0x30, 0x13, + 0x2c, 0x05, 0x30, 0xc0, 0x6f, 0x08, 0x1c, 0xa8, 0x0f, 0x16, 0x30, 0x05, 0x30, 0x5b, 0x50, 0x09, 0x2f, 0x02, 0x80, 0x2d, 0x2e, + 0x82, 0x01, 0x05, 0x42, 0x05, 0x80, 0x00, 0x2e, 0x02, 0x42, 0x3e, 0x80, 0x00, 0x2e, 0x06, 0x42, 0x02, 0x30, 0x90, 0x6f, 0x3e, + 0x88, 0x01, 0x40, 0x04, 0x41, 0x4c, 0x28, 0x01, 0x42, 0x07, 0x80, 0x10, 0x25, 0x24, 0x40, 0x00, 0x40, 0x00, 0xa8, 0xf5, 0x22, + 0x23, 0x29, 0x44, 0x42, 0x7a, 0x82, 0x7e, 0x88, 0x43, 0x40, 0x04, 0x41, 0x00, 0xab, 0xf5, 0x23, 0xdf, 0x28, 0x43, 0x42, 0xd9, + 0xa0, 0x14, 0x2f, 0x00, 0x90, 0x02, 0x2f, 0xd2, 0x6f, 0x81, 0xb2, 0x05, 0x2f, 0x63, 0x54, 0x06, 0x28, 0x90, 0x42, 0x85, 0x42, + 0x09, 0x2c, 0x02, 0x30, 0x5b, 0x50, 0x03, 0x80, 0x29, 0x2e, 0x7e, 0x01, 0x2b, 0x2e, 0x82, 0x01, 0x05, 0x42, 0x12, 0x30, 0x2b, + 0x2e, 0x83, 0x01, 0x45, 0x82, 0x00, 0x2e, 0x40, 0x40, 0x7a, 0x82, 0x02, 0xa0, 0x08, 0x2f, 0x63, 0x50, 0x3b, 0x30, 0x15, 0x42, + 0x05, 0x42, 0x37, 0x80, 0x37, 0x2e, 0x7e, 0x01, 0x05, 0x42, 0x12, 0x30, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x8c, 0x40, 0x40, 0x84, + 0x41, 0x7a, 0x8c, 0x04, 0x0f, 0x03, 0x2f, 0x01, 0x2e, 0x8b, 0x01, 0x19, 0xa4, 0x04, 0x2f, 0x2b, 0x2e, 0x82, 0x01, 0x98, 0x2e, + 0xf3, 0x03, 0x12, 0x30, 0x81, 0x90, 0x61, 0x52, 0x08, 0x2f, 0x65, 0x42, 0x65, 0x42, 0x43, 0x80, 0x39, 0x84, 0x82, 0x88, 0x05, + 0x42, 0x45, 0x42, 0x85, 0x42, 0x05, 0x43, 0x00, 0x2e, 0x80, 0x41, 0x00, 0x90, 0x90, 0x2e, 0xe1, 0xb4, 0x65, 0x54, 0xc1, 0x6f, + 0x80, 0x40, 0x00, 0xb2, 0x43, 0x58, 0x69, 0x50, 0x44, 0x2f, 0x55, 0x5c, 0xb7, 0x87, 0x8c, 0x0f, 0x0d, 0x2e, 0x96, 0x01, 0xc4, + 0x40, 0x36, 0x2f, 0x41, 0x56, 0x8b, 0x0e, 0x2a, 0x2f, 0x0b, 0x52, 0xa1, 0x0e, 0x0a, 0x2f, 0x05, 0x2e, 0x8f, 0x01, 0x14, 0x25, + 0x98, 0x2e, 0xfe, 0xc9, 0x4b, 0x54, 0x02, 0x0f, 0x69, 0x50, 0x05, 0x30, 0x65, 0x54, 0x15, 0x2f, 0x03, 0x2e, 0x8e, 0x01, 0x4d, + 0x5c, 0x8e, 0x0f, 0x3a, 0x2f, 0x05, 0x2e, 0x8f, 0x01, 0x98, 0x2e, 0xfe, 0xc9, 0x4f, 0x54, 0x82, 0x0f, 0x05, 0x30, 0x69, 0x50, + 0x65, 0x54, 0x30, 0x2f, 0x6d, 0x52, 0x15, 0x30, 0x42, 0x8c, 0x45, 0x42, 0x04, 0x30, 0x2b, 0x2c, 0x84, 0x43, 0x6b, 0x52, 0x42, + 0x8c, 0x00, 0x2e, 0x85, 0x43, 0x15, 0x30, 0x24, 0x2c, 0x45, 0x42, 0x8e, 0x0f, 0x20, 0x2f, 0x0d, 0x2e, 0x8e, 0x01, 0xb1, 0x0e, + 0x1c, 0x2f, 0x23, 0x2e, 0x8e, 0x01, 0x1a, 0x2d, 0x0e, 0x0e, 0x17, 0x2f, 0xa1, 0x0f, 0x15, 0x2f, 0x23, 0x2e, 0x8d, 0x01, 0x13, + 0x2d, 0x98, 0x2e, 0x74, 0xc0, 0x43, 0x54, 0xc2, 0x0e, 0x0a, 0x2f, 0x65, 0x50, 0x04, 0x80, 0x0b, 0x30, 0x06, 0x82, 0x0b, 0x42, + 0x79, 0x80, 0x41, 0x40, 0x12, 0x30, 0x25, 0x2e, 0x8c, 0x01, 0x01, 0x42, 0x05, 0x30, 0x69, 0x50, 0x65, 0x54, 0x84, 0x82, 0x43, + 0x84, 0xbe, 0x8c, 0x84, 0x40, 0x86, 0x41, 0x26, 0x29, 0x94, 0x42, 0xbe, 0x8e, 0xd5, 0x7f, 0x19, 0xa1, 0x43, 0x40, 0x0b, 0x2e, + 0x8c, 0x01, 0x84, 0x40, 0xc7, 0x41, 0x5d, 0x29, 0x27, 0x29, 0x45, 0x42, 0x84, 0x42, 0xc2, 0x7f, 0x01, 0x2f, 0xc0, 0xb3, 0x1d, + 0x2f, 0x05, 0x2e, 0x94, 0x01, 0x99, 0xa0, 0x01, 0x2f, 0x80, 0xb3, 0x13, 0x2f, 0x80, 0xb3, 0x18, 0x2f, 0xc0, 0xb3, 0x16, 0x2f, + 0x12, 0x40, 0x01, 0x40, 0x92, 0x7f, 0x98, 0x2e, 0x74, 0xc0, 0x92, 0x6f, 0x10, 0x0f, 0x20, 0x30, 0x03, 0x2f, 0x10, 0x30, 0x21, + 0x2e, 0x7e, 0x01, 0x0a, 0x2d, 0x21, 0x2e, 0x7e, 0x01, 0x07, 0x2d, 0x20, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0x03, 0x2d, 0x10, 0x30, + 0x21, 0x2e, 0x7e, 0x01, 0xc2, 0x6f, 0x01, 0x2e, 0xc9, 0x00, 0xbc, 0x84, 0x02, 0x80, 0x82, 0x40, 0x00, 0x40, 0x90, 0x0e, 0xd5, + 0x6f, 0x02, 0x2f, 0x15, 0x30, 0x98, 0x2e, 0xf3, 0x03, 0x41, 0x91, 0x05, 0x30, 0x07, 0x2f, 0x67, 0x50, 0x3d, 0x80, 0x2b, 0x2e, + 0x8f, 0x01, 0x05, 0x42, 0x04, 0x80, 0x00, 0x2e, 0x05, 0x42, 0x02, 0x2c, 0x00, 0x30, 0x00, 0x30, 0xa2, 0x6f, 0x98, 0x8a, 0x86, + 0x40, 0x80, 0xa7, 0x05, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0xc0, 0x30, 0x21, 0x2e, 0x95, 0x01, 0x06, 0x25, 0x1a, 0x25, 0xe2, 0x6f, + 0x76, 0x82, 0x96, 0x40, 0x56, 0x43, 0x51, 0x0e, 0xfb, 0x2f, 0xbb, 0x6f, 0x30, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xb8, 0x00, 0x01, + 0x31, 0x41, 0x08, 0x40, 0xb2, 0x20, 0x50, 0xf2, 0x30, 0x02, 0x08, 0xfb, 0x7f, 0x01, 0x30, 0x10, 0x2f, 0x05, 0x2e, 0xcc, 0x00, + 0x81, 0x90, 0xe0, 0x7f, 0x03, 0x2f, 0x23, 0x2e, 0xcc, 0x00, 0x98, 0x2e, 0x55, 0xb6, 0x98, 0x2e, 0x1d, 0xb5, 0x10, 0x25, 0xfb, + 0x6f, 0xe0, 0x6f, 0xe0, 0x5f, 0x80, 0x2e, 0x95, 0xcf, 0x98, 0x2e, 0x95, 0xcf, 0x10, 0x30, 0x21, 0x2e, 0xcc, 0x00, 0xfb, 0x6f, + 0xe0, 0x5f, 0xb8, 0x2e, 0x00, 0x51, 0x05, 0x58, 0xeb, 0x7f, 0x2a, 0x25, 0x89, 0x52, 0x6f, 0x5a, 0x89, 0x50, 0x13, 0x41, 0x06, + 0x40, 0xb3, 0x01, 0x16, 0x42, 0xcb, 0x16, 0x06, 0x40, 0xf3, 0x02, 0x13, 0x42, 0x65, 0x0e, 0xf5, 0x2f, 0x05, 0x40, 0x14, 0x30, + 0x2c, 0x29, 0x04, 0x42, 0x08, 0xa1, 0x00, 0x30, 0x90, 0x2e, 0x52, 0xb6, 0xb3, 0x88, 0xb0, 0x8a, 0xb6, 0x84, 0xa4, 0x7f, 0xc4, + 0x7f, 0xb5, 0x7f, 0xd5, 0x7f, 0x92, 0x7f, 0x73, 0x30, 0x04, 0x30, 0x55, 0x40, 0x42, 0x40, 0x8a, 0x17, 0xf3, 0x08, 0x6b, 0x01, + 0x90, 0x02, 0x53, 0xb8, 0x4b, 0x82, 0xad, 0xbe, 0x71, 0x7f, 0x45, 0x0a, 0x09, 0x54, 0x84, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0xa3, + 0x6f, 0x7b, 0x54, 0xd0, 0x42, 0xa3, 0x7f, 0xf2, 0x7f, 0x60, 0x7f, 0x20, 0x25, 0x71, 0x6f, 0x75, 0x5a, 0x77, 0x58, 0x79, 0x5c, + 0x75, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xb1, 0x6f, 0x62, 0x6f, 0x50, 0x42, 0xb1, 0x7f, 0xb3, 0x30, 0x10, 0x25, 0x98, 0x2e, 0x0f, + 0xca, 0x84, 0x6f, 0x20, 0x29, 0x71, 0x6f, 0x92, 0x6f, 0xa5, 0x6f, 0x76, 0x82, 0x6a, 0x0e, 0x73, 0x30, 0x00, 0x30, 0xd0, 0x2f, + 0xd2, 0x6f, 0xd1, 0x7f, 0xb4, 0x7f, 0x98, 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x02, 0x0a, 0xc2, 0x6f, 0xc0, 0x7f, 0x98, + 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x42, 0x0a, 0xc0, 0x6f, 0x08, 0x17, 0x41, 0x18, 0x89, 0x16, 0xe1, 0x18, 0xd0, 0x18, + 0xa1, 0x7f, 0x27, 0x25, 0x16, 0x25, 0x98, 0x2e, 0x79, 0xc0, 0x8b, 0x54, 0x90, 0x7f, 0xb3, 0x30, 0x82, 0x40, 0x80, 0x90, 0x0d, + 0x2f, 0x7d, 0x52, 0x92, 0x6f, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x0e, 0x06, 0x2f, 0x8b, 0x50, 0x14, 0x30, 0x42, 0x6f, + 0x51, 0x6f, 0x14, 0x42, 0x12, 0x42, 0x01, 0x42, 0x00, 0x2e, 0x31, 0x6f, 0x98, 0x2e, 0x74, 0xc0, 0x41, 0x6f, 0x80, 0x7f, 0x98, + 0x2e, 0x74, 0xc0, 0x82, 0x6f, 0x10, 0x04, 0x43, 0x52, 0x01, 0x0f, 0x05, 0x2e, 0xcb, 0x00, 0x00, 0x30, 0x04, 0x30, 0x21, 0x2f, + 0x51, 0x6f, 0x43, 0x58, 0x8c, 0x0e, 0x04, 0x30, 0x1c, 0x2f, 0x85, 0x88, 0x41, 0x6f, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30, 0x16, + 0x2f, 0x84, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x0f, 0x2f, 0x82, 0x88, 0x31, 0x6f, 0x04, 0x41, + 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x08, 0x2f, 0x83, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30, 0x02, 0x2f, 0x21, + 0x2e, 0xad, 0x01, 0x14, 0x30, 0x00, 0x91, 0x14, 0x2f, 0x03, 0x2e, 0xa1, 0x01, 0x41, 0x90, 0x0e, 0x2f, 0x03, 0x2e, 0xad, 0x01, + 0x14, 0x30, 0x4c, 0x28, 0x23, 0x2e, 0xad, 0x01, 0x46, 0xa0, 0x06, 0x2f, 0x81, 0x84, 0x8d, 0x52, 0x48, 0x82, 0x82, 0x40, 0x21, + 0x2e, 0xa1, 0x01, 0x42, 0x42, 0x5c, 0x2c, 0x02, 0x30, 0x05, 0x2e, 0xaa, 0x01, 0x80, 0xb2, 0x02, 0x30, 0x55, 0x2f, 0x03, 0x2e, + 0xa9, 0x01, 0x92, 0x6f, 0xb3, 0x30, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x0f, 0x00, 0x30, 0x02, 0x30, 0x4a, 0x2f, 0xa2, + 0x6f, 0x87, 0x52, 0x91, 0x00, 0x85, 0x52, 0x51, 0x0e, 0x02, 0x2f, 0x00, 0x2e, 0x43, 0x2c, 0x02, 0x30, 0xc2, 0x6f, 0x7f, 0x52, + 0x91, 0x0e, 0x02, 0x30, 0x3c, 0x2f, 0x51, 0x6f, 0x81, 0x54, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0xb3, 0x30, 0x21, 0x25, 0x98, + 0x2e, 0x0f, 0xca, 0x32, 0x6f, 0xc0, 0x7f, 0xb3, 0x30, 0x12, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x42, 0x6f, 0xb0, 0x7f, 0xb3, 0x30, + 0x12, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x28, 0x83, 0x52, 0x98, 0x2e, 0xfe, 0xc9, 0xc2, 0x6f, 0x90, 0x0f, 0x00, + 0x30, 0x02, 0x30, 0x1d, 0x2f, 0x05, 0x2e, 0xa1, 0x01, 0x80, 0xb2, 0x12, 0x30, 0x0f, 0x2f, 0x42, 0x6f, 0x03, 0x2e, 0xab, 0x01, + 0x91, 0x0e, 0x02, 0x30, 0x12, 0x2f, 0x52, 0x6f, 0x03, 0x2e, 0xac, 0x01, 0x91, 0x0f, 0x02, 0x30, 0x0c, 0x2f, 0x21, 0x2e, 0xaa, + 0x01, 0x0a, 0x2c, 0x12, 0x30, 0x03, 0x2e, 0xcb, 0x00, 0x8d, 0x58, 0x08, 0x89, 0x41, 0x40, 0x11, 0x43, 0x00, 0x43, 0x25, 0x2e, + 0xa1, 0x01, 0xd4, 0x6f, 0x8f, 0x52, 0x00, 0x43, 0x3a, 0x89, 0x00, 0x2e, 0x10, 0x43, 0x10, 0x43, 0x61, 0x0e, 0xfb, 0x2f, 0x03, + 0x2e, 0xa0, 0x01, 0x11, 0x1a, 0x02, 0x2f, 0x02, 0x25, 0x21, 0x2e, 0xa0, 0x01, 0xeb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x91, 0x52, + 0x10, 0x30, 0x02, 0x30, 0x95, 0x56, 0x52, 0x42, 0x4b, 0x0e, 0xfc, 0x2f, 0x8d, 0x54, 0x88, 0x82, 0x93, 0x56, 0x80, 0x42, 0x53, + 0x42, 0x40, 0x42, 0x42, 0x86, 0x83, 0x54, 0xc0, 0x2e, 0xc2, 0x42, 0x00, 0x2e, 0xa3, 0x52, 0x00, 0x51, 0x52, 0x40, 0x47, 0x40, + 0x1a, 0x25, 0x01, 0x2e, 0x97, 0x00, 0x8f, 0xbe, 0x72, 0x86, 0xfb, 0x7f, 0x0b, 0x30, 0x7c, 0xbf, 0xa5, 0x50, 0x10, 0x08, 0xdf, + 0xba, 0x70, 0x88, 0xf8, 0xbf, 0xcb, 0x42, 0xd3, 0x7f, 0x6c, 0xbb, 0xfc, 0xbb, 0xc5, 0x0a, 0x90, 0x7f, 0x1b, 0x7f, 0x0b, 0x43, + 0xc0, 0xb2, 0xe5, 0x7f, 0xb7, 0x7f, 0xa6, 0x7f, 0xc4, 0x7f, 0x90, 0x2e, 0x1c, 0xb7, 0x07, 0x2e, 0xd2, 0x00, 0xc0, 0xb2, 0x0b, + 0x2f, 0x97, 0x52, 0x01, 0x2e, 0xcd, 0x00, 0x82, 0x7f, 0x98, 0x2e, 0xbb, 0xcc, 0x0b, 0x30, 0x37, 0x2e, 0xd2, 0x00, 0x82, 0x6f, + 0x90, 0x6f, 0x1a, 0x25, 0x00, 0xb2, 0x8b, 0x7f, 0x14, 0x2f, 0xa6, 0xbd, 0x25, 0xbd, 0xb6, 0xb9, 0x2f, 0xb9, 0x80, 0xb2, 0xd4, + 0xb0, 0x0c, 0x2f, 0x99, 0x54, 0x9b, 0x56, 0x0b, 0x30, 0x0b, 0x2e, 0xb1, 0x00, 0xa1, 0x58, 0x9b, 0x42, 0xdb, 0x42, 0x6c, 0x09, + 0x2b, 0x2e, 0xb1, 0x00, 0x8b, 0x42, 0xcb, 0x42, 0x86, 0x7f, 0x73, 0x84, 0xa7, 0x56, 0xc3, 0x08, 0x39, 0x52, 0x05, 0x50, 0x72, + 0x7f, 0x63, 0x7f, 0x98, 0x2e, 0xc2, 0xc0, 0xe1, 0x6f, 0x62, 0x6f, 0xd1, 0x0a, 0x01, 0x2e, 0xcd, 0x00, 0xd5, 0x6f, 0xc4, 0x6f, + 0x72, 0x6f, 0x97, 0x52, 0x9d, 0x5c, 0x98, 0x2e, 0x06, 0xcd, 0x23, 0x6f, 0x90, 0x6f, 0x99, 0x52, 0xc0, 0xb2, 0x04, 0xbd, 0x54, + 0x40, 0xaf, 0xb9, 0x45, 0x40, 0xe1, 0x7f, 0x02, 0x30, 0x06, 0x2f, 0xc0, 0xb2, 0x02, 0x30, 0x03, 0x2f, 0x9b, 0x5c, 0x12, 0x30, + 0x94, 0x43, 0x85, 0x43, 0x03, 0xbf, 0x6f, 0xbb, 0x80, 0xb3, 0x20, 0x2f, 0x06, 0x6f, 0x26, 0x01, 0x16, 0x6f, 0x6e, 0x03, 0x45, + 0x42, 0xc0, 0x90, 0x29, 0x2e, 0xce, 0x00, 0x9b, 0x52, 0x14, 0x2f, 0x9b, 0x5c, 0x00, 0x2e, 0x93, 0x41, 0x86, 0x41, 0xe3, 0x04, + 0xae, 0x07, 0x80, 0xab, 0x04, 0x2f, 0x80, 0x91, 0x0a, 0x2f, 0x86, 0x6f, 0x73, 0x0f, 0x07, 0x2f, 0x83, 0x6f, 0xc0, 0xb2, 0x04, + 0x2f, 0x54, 0x42, 0x45, 0x42, 0x12, 0x30, 0x04, 0x2c, 0x11, 0x30, 0x02, 0x2c, 0x11, 0x30, 0x11, 0x30, 0x02, 0xbc, 0x0f, 0xb8, + 0xd2, 0x7f, 0x00, 0xb2, 0x0a, 0x2f, 0x01, 0x2e, 0xfc, 0x00, 0x05, 0x2e, 0xc7, 0x01, 0x10, 0x1a, 0x02, 0x2f, 0x21, 0x2e, 0xc7, + 0x01, 0x03, 0x2d, 0x02, 0x2c, 0x01, 0x30, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xd1, 0x6f, 0xa0, 0x6f, 0x98, 0x2e, + 0x95, 0xcf, 0xe2, 0x6f, 0x9f, 0x52, 0x01, 0x2e, 0xce, 0x00, 0x82, 0x40, 0x50, 0x42, 0x0c, 0x2c, 0x42, 0x42, 0x11, 0x30, 0x23, + 0x2e, 0xd2, 0x00, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xa0, 0x6f, 0x01, 0x30, 0x98, 0x2e, 0x95, 0xcf, 0x00, 0x2e, + 0xfb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x83, 0x86, 0x01, 0x30, 0x00, 0x30, 0x94, 0x40, 0x24, 0x18, 0x06, 0x00, 0x53, 0x0e, 0x4f, + 0x02, 0xf9, 0x2f, 0xb8, 0x2e, 0xa9, 0x52, 0x00, 0x2e, 0x60, 0x40, 0x41, 0x40, 0x0d, 0xbc, 0x98, 0xbc, 0xc0, 0x2e, 0x01, 0x0a, + 0x0f, 0xb8, 0xab, 0x52, 0x53, 0x3c, 0x52, 0x40, 0x40, 0x40, 0x4b, 0x00, 0x82, 0x16, 0x26, 0xb9, 0x01, 0xb8, 0x41, 0x40, 0x10, + 0x08, 0x97, 0xb8, 0x01, 0x08, 0xc0, 0x2e, 0x11, 0x30, 0x01, 0x08, 0x43, 0x86, 0x25, 0x40, 0x04, 0x40, 0xd8, 0xbe, 0x2c, 0x0b, + 0x22, 0x11, 0x54, 0x42, 0x03, 0x80, 0x4b, 0x0e, 0xf6, 0x2f, 0xb8, 0x2e, 0x9f, 0x50, 0x10, 0x50, 0xad, 0x52, 0x05, 0x2e, 0xd3, + 0x00, 0xfb, 0x7f, 0x00, 0x2e, 0x13, 0x40, 0x93, 0x42, 0x41, 0x0e, 0xfb, 0x2f, 0x98, 0x2e, 0xa5, 0xb7, 0x98, 0x2e, 0x87, 0xcf, + 0x01, 0x2e, 0xd9, 0x00, 0x00, 0xb2, 0xfb, 0x6f, 0x0b, 0x2f, 0x01, 0x2e, 0x69, 0xf7, 0xb1, 0x3f, 0x01, 0x08, 0x01, 0x30, 0xf0, + 0x5f, 0x23, 0x2e, 0xd9, 0x00, 0x21, 0x2e, 0x69, 0xf7, 0x80, 0x2e, 0x7a, 0xb7, 0xf0, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xc0, 0xf8, + 0x03, 0x2e, 0xfc, 0xf5, 0x15, 0x54, 0xaf, 0x56, 0x82, 0x08, 0x0b, 0x2e, 0x69, 0xf7, 0xcb, 0x0a, 0xb1, 0x58, 0x80, 0x90, 0xdd, + 0xbe, 0x4c, 0x08, 0x5f, 0xb9, 0x59, 0x22, 0x80, 0x90, 0x07, 0x2f, 0x03, 0x34, 0xc3, 0x08, 0xf2, 0x3a, 0x0a, 0x08, 0x02, 0x35, + 0xc0, 0x90, 0x4a, 0x0a, 0x48, 0x22, 0xc0, 0x2e, 0x23, 0x2e, 0xfc, 0xf5, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x56, 0xc7, 0x98, + 0x2e, 0x49, 0xc3, 0x10, 0x30, 0xfb, 0x6f, 0xf0, 0x5f, 0x21, 0x2e, 0xcc, 0x00, 0x21, 0x2e, 0xca, 0x00, 0xb8, 0x2e, 0x03, 0x2e, + 0xd3, 0x00, 0x16, 0xb8, 0x02, 0x34, 0x4a, 0x0c, 0x21, 0x2e, 0x2d, 0xf5, 0xc0, 0x2e, 0x23, 0x2e, 0xd3, 0x00, 0x03, 0xbc, 0x21, + 0x2e, 0xd5, 0x00, 0x03, 0x2e, 0xd5, 0x00, 0x40, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x01, 0x30, 0x05, 0x2f, 0x05, 0x2e, + 0xd8, 0x00, 0x80, 0x90, 0x01, 0x2f, 0x23, 0x2e, 0x6f, 0xf5, 0xc0, 0x2e, 0x21, 0x2e, 0xd9, 0x00, 0x11, 0x30, 0x81, 0x08, 0x01, + 0x2e, 0x6a, 0xf7, 0x71, 0x3f, 0x23, 0xbd, 0x01, 0x08, 0x02, 0x0a, 0xc0, 0x2e, 0x21, 0x2e, 0x6a, 0xf7, 0x30, 0x25, 0x00, 0x30, + 0x21, 0x2e, 0x5a, 0xf5, 0x10, 0x50, 0x21, 0x2e, 0x7b, 0x00, 0x21, 0x2e, 0x7c, 0x00, 0xfb, 0x7f, 0x98, 0x2e, 0xc3, 0xb7, 0x40, + 0x30, 0x21, 0x2e, 0xd4, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, 0x03, 0x25, 0x80, 0x2e, 0xaf, 0xb7, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x01, 0x2e, 0x5d, 0xf7, 0x08, 0xbc, 0x80, 0xac, 0x0e, + 0xbb, 0x02, 0x2f, 0x00, 0x30, 0x41, 0x04, 0x82, 0x06, 0xc0, 0xa4, 0x00, 0x30, 0x11, 0x2f, 0x40, 0xa9, 0x03, 0x2f, 0x40, 0x91, + 0x0d, 0x2f, 0x00, 0xa7, 0x0b, 0x2f, 0x80, 0xb3, 0xb3, 0x58, 0x02, 0x2f, 0x90, 0xa1, 0x26, 0x13, 0x20, 0x23, 0x80, 0x90, 0x10, + 0x30, 0x01, 0x2f, 0xcc, 0x0e, 0x00, 0x2f, 0x00, 0x30, 0xb8, 0x2e, 0xb5, 0x50, 0x18, 0x08, 0x08, 0xbc, 0x88, 0xb6, 0x0d, 0x17, + 0xc6, 0xbd, 0x56, 0xbc, 0xb7, 0x58, 0xda, 0xba, 0x04, 0x01, 0x1d, 0x0a, 0x10, 0x50, 0x05, 0x30, 0x32, 0x25, 0x45, 0x03, 0xfb, + 0x7f, 0xf6, 0x30, 0x21, 0x25, 0x98, 0x2e, 0x37, 0xca, 0x16, 0xb5, 0x9a, 0xbc, 0x06, 0xb8, 0x80, 0xa8, 0x41, 0x0a, 0x0e, 0x2f, + 0x80, 0x90, 0x02, 0x2f, 0x2d, 0x50, 0x48, 0x0f, 0x09, 0x2f, 0xbf, 0xa0, 0x04, 0x2f, 0xbf, 0x90, 0x06, 0x2f, 0xb7, 0x54, 0xca, + 0x0f, 0x03, 0x2f, 0x00, 0x2e, 0x02, 0x2c, 0xb7, 0x52, 0x2d, 0x52, 0xf2, 0x33, 0x98, 0x2e, 0xd9, 0xc0, 0xfb, 0x6f, 0xf1, 0x37, + 0xc0, 0x2e, 0x01, 0x08, 0xf0, 0x5f, 0xbf, 0x56, 0xb9, 0x54, 0xd0, 0x40, 0xc4, 0x40, 0x0b, 0x2e, 0xfd, 0xf3, 0xbf, 0x52, 0x90, + 0x42, 0x94, 0x42, 0x95, 0x42, 0x05, 0x30, 0xc1, 0x50, 0x0f, 0x88, 0x06, 0x40, 0x04, 0x41, 0x96, 0x42, 0xc5, 0x42, 0x48, 0xbe, + 0x73, 0x30, 0x0d, 0x2e, 0xd8, 0x00, 0x4f, 0xba, 0x84, 0x42, 0x03, 0x42, 0x81, 0xb3, 0x02, 0x2f, 0x2b, 0x2e, 0x6f, 0xf5, 0x06, + 0x2d, 0x05, 0x2e, 0x77, 0xf7, 0xbd, 0x56, 0x93, 0x08, 0x25, 0x2e, 0x77, 0xf7, 0xbb, 0x54, 0x25, 0x2e, 0xc2, 0xf5, 0x07, 0x2e, + 0xfd, 0xf3, 0x42, 0x30, 0xb4, 0x33, 0xda, 0x0a, 0x4c, 0x00, 0x27, 0x2e, 0xfd, 0xf3, 0x43, 0x40, 0xd4, 0x3f, 0xdc, 0x08, 0x43, + 0x42, 0x00, 0x2e, 0x00, 0x2e, 0x43, 0x40, 0x24, 0x30, 0xdc, 0x0a, 0x43, 0x42, 0x04, 0x80, 0x03, 0x2e, 0xfd, 0xf3, 0x4a, 0x0a, + 0x23, 0x2e, 0xfd, 0xf3, 0x61, 0x34, 0xc0, 0x2e, 0x01, 0x42, 0x00, 0x2e, 0x60, 0x50, 0x1a, 0x25, 0x7a, 0x86, 0xe0, 0x7f, 0xf3, + 0x7f, 0x03, 0x25, 0xc3, 0x52, 0x41, 0x84, 0xdb, 0x7f, 0x33, 0x30, 0x98, 0x2e, 0x16, 0xc2, 0x1a, 0x25, 0x7d, 0x82, 0xf0, 0x6f, + 0xe2, 0x6f, 0x32, 0x25, 0x16, 0x40, 0x94, 0x40, 0x26, 0x01, 0x85, 0x40, 0x8e, 0x17, 0xc4, 0x42, 0x6e, 0x03, 0x95, 0x42, 0x41, + 0x0e, 0xf4, 0x2f, 0xdb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e, 0xb0, 0x51, 0xfb, 0x7f, 0x98, 0x2e, 0xe8, 0x0d, 0x5a, 0x25, 0x98, 0x2e, + 0x0f, 0x0e, 0xcb, 0x58, 0x32, 0x87, 0xc4, 0x7f, 0x65, 0x89, 0x6b, 0x8d, 0xc5, 0x5a, 0x65, 0x7f, 0xe1, 0x7f, 0x83, 0x7f, 0xa6, + 0x7f, 0x74, 0x7f, 0xd0, 0x7f, 0xb6, 0x7f, 0x94, 0x7f, 0x17, 0x30, 0xc7, 0x52, 0xc9, 0x54, 0x51, 0x7f, 0x00, 0x2e, 0x85, 0x6f, + 0x42, 0x7f, 0x00, 0x2e, 0x51, 0x41, 0x45, 0x81, 0x42, 0x41, 0x13, 0x40, 0x3b, 0x8a, 0x00, 0x40, 0x4b, 0x04, 0xd0, 0x06, 0xc0, + 0xac, 0x85, 0x7f, 0x02, 0x2f, 0x02, 0x30, 0x51, 0x04, 0xd3, 0x06, 0x41, 0x84, 0x05, 0x30, 0x5d, 0x02, 0xc9, 0x16, 0xdf, 0x08, + 0xd3, 0x00, 0x8d, 0x02, 0xaf, 0xbc, 0xb1, 0xb9, 0x59, 0x0a, 0x65, 0x6f, 0x11, 0x43, 0xa1, 0xb4, 0x52, 0x41, 0x53, 0x41, 0x01, + 0x43, 0x34, 0x7f, 0x65, 0x7f, 0x26, 0x31, 0xe5, 0x6f, 0xd4, 0x6f, 0x98, 0x2e, 0x37, 0xca, 0x32, 0x6f, 0x75, 0x6f, 0x83, 0x40, + 0x42, 0x41, 0x23, 0x7f, 0x12, 0x7f, 0xf6, 0x30, 0x40, 0x25, 0x51, 0x25, 0x98, 0x2e, 0x37, 0xca, 0x14, 0x6f, 0x20, 0x05, 0x70, + 0x6f, 0x25, 0x6f, 0x69, 0x07, 0xa2, 0x6f, 0x31, 0x6f, 0x0b, 0x30, 0x04, 0x42, 0x9b, 0x42, 0x8b, 0x42, 0x55, 0x42, 0x32, 0x7f, + 0x40, 0xa9, 0xc3, 0x6f, 0x71, 0x7f, 0x02, 0x30, 0xd0, 0x40, 0xc3, 0x7f, 0x03, 0x2f, 0x40, 0x91, 0x15, 0x2f, 0x00, 0xa7, 0x13, + 0x2f, 0x00, 0xa4, 0x11, 0x2f, 0x84, 0xbd, 0x98, 0x2e, 0x79, 0xca, 0x55, 0x6f, 0xb7, 0x54, 0x54, 0x41, 0x82, 0x00, 0xf3, 0x3f, + 0x45, 0x41, 0xcb, 0x02, 0xf6, 0x30, 0x98, 0x2e, 0x37, 0xca, 0x35, 0x6f, 0xa4, 0x6f, 0x41, 0x43, 0x03, 0x2c, 0x00, 0x43, 0xa4, + 0x6f, 0x35, 0x6f, 0x17, 0x30, 0x42, 0x6f, 0x51, 0x6f, 0x93, 0x40, 0x42, 0x82, 0x00, 0x41, 0xc3, 0x00, 0x03, 0x43, 0x51, 0x7f, + 0x00, 0x2e, 0x94, 0x40, 0x41, 0x41, 0x4c, 0x02, 0xc4, 0x6f, 0xd1, 0x56, 0x63, 0x0e, 0x74, 0x6f, 0x51, 0x43, 0xa5, 0x7f, 0x8a, + 0x2f, 0x09, 0x2e, 0xd8, 0x00, 0x01, 0xb3, 0x21, 0x2f, 0xcb, 0x58, 0x90, 0x6f, 0x13, 0x41, 0xb6, 0x6f, 0xe4, 0x7f, 0x00, 0x2e, + 0x91, 0x41, 0x14, 0x40, 0x92, 0x41, 0x15, 0x40, 0x17, 0x2e, 0x6f, 0xf5, 0xb6, 0x7f, 0xd0, 0x7f, 0xcb, 0x7f, 0x98, 0x2e, 0x00, + 0x0c, 0x07, 0x15, 0xc2, 0x6f, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0xc3, 0xa3, 0xc1, 0x8f, 0xe4, 0x6f, 0xd0, 0x6f, 0xe6, 0x2f, + 0x14, 0x30, 0x05, 0x2e, 0x6f, 0xf5, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0x18, 0x2d, 0xcd, 0x56, 0x04, 0x32, 0xb5, 0x6f, 0x1c, + 0x01, 0x51, 0x41, 0x52, 0x41, 0xc3, 0x40, 0xb5, 0x7f, 0xe4, 0x7f, 0x98, 0x2e, 0x1f, 0x0c, 0xe4, 0x6f, 0x21, 0x87, 0x00, 0x43, + 0x04, 0x32, 0xcf, 0x54, 0x5a, 0x0e, 0xef, 0x2f, 0x15, 0x54, 0x09, 0x2e, 0x77, 0xf7, 0x22, 0x0b, 0x29, 0x2e, 0x77, 0xf7, 0xfb, + 0x6f, 0x50, 0x5e, 0xb8, 0x2e, 0x10, 0x50, 0x01, 0x2e, 0xd4, 0x00, 0x00, 0xb2, 0xfb, 0x7f, 0x51, 0x2f, 0x01, 0xb2, 0x48, 0x2f, + 0x02, 0xb2, 0x42, 0x2f, 0x03, 0x90, 0x56, 0x2f, 0xd7, 0x52, 0x79, 0x80, 0x42, 0x40, 0x81, 0x84, 0x00, 0x40, 0x42, 0x42, 0x98, + 0x2e, 0x93, 0x0c, 0xd9, 0x54, 0xd7, 0x50, 0xa1, 0x40, 0x98, 0xbd, 0x82, 0x40, 0x3e, 0x82, 0xda, 0x0a, 0x44, 0x40, 0x8b, 0x16, + 0xe3, 0x00, 0x53, 0x42, 0x00, 0x2e, 0x43, 0x40, 0x9a, 0x02, 0x52, 0x42, 0x00, 0x2e, 0x41, 0x40, 0x15, 0x54, 0x4a, 0x0e, 0x3a, + 0x2f, 0x3a, 0x82, 0x00, 0x30, 0x41, 0x40, 0x21, 0x2e, 0x85, 0x0f, 0x40, 0xb2, 0x0a, 0x2f, 0x98, 0x2e, 0xb1, 0x0c, 0x98, 0x2e, + 0x45, 0x0e, 0x98, 0x2e, 0x5b, 0x0e, 0xfb, 0x6f, 0xf0, 0x5f, 0x00, 0x30, 0x80, 0x2e, 0xce, 0xb7, 0xdd, 0x52, 0xd3, 0x54, 0x42, + 0x42, 0x4f, 0x84, 0x73, 0x30, 0xdb, 0x52, 0x83, 0x42, 0x1b, 0x30, 0x6b, 0x42, 0x23, 0x30, 0x27, 0x2e, 0xd7, 0x00, 0x37, 0x2e, + 0xd4, 0x00, 0x21, 0x2e, 0xd6, 0x00, 0x7a, 0x84, 0x17, 0x2c, 0x42, 0x42, 0x30, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x12, 0x2d, 0x21, + 0x30, 0x00, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x21, 0x2e, 0x7b, 0xf7, 0x0b, 0x2d, 0x17, 0x30, 0x98, 0x2e, 0x51, 0x0c, 0xd5, 0x50, + 0x0c, 0x82, 0x72, 0x30, 0x2f, 0x2e, 0xd4, 0x00, 0x25, 0x2e, 0x7b, 0xf7, 0x40, 0x42, 0x00, 0x2e, 0xfb, 0x6f, 0xf0, 0x5f, 0xb8, + 0x2e, 0x70, 0x50, 0x0a, 0x25, 0x39, 0x86, 0xfb, 0x7f, 0xe1, 0x32, 0x62, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xb5, 0x56, 0xa5, 0x6f, + 0xab, 0x08, 0x91, 0x6f, 0x4b, 0x08, 0xdf, 0x56, 0xc4, 0x6f, 0x23, 0x09, 0x4d, 0xba, 0x93, 0xbc, 0x8c, 0x0b, 0xd1, 0x6f, 0x0b, + 0x09, 0xcb, 0x52, 0xe1, 0x5e, 0x56, 0x42, 0xaf, 0x09, 0x4d, 0xba, 0x23, 0xbd, 0x94, 0x0a, 0xe5, 0x6f, 0x68, 0xbb, 0xeb, 0x08, + 0xbd, 0xb9, 0x63, 0xbe, 0xfb, 0x6f, 0x52, 0x42, 0xe3, 0x0a, 0xc0, 0x2e, 0x43, 0x42, 0x90, 0x5f, 0xd1, 0x50, 0x03, 0x2e, 0x25, + 0xf3, 0x13, 0x40, 0x00, 0x40, 0x9b, 0xbc, 0x9b, 0xb4, 0x08, 0xbd, 0xb8, 0xb9, 0x98, 0xbc, 0xda, 0x0a, 0x08, 0xb6, 0x89, 0x16, + 0xc0, 0x2e, 0x19, 0x00, 0x62, 0x02, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x81, 0x0d, 0x01, 0x2e, 0xd4, 0x00, 0x31, 0x30, 0x08, + 0x04, 0xfb, 0x6f, 0x01, 0x30, 0xf0, 0x5f, 0x23, 0x2e, 0xd6, 0x00, 0x21, 0x2e, 0xd7, 0x00, 0xb8, 0x2e, 0x01, 0x2e, 0xd7, 0x00, + 0x03, 0x2e, 0xd6, 0x00, 0x48, 0x0e, 0x01, 0x2f, 0x80, 0x2e, 0x1f, 0x0e, 0xb8, 0x2e, 0xe3, 0x50, 0x21, 0x34, 0x01, 0x42, 0x82, + 0x30, 0xc1, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x01, 0x00, 0x22, 0x30, 0x01, 0x40, 0x4a, 0x0a, 0x01, 0x42, 0xb8, 0x2e, 0xe3, 0x54, + 0xf0, 0x3b, 0x83, 0x40, 0xd8, 0x08, 0xe5, 0x52, 0x83, 0x42, 0x00, 0x30, 0x83, 0x30, 0x50, 0x42, 0xc4, 0x32, 0x27, 0x2e, 0x64, + 0xf5, 0x94, 0x00, 0x50, 0x42, 0x40, 0x42, 0xd3, 0x3f, 0x84, 0x40, 0x7d, 0x82, 0xe3, 0x08, 0x40, 0x42, 0x83, 0x42, 0xb8, 0x2e, + 0xdd, 0x52, 0x00, 0x30, 0x40, 0x42, 0x7c, 0x86, 0xb9, 0x52, 0x09, 0x2e, 0x70, 0x0f, 0xbf, 0x54, 0xc4, 0x42, 0xd3, 0x86, 0x54, + 0x40, 0x55, 0x40, 0x94, 0x42, 0x85, 0x42, 0x21, 0x2e, 0xd7, 0x00, 0x42, 0x40, 0x25, 0x2e, 0xfd, 0xf3, 0xc0, 0x42, 0x7e, 0x82, + 0x05, 0x2e, 0x7d, 0x00, 0x80, 0xb2, 0x14, 0x2f, 0x05, 0x2e, 0x89, 0x00, 0x27, 0xbd, 0x2f, 0xb9, 0x80, 0x90, 0x02, 0x2f, 0x21, + 0x2e, 0x6f, 0xf5, 0x0c, 0x2d, 0x07, 0x2e, 0x71, 0x0f, 0x14, 0x30, 0x1c, 0x09, 0x05, 0x2e, 0x77, 0xf7, 0xbd, 0x56, 0x47, 0xbe, + 0x93, 0x08, 0x94, 0x0a, 0x25, 0x2e, 0x77, 0xf7, 0xe7, 0x54, 0x50, 0x42, 0x4a, 0x0e, 0xfc, 0x2f, 0xb8, 0x2e, 0x50, 0x50, 0x02, + 0x30, 0x43, 0x86, 0xe5, 0x50, 0xfb, 0x7f, 0xe3, 0x7f, 0xd2, 0x7f, 0xc0, 0x7f, 0xb1, 0x7f, 0x00, 0x2e, 0x41, 0x40, 0x00, 0x40, + 0x48, 0x04, 0x98, 0x2e, 0x74, 0xc0, 0x1e, 0xaa, 0xd3, 0x6f, 0x14, 0x30, 0xb1, 0x6f, 0xe3, 0x22, 0xc0, 0x6f, 0x52, 0x40, 0xe4, + 0x6f, 0x4c, 0x0e, 0x12, 0x42, 0xd3, 0x7f, 0xeb, 0x2f, 0x03, 0x2e, 0x86, 0x0f, 0x40, 0x90, 0x11, 0x30, 0x03, 0x2f, 0x23, 0x2e, + 0x86, 0x0f, 0x02, 0x2c, 0x00, 0x30, 0xd0, 0x6f, 0xfb, 0x6f, 0xb0, 0x5f, 0xb8, 0x2e, 0x40, 0x50, 0xf1, 0x7f, 0x0a, 0x25, 0x3c, + 0x86, 0xeb, 0x7f, 0x41, 0x33, 0x22, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xd3, 0x6f, 0xf4, 0x30, 0xdc, 0x09, 0x47, 0x58, 0xc2, 0x6f, + 0x94, 0x09, 0xeb, 0x58, 0x6a, 0xbb, 0xdc, 0x08, 0xb4, 0xb9, 0xb1, 0xbd, 0xe9, 0x5a, 0x95, 0x08, 0x21, 0xbd, 0xf6, 0xbf, 0x77, + 0x0b, 0x51, 0xbe, 0xf1, 0x6f, 0xeb, 0x6f, 0x52, 0x42, 0x54, 0x42, 0xc0, 0x2e, 0x43, 0x42, 0xc0, 0x5f, 0x50, 0x50, 0xf5, 0x50, + 0x31, 0x30, 0x11, 0x42, 0xfb, 0x7f, 0x7b, 0x30, 0x0b, 0x42, 0x11, 0x30, 0x02, 0x80, 0x23, 0x33, 0x01, 0x42, 0x03, 0x00, 0x07, + 0x2e, 0x80, 0x03, 0x05, 0x2e, 0xd3, 0x00, 0x23, 0x52, 0xe2, 0x7f, 0xd3, 0x7f, 0xc0, 0x7f, 0x98, 0x2e, 0xb6, 0x0e, 0xd1, 0x6f, + 0x08, 0x0a, 0x1a, 0x25, 0x7b, 0x86, 0xd0, 0x7f, 0x01, 0x33, 0x12, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xd1, 0x6f, 0x08, 0x0a, 0x00, + 0xb2, 0x0d, 0x2f, 0xe3, 0x6f, 0x01, 0x2e, 0x80, 0x03, 0x51, 0x30, 0xc7, 0x86, 0x23, 0x2e, 0x21, 0xf2, 0x08, 0xbc, 0xc0, 0x42, + 0x98, 0x2e, 0xa5, 0xb7, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0xb0, 0x6f, 0x0b, 0xb8, 0x03, 0x2e, 0x1b, 0x00, 0x08, 0x1a, 0xb0, + 0x7f, 0x70, 0x30, 0x04, 0x2f, 0x21, 0x2e, 0x21, 0xf2, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x98, 0x2e, 0x6d, 0xc0, 0x98, 0x2e, + 0x5d, 0xc0, 0xed, 0x50, 0x98, 0x2e, 0x44, 0xcb, 0xef, 0x50, 0x98, 0x2e, 0x46, 0xc3, 0xf1, 0x50, 0x98, 0x2e, 0x53, 0xc7, 0x35, + 0x50, 0x98, 0x2e, 0x64, 0xcf, 0x10, 0x30, 0x98, 0x2e, 0xdc, 0x03, 0x20, 0x26, 0xc0, 0x6f, 0x02, 0x31, 0x12, 0x42, 0xab, 0x33, + 0x0b, 0x42, 0x37, 0x80, 0x01, 0x30, 0x01, 0x42, 0xf3, 0x37, 0xf7, 0x52, 0xfb, 0x50, 0x44, 0x40, 0xa2, 0x0a, 0x42, 0x42, 0x8b, + 0x31, 0x09, 0x2e, 0x5e, 0xf7, 0xf9, 0x54, 0xe3, 0x08, 0x83, 0x42, 0x1b, 0x42, 0x23, 0x33, 0x4b, 0x00, 0xbc, 0x84, 0x0b, 0x40, + 0x33, 0x30, 0x83, 0x42, 0x0b, 0x42, 0xe0, 0x7f, 0xd1, 0x7f, 0x98, 0x2e, 0x58, 0xb7, 0xd1, 0x6f, 0x80, 0x30, 0x40, 0x42, 0x03, + 0x30, 0xe0, 0x6f, 0xf3, 0x54, 0x04, 0x30, 0x00, 0x2e, 0x00, 0x2e, 0x01, 0x89, 0x62, 0x0e, 0xfa, 0x2f, 0x43, 0x42, 0x11, 0x30, + 0xfb, 0x6f, 0xc0, 0x2e, 0x01, 0x42, 0xb0, 0x5f, 0xc1, 0x4a, 0x00, 0x00, 0x6d, 0x57, 0x00, 0x00, 0x77, 0x8e, 0x00, 0x00, 0xe0, + 0xff, 0xff, 0xff, 0xd3, 0xff, 0xff, 0xff, 0xe5, 0xff, 0xff, 0xff, 0xee, 0xe1, 0xff, 0xff, 0x7c, 0x13, 0x00, 0x00, 0x46, 0xe6, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1}; + +#define BMI270_CONFIG_FILE_SIZE sizeof(bmi270_config_file) + +BMI270Sensor::BMI270Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) +{ + if (foundDevice.address.port == ScanI2C::I2CPort::WIRE1) { +#ifdef I2C_SDA1 + wire = &Wire1; +#else + wire = &Wire; +#endif + } else { + wire = &Wire; + } +} + +bool BMI270Sensor::writeRegister(uint8_t reg, uint8_t value) +{ + wire->beginTransmission(deviceAddress()); + wire->write(reg); + wire->write(value); + return wire->endTransmission() == 0; +} + +bool BMI270Sensor::writeRegisters(uint8_t reg, const uint8_t *data, size_t len) +{ + wire->beginTransmission(deviceAddress()); + wire->write(reg); + for (size_t i = 0; i < len; i++) { + wire->write(data[i]); + } + return wire->endTransmission() == 0; +} + +uint8_t BMI270Sensor::readRegister(uint8_t reg) +{ + wire->beginTransmission(deviceAddress()); + wire->write(reg); + wire->endTransmission(false); + wire->requestFrom(deviceAddress(), (uint8_t)1); + if (wire->available()) { + return wire->read(); + } + return 0; +} + +bool BMI270Sensor::readRegisters(uint8_t reg, uint8_t *data, size_t len) +{ + wire->beginTransmission(deviceAddress()); + wire->write(reg); + wire->endTransmission(false); + size_t bytesRead = wire->requestFrom(deviceAddress(), (uint8_t)len); + if (bytesRead != len) { + // Read any available bytes to keep the bus state clean, but report failure. + for (size_t i = 0; i < bytesRead && wire->available(); i++) { + data[i] = wire->read(); + } + return false; + } + + for (size_t i = 0; i < len && wire->available(); i++) { + data[i] = wire->read(); + } + return true; +} + +bool BMI270Sensor::uploadConfigFile() +{ + if (!writeRegister(BMI270_REG_INIT_CTRL, 0x00)) + return false; + + const size_t chunkSize = 32; + size_t bytesWritten = 0; + + while (bytesWritten < BMI270_CONFIG_FILE_SIZE) { + size_t remaining = BMI270_CONFIG_FILE_SIZE - bytesWritten; + size_t toWrite = (remaining < chunkSize) ? remaining : chunkSize; + + // Set address in word units before each chunk + uint16_t wordAddr = bytesWritten / 2; + if (!writeRegister(BMI270_REG_INIT_ADDR_0, (uint8_t)(wordAddr & 0x0F))) + return false; + if (!writeRegister(BMI270_REG_INIT_ADDR_1, (uint8_t)(wordAddr >> 4))) + return false; + + wire->beginTransmission(deviceAddress()); + wire->write(BMI270_REG_INIT_DATA); + for (size_t i = 0; i < toWrite; i++) { + wire->write(pgm_read_byte(&bmi270_config_file[bytesWritten + i])); + } + if (wire->endTransmission() != 0) + return false; + + bytesWritten += toWrite; + } + + if (!writeRegister(BMI270_REG_INIT_CTRL, 0x01)) + return false; + + delay(50); + + for (int i = 0; i < 10; i++) { + uint8_t status = readRegister(BMI270_REG_INTERNAL_STATUS); + if ((status & 0x0F) == BMI270_INIT_OK) + return true; + delay(20); + } + + LOG_WARN("BMI270 status=0x%02X", readRegister(BMI270_REG_INTERNAL_STATUS)); + return false; +} + +bool BMI270Sensor::init() +{ + delay(10); + writeRegister(BMI270_REG_CMD, BMI270_CMD_SOFTRESET); + delay(50); + + if (!writeRegister(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_ADV_POWER_SAVE_DISABLED)) + return false; + delay(2); + + if (!uploadConfigFile()) { + LOG_WARN("BMI270 config failed"); + return false; + } + + uint8_t accConf = BMI270_ACC_ODR_50HZ | BMI270_ACC_BWP_NORMAL | BMI270_ACC_FILTER_PERF; + if (!writeRegister(BMI270_REG_ACC_CONF, accConf) || !writeRegister(BMI270_REG_ACC_RANGE, BMI270_ACC_RANGE_2G) || + !writeRegister(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_ACC_EN)) + return false; + + delay(50); + initialized = true; + return true; +} + +int32_t BMI270Sensor::runOnce() +{ + if (!initialized) { + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + // Read accelerometer data (6 bytes) + uint8_t data[6]; + if (!readRegisters(BMI270_REG_ACC_X_LSB, data, 6)) { + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + // Convert to 16-bit signed values + int16_t x = (int16_t)((data[1] << 8) | data[0]); + int16_t y = (int16_t)((data[3] << 8) | data[2]); + int16_t z = (int16_t)((data[5] << 8) | data[4]); + + if (!hasBaseline) { + prevX = x; + prevY = y; + prevZ = z; + hasBaseline = true; + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + // Calculate change in acceleration + int16_t deltaX = abs(x - prevX); + int16_t deltaY = abs(y - prevY); + int16_t deltaZ = abs(z - prevZ); + + // Update baseline with low-pass filter + prevX = (prevX * 9 + x) / 10; + prevY = (prevY * 9 + y) / 10; + prevZ = (prevZ * 9 + z) / 10; + + // Check for significant motion (~0.2g at 2g range) + const int16_t threshold = 3200; + if (deltaX > threshold || deltaY > threshold || deltaZ > threshold) { + wakeScreen(); + return 500; + } + + return MOTION_SENSOR_CHECK_INTERVAL_MS; +} + +#endif diff --git a/src/motion/BMI270Sensor.h b/src/motion/BMI270Sensor.h new file mode 100644 index 000000000..7d6cdeaa9 --- /dev/null +++ b/src/motion/BMI270Sensor.h @@ -0,0 +1,36 @@ +#pragma once +#ifndef _BMI270_SENSOR_H_ +#define _BMI270_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMI270) + +class BMI270Sensor : public MotionSensor +{ + private: + bool initialized = false; + TwoWire *wire = nullptr; + + // Previous readings for motion detection + int16_t prevX = 0, prevY = 0, prevZ = 0; + bool hasBaseline = false; + + // BMI270 register access + bool writeRegister(uint8_t reg, uint8_t value); + bool writeRegisters(uint8_t reg, const uint8_t *data, size_t len); + uint8_t readRegister(uint8_t reg); + bool readRegisters(uint8_t reg, uint8_t *data, size_t len); + + // Config file upload (BMI270 requires 8KB config blob) + bool uploadConfigFile(); + + public: + explicit BMI270Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; +}; + +#endif + +#endif diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/LIS3DHSensor.cpp b/src/motion/LIS3DHSensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/LIS3DHSensor.h b/src/motion/LIS3DHSensor.h old mode 100755 new mode 100644 diff --git a/src/motion/LSM6DS3Sensor.cpp b/src/motion/LSM6DS3Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/LSM6DS3Sensor.h b/src/motion/LSM6DS3Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/MPU6050Sensor.cpp b/src/motion/MPU6050Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/MPU6050Sensor.h b/src/motion/MPU6050Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h old mode 100755 new mode 100644 diff --git a/src/motion/STK8XXXSensor.cpp b/src/motion/STK8XXXSensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/STK8XXXSensor.h b/src/motion/STK8XXXSensor.h old mode 100755 new mode 100644 diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 18a4f913e..aba06c210 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -53,6 +53,9 @@ static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for cha static bool isMqttServerAddressPrivate = false; static bool isConnected = false; +static uint32_t lastPositionUnavailableWarning = 0; +static const uint32_t POSITION_UNAVAILABLE_WARNING_INTERVAL_MS = 15000; // 15 seconds + inline void onReceiveProto(char *topic, byte *payload, size_t length) { const DecodedServiceEnvelope e(payload, length); @@ -297,7 +300,9 @@ struct PubSubConfig { if (config.tls_enabled) { serverPort = 8883; } - std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort); + auto [parsedServerAddr, parsedServerPort] = parseHostAndPort(serverAddr.c_str(), serverPort); + serverAddr = std::move(parsedServerAddr); + serverPort = parsedServerPort; } // Defaults @@ -317,8 +322,8 @@ bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &cli pubSub.setClient(client); pubSub.setServer(config.serverAddr.c_str(), config.serverPort); - LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(), - config.serverPort, config.mqttUsername, config.mqttPassword); + LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: ***", config.serverAddr.c_str(), + config.serverPort, config.mqttUsername); // Generate node ID from nodenum for client identification std::string nodeId = nodeDB->getNodeId(); @@ -438,7 +443,8 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) moduleConfig.mqtt.map_report_settings.publish_interval_secs, default_map_publish_interval_secs); } - String host = parseHostAndPort(moduleConfig.mqtt.address).first; + auto [host, parsedPort] = parseHostAndPort(moduleConfig.mqtt.address); + (void)parsedPort; isConfiguredForDefaultServer = isDefaultServer(host); IPAddress ip; isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip); @@ -453,7 +459,9 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) enabled = true; runASAP = true; reconnectCount = 0; +#if !IS_RUNNING_TESTS publishNodeInfo(); +#endif } // preflightSleepObserver.observe(&preflightSleep); } else { @@ -643,22 +651,34 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC if (config.enabled && !config.proxy_to_client_enabled) { #if HAS_NETWORKING - std::unique_ptr clientConnection; if (config.tls_enabled) { -#if MQTT_SUPPORTS_TLS - MQTTClientTLS *tlsClient = new MQTTClientTLS; - clientConnection.reset(tlsClient); - tlsClient->setInsecure(); -#else +#if !MQTT_SUPPORTS_TLS LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); return false; #endif - } else { - clientConnection.reset(new MQTTClient); } - std::unique_ptr pubSub(new PubSubClient); + // Perform a lightweight TCP connectivity check without using connectPubSub(), + // which mutates the module's isConnected state. This only checks if the server + // is reachable — it does not establish an MQTT session. + // Settings are always saved regardless of the result. if (isConnectedToNetwork()) { - return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); + MQTTClient testClient; + if (!testClient.connect(parsed.serverAddr.c_str(), parsed.serverPort)) { + const char *warning = "Could not reach the MQTT server. Settings will be saved, but please verify the server " + "address and credentials."; + LOG_WARN(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + if (cn) { + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; + service->sendClientNotification(cn); + } +#endif + } + testClient.stop(); } #else const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; @@ -754,10 +774,8 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_Me if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { // For uplinking other's packets, check if it's not OK to MQTT or if it's an older packet without the bitfield bool dontUplink = !mp_decoded.decoded.has_bitfield || !(mp_decoded.decoded.bitfield & BITFIELD_OK_TO_MQTT_MASK); - // check for the lowest bit of the data bitfield set false, and the use of one of the default keys. - if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink && - (ch.settings.psk.size < 2 || (ch.settings.psk.size == 16 && memcmp(ch.settings.psk.bytes, defaultpsk, 16)) || - (ch.settings.psk.size == 32 && memcmp(ch.settings.psk.bytes, eventpsk, 32)))) { + // Respect the DontMqttMeBro flag for other nodes' packets on public MQTT servers + if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink) { LOG_INFO("MQTT onSend - Not forwarding packet due to DontMqttMeBro flag"); return; } @@ -847,12 +865,14 @@ void MQTT::perhapsReportToMap() map_position_precision = default_map_position_precision; } - if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs)) + if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs) && last_report_to_map != 0) return; if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { - last_report_to_map = millis(); - LOG_WARN("MQTT Map report enabled, but no position available"); + if (Throttle::isWithinTimespanMs(lastPositionUnavailableWarning, POSITION_UNAVAILABLE_WARNING_INTERVAL_MS) == false) { + LOG_WARN("MQTT Map report enabled, but no position available"); + lastPositionUnavailableWarning = millis(); + } return; } diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index fc1f27ea2..3bb4ce817 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -52,6 +52,20 @@ NimBLEServer *bleServer; static bool passkeyShowing; static std::atomic nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection" +static void clearPairingDisplay() +{ + if (!passkeyShowing) { + return; + } + + passkeyShowing = false; +#if HAS_SCREEN + if (screen) { + screen->endAlert(); + } +#endif +} + class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { /* @@ -630,13 +644,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newStatus); - - // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (passkeyShowing) { - passkeyShowing = false; - if (screen) - screen->endAlert(); - } + clearPairingDisplay(); // Store the connection handle for future use #ifdef NIMBLE_TWO @@ -686,10 +694,14 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks #ifdef NIMBLE_TWO if (ble->isDeInit) return; +#else + if (nimbleBluetooth && nimbleBluetooth->isDeInit) + return; #endif meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); + clearPairingDisplay(); if (bluetoothPhoneAPI) { bluetoothPhoneAPI->close(); @@ -754,11 +766,7 @@ void NimbleBluetooth::deinit() isDeInit = true; #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif + digitalWrite(BLE_LED, LED_STATE_OFF); #endif #ifndef NIMBLE_TWO NimBLEDevice::deinit(); diff --git a/src/platform/esp32/MeshtasticOTA.cpp b/src/platform/esp32/MeshtasticOTA.cpp index 4ca074723..20a3c59cc 100644 --- a/src/platform/esp32/MeshtasticOTA.cpp +++ b/src/platform/esp32/MeshtasticOTA.cpp @@ -75,7 +75,7 @@ bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) return true; } -bool checkOTACapability(esp_app_desc_t *app_desc, uint8_t method) +bool checkOTACapability(const esp_app_desc_t *app_desc, uint8_t method) { // Combined loader supports all (both) transports, BLE and WiFi if (strcmp(app_desc->project_name, combinedAppProjectName) == 0) { diff --git a/src/platform/esp32/MeshtasticOTA.h b/src/platform/esp32/MeshtasticOTA.h index 7c158775f..ce5a7e86b 100644 --- a/src/platform/esp32/MeshtasticOTA.h +++ b/src/platform/esp32/MeshtasticOTA.h @@ -16,7 +16,7 @@ void initialize(); bool isUpdated(); const esp_partition_t *getAppPartition(); bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc); -bool checkOTACapability(esp_app_desc_t *app_desc, uint8_t method); +bool checkOTACapability(const esp_app_desc_t *app_desc, uint8_t method); void recoverConfig(meshtastic_Config_NetworkConfig *network); void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash); bool trySwitchToOTA(); diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 11c9c6ff0..55750592f 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -33,9 +33,6 @@ #ifndef HAS_RADIO #define HAS_RADIO 1 #endif -#ifndef HAS_RTC -#define HAS_RTC 1 -#endif #ifndef HAS_CPU_SHUTDOWN #define HAS_CPU_SHUTDOWN 1 #endif @@ -207,6 +204,8 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 #elif defined(T_WATCH_ULTRA) #define HW_VENDOR meshtastic_HardwareModel_T_WATCH_ULTRA +#elif defined(M5STACK_CARDPUTER_ADV) +#define HW_VENDOR meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV #else #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #endif diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 6667acf5c..25cb30e96 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -24,6 +24,11 @@ #include #include +// Weak empty variant shutdown prep function. +// May be redefined by variant files. +void variant_shutdown() __attribute__((weak)); +void variant_shutdown() {} + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH void setBluetoothEnable(bool enable) { @@ -226,7 +231,9 @@ void cpuDeepSleep(uint32_t msecToWake) #if SOC_RTCIO_HOLD_SUPPORTED && SOC_PM_SUPPORT_EXT_WAKEUP uint64_t gpioMask = (1ULL << (config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); #endif - +#ifdef ALT_BUTTON_WAKE + gpioMask |= (1ULL << BUTTON_PIN_ALT); +#endif #ifdef BUTTON_NEED_PULLUP gpio_pullup_en((gpio_num_t)BUTTON_PIN); #endif @@ -249,6 +256,7 @@ void cpuDeepSleep(uint32_t msecToWake) #endif // #end ESP32S3_WAKE_TYPE #endif + variant_shutdown(); // We want RTC peripherals to stay on esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp index ea5773d30..19c8881ca 100644 --- a/src/platform/extra_variants/t_lora_pager/variant.cpp +++ b/src/platform/extra_variants/t_lora_pager/variant.cpp @@ -23,5 +23,6 @@ void lateInitVariant() cfg.i2s.bits = BIT_LENGTH_16BITS; cfg.i2s.rate = RATE_44K; board.begin(cfg); + board.setVolume(75); // 75% volume } #endif \ No newline at end of file diff --git a/src/platform/nrf52/AsyncUDP.cpp b/src/platform/nrf52/AsyncUDP.cpp index 836fb1307..8c937d71f 100644 --- a/src/platform/nrf52/AsyncUDP.cpp +++ b/src/platform/nrf52/AsyncUDP.cpp @@ -33,7 +33,17 @@ bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t p if (!udp.beginPacket(ip, port)) return false; udp.write(data, len); - return udp.endPacket(); + isSending = true; + bool ok = udp.endPacket(); + isSending = false; + return ok; +} + +void AsyncUDP::close() +{ + udp.stop(); + localPort = 0; + _onPacket = nullptr; } // AsyncUDPPacket @@ -63,7 +73,7 @@ const uint8_t *AsyncUDPPacket::data() int32_t AsyncUDP::runOnce() { - if (_onPacket && udp.parsePacket() > 0) { + if (_onPacket && !isSending && udp.parsePacket() > 0) { AsyncUDPPacket packet(udp); _onPacket(packet); } diff --git a/src/platform/nrf52/AsyncUDP.h b/src/platform/nrf52/AsyncUDP.h index e2b406ba9..eb6083bc4 100644 --- a/src/platform/nrf52/AsyncUDP.h +++ b/src/platform/nrf52/AsyncUDP.h @@ -22,6 +22,7 @@ class AsyncUDP : public Print, private concurrency::OSThread bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64); bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port); + void close(); size_t write(uint8_t b) override; size_t write(const uint8_t *data, size_t len) override; @@ -30,6 +31,7 @@ class AsyncUDP : public Print, private concurrency::OSThread private: EthernetUDP udp; uint16_t localPort; + volatile bool isSending = false; std::function _onPacket; virtual int32_t runOnce() override; }; @@ -37,7 +39,7 @@ class AsyncUDP : public Print, private concurrency::OSThread class AsyncUDPPacket { public: - AsyncUDPPacket(EthernetUDP &source); + explicit AsyncUDPPacket(EthernetUDP &source); IPAddress remoteIP(); uint16_t length(); diff --git a/src/platform/nrf52/BLEDfuScure.cpp b/src/platform/nrf52/BLEDfuSecure.cpp similarity index 99% rename from src/platform/nrf52/BLEDfuScure.cpp rename to src/platform/nrf52/BLEDfuSecure.cpp index 82cb8905a..040df8bdf 100644 --- a/src/platform/nrf52/BLEDfuScure.cpp +++ b/src/platform/nrf52/BLEDfuSecure.cpp @@ -1,6 +1,6 @@ /**************************************************************************/ /*! - @file BLEDfu.cpp + @file BLEDfuSecure.cpp @author hathach (tinyusb.org) @section LICENSE diff --git a/src/platform/nrf52/BLEDfuSecure.h b/src/platform/nrf52/BLEDfuSecure.h index bd5d910e8..dc52d3940 100644 --- a/src/platform/nrf52/BLEDfuSecure.h +++ b/src/platform/nrf52/BLEDfuSecure.h @@ -1,6 +1,6 @@ /**************************************************************************/ /*! - @file BLEDfu.h + @file BLEDfuSecure.h @author hathach (tinyusb.org) @section LICENSE diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 6a552f236..307e35b0c 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -32,6 +32,7 @@ static uint8_t toRadioBytes[meshtastic_ToRadio_size]; static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; static uint16_t connectionHandle; +static bool passkeyShowing; class BluetoothPhoneAPI : public PhoneAPI { @@ -86,6 +87,16 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason) // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); + +#if HAS_SCREEN + // If a pairing prompt is active, make sure we dismiss it on disconnect/cancel/failure paths. + if (passkeyShowing) { + passkeyShowing = false; + if (screen) { + screen->endAlert(); + } + } +#endif } void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { @@ -400,6 +411,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke }); } #endif + passkeyShowing = true; + if (match_request) { uint32_t start_time = millis(); while (millis() < start_time + 30000) { @@ -451,6 +464,7 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus + passkeyShowing = false; if (screen) { screen->endAlert(); } @@ -464,4 +478,4 @@ void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) logRadio.indicate(logMessage, (uint16_t)length); else logRadio.notify(logMessage, (uint16_t)length); -} \ No newline at end of file +} diff --git a/src/platform/nrf52/Nrf52SaadcLock.cpp b/src/platform/nrf52/Nrf52SaadcLock.cpp new file mode 100644 index 000000000..21f4f4dfd --- /dev/null +++ b/src/platform/nrf52/Nrf52SaadcLock.cpp @@ -0,0 +1,13 @@ +#include "Nrf52SaadcLock.h" +#include "concurrency/Lock.h" +#include "configuration.h" + +#ifdef ARCH_NRF52 + +namespace concurrency +{ +static Lock nrf52SaadcLockInstance; +Lock *nrf52SaadcLock = &nrf52SaadcLockInstance; +} // namespace concurrency + +#endif diff --git a/src/platform/nrf52/Nrf52SaadcLock.h b/src/platform/nrf52/Nrf52SaadcLock.h new file mode 100644 index 000000000..77024eea3 --- /dev/null +++ b/src/platform/nrf52/Nrf52SaadcLock.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef ARCH_NRF52 + +namespace concurrency +{ +class Lock; +/** Shared mutex for SAADC configuration and reads (VDD + battery analog path). */ +extern Lock *nrf52SaadcLock; +} // namespace concurrency + +#endif diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index d1965f03e..eafd799fc 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -157,8 +157,8 @@ #endif -#ifdef PIN_LED1 -#define LED_PIN PIN_LED1 // LED1 on nrf52840-DK +#if defined(PIN_LED1) && !defined(LED_POWER) +#define LED_POWER PIN_LED1 // LED1 on nrf52840-DK #endif #ifdef PIN_BUTTON1 diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 0376a1dad..5cf3a4465 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -17,6 +17,7 @@ #include #include // #include +#include "HardwareRNG.h" #include "NodeDB.h" #include "PowerMon.h" #include "error.h" @@ -25,6 +26,8 @@ #include "power.h" #include +#include "Nrf52SaadcLock.h" +#include "concurrency/LockGuard.h" #include #ifdef BQ25703A_ADDR @@ -46,11 +49,15 @@ uint16_t getVDDVoltage(); -// Weak empty variant initialization function. +// Weak empty variant shutdown prep function. // May be redefined by variant files. void variant_shutdown() __attribute__((weak)); void variant_shutdown() {} +// Optional variant hook called each nrf52Loop(); e.g. for low-VDD System OFF. +void variant_nrf52LoopHook(void) __attribute__((weak)); +void variant_nrf52LoopHook(void) {} + static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; @@ -74,11 +81,18 @@ bool powerHAL_isVBUSConnected() bool powerHAL_isPowerLevelSafe() { - static bool powerLevelSafe = true; - uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD * 1000; // convert V to mV - uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000; +#ifdef SAFE_VDD_VOLTAGE_THRESHOLD_MV + uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD_MV; +#else + uint16_t threshold = (uint16_t)(SAFE_VDD_VOLTAGE_THRESHOLD * 1000.0f + 0.5f); // convert V to mV +#endif +#ifdef SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV + uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV; +#else + uint16_t hysteresis = (uint16_t)(SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000.0f + 0.5f); +#endif if (powerLevelSafe) { if (getVDDVoltage() < threshold) { @@ -125,11 +139,12 @@ void powerHAL_platformInit() // get VDD voltage (in millivolts) uint16_t getVDDVoltage() { - // we use the same values as regular battery read so there is no conflict on SAADC + concurrency::LockGuard guard(concurrency::nrf52SaadcLock); + + // Match battery read resolution; SAADC is shared with AnalogBatteryLevel in Power.cpp. analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); // VDD range on NRF52840 is 1.8-3.3V so we need to remap analog reference to 3.6V - // let's hope battery reading runs in same task and we don't have race condition analogReference(AR_INTERNAL); uint16_t vddADCRead = analogReadVDD(); @@ -326,6 +341,8 @@ void nrf52Loop() checkSDEvents(); reportLittleFSCorruptionOnce(); + + variant_nrf52LoopHook(); // Optional variant hook called each nrf52Loop(); } #ifdef USE_SEMIHOSTING @@ -382,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 diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index d0d8ba40f..7833b3603 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -1,4 +1,5 @@ #include "CryptoEngine.h" +#include "HardwareRNG.h" #include "PortduinoGPIO.h" #include "SPIChip.h" #include "mesh/RF95Interface.h" @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -180,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) { @@ -232,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; } @@ -277,6 +285,24 @@ void portduinoSetup() std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" << std::endl; + // check for custom data fields + int i = 0; + while (access(("/proc/device-tree/hat/custom_" + std::to_string(i)).c_str(), R_OK) == 0) { + std::ifstream customFieldFile(("/proc/device-tree/hat/custom_" + std::to_string(i)).c_str()); + if (customFieldFile.is_open()) { + std::string customFieldName; + std::string customFieldValue; + getline(customFieldFile, customFieldName, ' '); + getline(customFieldFile, customFieldValue, ' '); + customFieldFile.close(); + + printf("autoconf: Found hat+ custom field %s: %s\n", customFieldName.c_str(), customFieldValue.c_str()); + portduino_config.hat_plus_custom_fields[customFieldName] = customFieldValue; + } + + i++; + } + // potential TODO: Validate that this is a real UUID std::ifstream hatUUID("/proc/device-tree/hat/uuid"); char uuid[38] = {0}; @@ -493,36 +519,49 @@ 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(time(NULL)); + HardwareRNG::seed(seed); + randomSeed(seed); std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); - for (auto i : portduino_config.all_pins) { - if (i->enabled && i->pin > max_GPIO) + + std::set used_pins; + + for (const auto *i : portduino_config.all_pins) { + if (i->enabled && i->pin > max_GPIO) { max_GPIO = i->pin; + } } for (auto i : portduino_config.extra_pins) { - if (i.enabled && i.pin > max_GPIO) + if (i.enabled && i.pin > max_GPIO) { max_GPIO = i.pin; + } } gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. // Need to bind all the configured GPIO pins so they're not simulated // TODO: If one of these fails, we should log and terminate - for (auto i : portduino_config.all_pins) { + for (const auto *i : portduino_config.all_pins) { // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora // Those GPIO are handled in our usermode driver instead. if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { continue; } if (i->enabled) { - if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) { - printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line); - exit(EXIT_FAILURE); + if (used_pins.find(i->pin) != used_pins.end()) { + printf("Pin %d is in use for multiple purposes\n", i->pin); + } else { + if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) { + printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line); + exit(EXIT_FAILURE); + } + used_pins.insert(i->pin); } } } + printf("Initializing extra pins\n"); for (auto i : portduino_config.extra_pins) { // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora // Those GPIO are handled in our usermode driver instead. @@ -530,13 +569,58 @@ void portduinoSetup() continue; } if (i.enabled) { - if (initGPIOPin(i.pin, gpioChipName + std::to_string(i.gpiochip), i.line) != ERRNO_OK) { - printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i.line); - exit(EXIT_FAILURE); + if (used_pins.find(i.pin) != used_pins.end()) { + printf("Pin %d is in use for multiple purposes\n", i.pin); + } else { + if (initGPIOPin(i.pin, gpioChipName + std::to_string(i.gpiochip), i.line) != ERRNO_OK) { + printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i.line); + exit(EXIT_FAILURE); + } + used_pins.insert(i.pin); } } } + // In one test, this dance seemed necessary to trigger the pin to detect properly. + if (portduino_config.lora_pa_detect_pin.enabled) { + pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLDOWN); + sleep(1); + if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == LOW) { + std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLDOWN is LOW" << std::endl; + } + pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLUP); + sleep(1); + if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == HIGH) { + std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is HIGH, dropping PA curve" << std::endl; + portduino_config.num_pa_points = 1; + portduino_config.tx_gain_lora[0] = 0; + } else { + std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is LOW, using PA curve" << std::endl; + } + + // disable bias once finished + pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT); + } else if (portduino_config.hat_plus_custom_fields.find("io_slot1") != portduino_config.hat_plus_custom_fields.end()) { + printf("Hat+ io_slot1 is %s\n", portduino_config.hat_plus_custom_fields["io_slot1"].c_str()); + if (portduino_config.hat_plus_custom_fields["io_slot1"] != "RAK13302") { + std::cout << "Hat+ io_slot1 is not RAK13302, skipping PA curve" << std::endl; + portduino_config.num_pa_points = 1; + portduino_config.tx_gain_lora[0] = 0; + } + } + + for (auto i : portduino_config.extra_pins) { + // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora + // Those GPIO are handled in our usermode driver instead. + if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { + continue; + } + if (i.enabled && i.default_high) { + pinMode(i.pin, OUTPUT); + digitalWrite(i.pin, HIGH); + } + } + // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { SPI.begin(portduino_config.lora_spi_dev.c_str()); @@ -555,7 +639,9 @@ void portduinoSetup() } } else if (portduino_config.JSONFilename != "") { try { - JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); + if (portduino_config.JSONFileRotate == 0) { + JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); + } } catch (std::ofstream::failure &e) { std::cout << "*** JSONFile Exception " << e.what() << std::endl; exit(EXIT_FAILURE); @@ -568,11 +654,13 @@ void portduinoSetup() if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { portduino_config.logoutputlevel = level_debug; } - + if (portduino_config.lora_spi_dev != "") { + portduinoSetOptions({.realHardware = true}); + } return; } -int initGPIOPin(int pinNum, const std::string gpioChipName, int line) +int initGPIOPin(int pinNum, const std::string &gpioChipName, int line) { #ifdef PORTDUINO_LINUX_HARDWARE std::string gpio_name = "GPIO" + std::to_string(pinNum); @@ -612,6 +700,7 @@ bool loadConfig(const char *configPath) } portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); + portduino_config.JSONFileRotate = yamlConfig["Logging"]["JSONFileRotate"].as(0); portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; @@ -643,7 +732,7 @@ bool loadConfig(const char *configPath) if (yamlConfig["Lora"]) { if (yamlConfig["Lora"]["Module"]) { - for (auto &loraModule : portduino_config.loraModules) { + for (const auto &loraModule : portduino_config.loraModules) { if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) { portduino_config.lora_module = loraModule.first; break; @@ -691,6 +780,17 @@ bool loadConfig(const char *configPath) } } + if (yamlConfig["Lora"]["Enable_Pins"]) { + for (auto extra_pin : yamlConfig["Lora"]["Enable_Pins"]) { + portduino_config.extra_pins.push_back(pinMapping()); + portduino_config.extra_pins.back().config_section = "Lora"; + portduino_config.extra_pins.back().config_name = "Enable_Pins"; + portduino_config.extra_pins.back().enabled = true; + portduino_config.extra_pins.back().default_high = true; + readGPIOFromYaml(extra_pin, portduino_config.extra_pins.back()); + } + } + portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as(2000000); portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as(""); portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as(0x5512); @@ -777,7 +877,7 @@ bool loadConfig(const char *configPath) } if (yamlConfig["Display"]) { - for (auto &screen_name : portduino_config.screen_names) { + for (const auto &screen_name : portduino_config.screen_names) { if (yamlConfig["Display"]["Panel"].as("") == screen_name.second) portduino_config.displayPanel = screen_name.first; } @@ -872,6 +972,7 @@ bool loadConfig(const char *configPath) } if (yamlConfig["Config"]) { + portduino_config.has_config_overrides = true; if (yamlConfig["Config"]["DisplayMode"]) { portduino_config.has_configDisplayMode = true; if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { @@ -884,6 +985,13 @@ bool loadConfig(const char *configPath) portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; } } + if (yamlConfig["Config"]["StatusMessage"]) { + portduino_config.has_statusMessage = true; + portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as(""); + } + if ((yamlConfig["Config"]["EnableUDP"]).as(false)) { + portduino_config.enable_UDP = true; + } } if (yamlConfig["General"]) { @@ -899,7 +1007,7 @@ bool loadConfig(const char *configPath) } if (checkConfigPort) { portduino_config.api_port = (yamlConfig["General"]["APIPort"]).as(-1); - if (portduino_config.api_port != -1 && portduino_config.api_port > 1023 && portduino_config.api_port < 65536) { + if (portduino_config.api_port > 1023 && portduino_config.api_port < 65536) { TCPPort = (portduino_config.api_port); } } diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index af511be6e..b38cfca25 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -52,13 +52,14 @@ struct pinMapping { int gpiochip; int line; bool enabled = false; + bool default_high = false; }; extern std::ofstream traceFile; extern std::ofstream JSONFile; extern Ch341Hal *ch341Hal; -int initGPIOPin(int pinNum, std::string gpioChipname, int line); +int initGPIOPin(int pinNum, const std::string &gpioChipname, int line); bool loadConfig(const char *configPath); static bool ends_with(std::string_view str, std::string_view suffix); void getMacAddr(uint8_t *dmac); @@ -107,6 +108,7 @@ extern struct portduino_config_struct { pinMapping lora_txen_pin = {"Lora", "TXen"}; pinMapping lora_rxen_pin = {"Lora", "RXen"}; pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; + pinMapping lora_pa_detect_pin = {"Lora", "GPIO_DETECT_PA"}; std::vector extra_pins = {}; // GPS @@ -163,6 +165,7 @@ extern struct portduino_config_struct { bool ascii_logs_explicit = false; std::string JSONFilename; + int JSONFileRotate = 0; meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; // Webserver @@ -177,8 +180,12 @@ extern struct portduino_config_struct { int hostMetrics_channel = 0; // config + bool has_config_overrides = false; int configDisplayMode = 0; bool has_configDisplayMode = false; + std::string statusMessage = ""; + bool has_statusMessage = false; + bool enable_UDP = false; // General std::string mac_address = ""; @@ -190,13 +197,16 @@ extern struct portduino_config_struct { int maxtophone = 100; int MaxNodes = 200; - pinMapping *all_pins[20] = {&lora_cs_pin, + std::unordered_map hat_plus_custom_fields; + + pinMapping *all_pins[21] = {&lora_cs_pin, &lora_irq_pin, &lora_busy_pin, &lora_reset_pin, &lora_txen_pin, &lora_rxen_pin, &lora_sx126x_ant_sw_pin, + &lora_pa_detect_pin, &displayDC, &displayCS, &displayBacklight, @@ -220,7 +230,7 @@ extern struct portduino_config_struct { out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module]; - for (auto lora_pin : all_pins) { + for (const auto *lora_pin : all_pins) { if (lora_pin->config_section == "Lora" && lora_pin->enabled) { out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << lora_pin->pin; @@ -251,18 +261,23 @@ extern struct portduino_config_struct { out << YAML::Key << "TX_GAIN_LORA" << YAML::Value << tx_gain_lora[0]; } - out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; + if (dio2_as_rf_switch) + out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; if (dio3_tcxo_voltage != 0) out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000; if (lora_usb_pid != 0x5512) out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid; if (lora_usb_vid != 0x1A86) out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid; - if (lora_spi_dev != "") + if (lora_spi_dev != "" && !(lora_spi_dev == "/dev/spidev0.0" && lora_module == use_autoconf)) { + if (lora_spi_dev.find("/dev/") != std::string::npos) + lora_spi_dev = lora_spi_dev.substr(5); out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev; + } if (lora_usb_serial_num != "") out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num; - out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; + if (spiSpeed != 2000000) + out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; if (rfswitch_dio_pins[0] != RADIOLIB_NC) { out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap; @@ -346,11 +361,11 @@ extern struct portduino_config_struct { // Display if (displayPanel != no_screen) { out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap; - for (auto &screen_name : screen_names) { + for (const auto &screen_name : screen_names) { if (displayPanel == screen_name.first) out << YAML::Key << "Module" << YAML::Value << screen_name.second; } - for (auto display_pin : all_pins) { + for (const auto *display_pin : all_pins) { if (display_pin->config_section == "Display" && display_pin->enabled) { out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << display_pin->pin; @@ -398,7 +413,7 @@ extern struct portduino_config_struct { case ft5x06: out << YAML::Key << "Module" << YAML::Value << "FT5x06"; } - for (auto touchscreen_pin : all_pins) { + for (const auto *touchscreen_pin : all_pins) { if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) { out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin; @@ -421,7 +436,7 @@ extern struct portduino_config_struct { if (pointerDevice != "") out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice; - for (auto input_pin : all_pins) { + for (const auto *input_pin : all_pins) { if (input_pin->config_section == "Input" && input_pin->enabled) { out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << input_pin->pin; @@ -458,6 +473,9 @@ extern struct portduino_config_struct { out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; if (JSONFilename != "") { out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; + if (JSONFileRotate != 0) + out << YAML::Key << "JSONFileRotate" << YAML::Value << JSONFileRotate; + if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) @@ -505,21 +523,30 @@ extern struct portduino_config_struct { } // config - if (has_configDisplayMode) { + if (has_config_overrides) { out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; - switch (configDisplayMode) { - case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: - out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: - out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; - break; + if (has_configDisplayMode) { + + switch (configDisplayMode) { + case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: + out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: + out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; + break; + } + } + if (has_statusMessage) { + out << YAML::Key << "StatusMessage" << YAML::Value << statusMessage; + } + if (enable_UDP) { + out << YAML::Key << "EnableUDP" << YAML::Value << true; } out << YAML::EndMap; // Config diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index 441f75b10..1725763f2 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -29,7 +29,7 @@ class Ch341Hal : public RadioLibHal { public: // default constructor - initializes the base HAL and any needed private members - explicit Ch341Hal(uint8_t spiChannel, std::string serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, + explicit Ch341Hal(uint8_t spiChannel, const std::string &serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0) : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) { diff --git a/src/platform/portduino/architecture.h b/src/platform/portduino/architecture.h index 9ee8ad366..b1698a4eb 100644 --- a/src/platform/portduino/architecture.h +++ b/src/platform/portduino/architecture.h @@ -17,9 +17,6 @@ #ifndef HAS_RADIO #define HAS_RADIO 1 #endif -#ifndef HAS_RTC -#define HAS_RTC 1 -#endif #ifndef HAS_TELEMETRY #define HAS_TELEMETRY 1 #endif diff --git a/src/platform/rp2xx0/main-rp2xx0.cpp b/src/platform/rp2xx0/main-rp2xx0.cpp index 6c73e385a..e59b0a9cd 100644 --- a/src/platform/rp2xx0/main-rp2xx0.cpp +++ b/src/platform/rp2xx0/main-rp2xx0.cpp @@ -1,3 +1,4 @@ +#include "HardwareRNG.h" #include "configuration.h" #include "hardware/xosc.h" #include @@ -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); diff --git a/src/platform/stm32wl/hardfault_handler.s b/src/platform/stm32wl/hardfault_handler.s new file mode 100644 index 000000000..ab76096fa --- /dev/null +++ b/src/platform/stm32wl/hardfault_handler.s @@ -0,0 +1,11 @@ +.globl HardFault_Handler +.syntax unified +.thumb + +.type HardFault_Handler, %function +HardFault_Handler: +tst lr, #4 +ite eq +mrseq r0, msp +mrsne r0, psp +b HardFault_Handler_C \ No newline at end of file diff --git a/src/platform/stm32wl/main-stm32wl.cpp b/src/platform/stm32wl/main-stm32wl.cpp index e841f8f29..241020126 100644 --- a/src/platform/stm32wl/main-stm32wl.cpp +++ b/src/platform/stm32wl/main-stm32wl.cpp @@ -1,8 +1,60 @@ #include "RTC.h" #include "configuration.h" +#include #include #include +// ─── 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() {} @@ -53,4 +105,99 @@ extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr) { return; } -#endif \ No newline at end of file +#endif + +// Taken from https://interrupt.memfault.com/blog/cortex-m-hardfault-debug +typedef struct __attribute__((packed)) ContextStateFrame { + uint32_t r0; + uint32_t r1; + uint32_t r2; + uint32_t r3; + uint32_t r12; + uint32_t lr; + uint32_t return_address; + uint32_t xpsr; +} sContextStateFrame; + +// NOTE: If you are using CMSIS, the registers can also be +// accessed through CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk +#define HALT_IF_DEBUGGING() \ + do { \ + if ((*(volatile uint32_t *)0xE000EDF0) & (1 << 0)) { \ + __asm("bkpt 1"); \ + } \ + } while (0) + +static char hardfault_message_buffer[256]; + +// printf directly using srcwrapper's debug UART function. +static void debug_printf(const char *format, ...) +{ + va_list args; + va_start(args, format); + int length = vsnprintf(hardfault_message_buffer, sizeof(hardfault_message_buffer), format, args); + va_end(args); + + if (length < 0) + return; + uart_debug_write((uint8_t *)hardfault_message_buffer, min((unsigned int)length, sizeof(hardfault_message_buffer) - 1)); +} + +// N picked by guessing +#define DOT_TIME 1200000 +static void dot() +{ + digitalWrite(LED_POWER, LED_STATE_ON); + for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */ + } + digitalWrite(LED_POWER, LED_STATE_OFF); + for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */ + } +} + +static void dash() +{ + digitalWrite(LED_POWER, LED_STATE_ON); + for (volatile int i = 0; i < (DOT_TIME * 3); i++) { /* busy wait */ + } + digitalWrite(LED_POWER, LED_STATE_OFF); + for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */ + } +} + +static void space() +{ + for (volatile int i = 0; i < (DOT_TIME * 3); i++) { /* busy wait */ + } +} + +// Disable optimizations for this function so "frame" argument +// does not get optimized away +extern "C" __attribute__((optimize("O0"))) void HardFault_Handler_C(sContextStateFrame *frame) +{ + debug_printf("HardFault!\r\n"); + debug_printf("r0: %08x\r\n", frame->r0); + debug_printf("r1: %08x\r\n", frame->r1); + debug_printf("r2: %08x\r\n", frame->r2); + debug_printf("r3: %08x\r\n", frame->r3); + debug_printf("r12: %08x\r\n", frame->r12); + debug_printf("lr: %08x\r\n", frame->lr); + debug_printf("pc[return address]: %08x\r\n", frame->return_address); + debug_printf("xpsr: %08x\r\n", frame->xpsr); + + HALT_IF_DEBUGGING(); + + // blink SOS forever + while (1) { + dot(); + dot(); + dot(); + dash(); + dash(); + dash(); + dot(); + dot(); + dot(); + space(); + } +} \ No newline at end of file diff --git a/src/power.h b/src/power.h index e4b456d3b..d46eaadd2 100644 --- a/src/power.h +++ b/src/power.h @@ -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 @@ -103,8 +108,10 @@ class Power : private concurrency::OSThread bool axpChipInit(); /// Setup a simple ADC input based battery sensor bool analogInit(); - /// Setup a Lipo battery level sensor - bool lipoInit(); + /// Setup cw2015 battery level sensor + bool cw2015Init(); + /// Setup a 17048 battery level sensor + bool max17048Init(); /// Setup a Lipo charger bool lipoChargerInit(); /// Setup a meshSolar battery sensor diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index a12972cb0..819ba3da5 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -149,17 +149,23 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.air_quality_metrics.has_pm100_standard) { msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - msgPayload["pm10_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + if (decoded->variant.air_quality_metrics.has_co2) { + msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2); } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - msgPayload["pm25_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); + if (decoded->variant.air_quality_metrics.has_co2_temperature) { + msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature); } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - msgPayload["pm100_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + if (decoded->variant.air_quality_metrics.has_co2_humidity) { + msgPayload["co2_humidity"] = new JSONValue(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); + } + if (decoded->variant.air_quality_metrics.has_form_temperature) { + msgPayload["form_temperature"] = new JSONValue(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); } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { if (decoded->variant.power_metrics.has_ch1_voltage) { diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 41f505b94..bd0a29c51 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -120,14 +120,23 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, 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_pm10_environmental) { - jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; + 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_pm25_environmental) { - jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; + 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_pm100_environmental) { - jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + 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) { diff --git a/src/sleep.cpp b/src/sleep.cpp index 6b06613eb..2af6222f4 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -5,14 +5,15 @@ #endif #include "Default.h" -#include "Led.h" #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" +#include "TransmitHistory.h" #include "detect/LoRaRadioType.h" #include "error.h" #include "main.h" +#include "modules/StatusLEDModule.h" #include "sleep.h" #include "target_specific.h" @@ -238,10 +239,13 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN nodeDB->saveToDisk(); } + // Persist broadcast transmit times so throttle survives reboot + if (transmitHistory) + transmitHistory->saveToDisk(); + #ifdef PIN_POWER_EN digitalWrite(PIN_POWER_EN, LOW); pinMode(PIN_POWER_EN, INPUT); // power off peripherals - // pinMode(PIN_POWER_EN1, INPUT_PULLDOWN); #endif #ifdef RAK_WISMESH_TAP_V2 @@ -269,8 +273,7 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN digitalWrite(PIN_WD_EN, LOW); #endif #endif - ledBlink.set(false); - + statusLEDModule->setPowerLED(false); #ifdef RESET_OLED digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power #endif @@ -426,8 +429,13 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); #endif -#ifdef INPUTDRIVER_ENCODER_BTN - gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL); +#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) || defined(INPUTDRIVER_ENCODER_BTN) +#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) +#define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_TWO_WAY_ROCKER_BTN +#else +#define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_ENCODER_BTN +#endif + gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN, GPIO_INTR_LOW_LEVEL); #endif #if defined(WAKE_ON_TOUCH) gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); @@ -468,8 +476,9 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r // Disable wake-on-button interrupt. Re-attach normal button-interrupts gpio_wakeup_disable(pin); #endif -#if defined(INPUTDRIVER_ENCODER_BTN) - gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_ENCODER_BTN); +#ifdef INPUTDRIVER_WAKE_BTN_PIN + gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN); +#undef INPUTDRIVER_WAKE_BTN_PIN #endif #if defined(WAKE_ON_TOUCH) gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT); @@ -536,7 +545,8 @@ void enableModemSleep() bool shouldLoraWake(uint32_t msecToWake) { - return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER); + return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE); } void enableLoraInterrupt() @@ -557,10 +567,8 @@ void enableLoraInterrupt() gpio_pullup_en((gpio_num_t)LORA_CS); #endif -#if defined(USE_GC1109_PA) - gpio_pullup_en((gpio_num_t)LORA_PA_POWER); - gpio_pullup_en((gpio_num_t)LORA_PA_EN); - gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); +#if HAS_LORA_FEM + loraFEMInterface.setRxModeEnableWhenMCUSleep(); #endif LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); diff --git a/src/watchdog/watchdogThread.cpp b/src/watchdog/watchdogThread.cpp new file mode 100644 index 000000000..3e8a5466f --- /dev/null +++ b/src/watchdog/watchdogThread.cpp @@ -0,0 +1,37 @@ +#include "watchdogThread.h" +#include "configuration.h" + +#ifdef HAS_HARDWARE_WATCHDOG +WatchdogThread *watchdogThread; + +WatchdogThread::WatchdogThread() : OSThread("Watchdog") +{ + setup(); +} + +void WatchdogThread::feedDog(void) +{ + digitalWrite(HARDWARE_WATCHDOG_DONE, HIGH); + delay(1); + digitalWrite(HARDWARE_WATCHDOG_DONE, LOW); +} + +int32_t WatchdogThread::runOnce() +{ + LOG_DEBUG("Feeding hardware watchdog"); + feedDog(); + return HARDWARE_WATCHDOG_TIMEOUT_MS; +} + +bool WatchdogThread::setup() +{ + LOG_DEBUG("init hardware watchdog"); + pinMode(HARDWARE_WATCHDOG_WAKE, INPUT); + pinMode(HARDWARE_WATCHDOG_DONE, OUTPUT); + delay(1); + digitalWrite(HARDWARE_WATCHDOG_DONE, LOW); + delay(1); + feedDog(); + return true; +} +#endif \ No newline at end of file diff --git a/src/watchdog/watchdogThread.h b/src/watchdog/watchdogThread.h new file mode 100644 index 000000000..3a3830aa4 --- /dev/null +++ b/src/watchdog/watchdogThread.h @@ -0,0 +1,17 @@ +#pragma once + +#include "concurrency/OSThread.h" +#include + +#ifdef HAS_HARDWARE_WATCHDOG +class WatchdogThread : private concurrency::OSThread +{ + public: + WatchdogThread(); + void feedDog(void); + virtual bool setup(); + virtual int32_t runOnce() override; +}; + +extern WatchdogThread *watchdogThread; +#endif diff --git a/test/TestUtil.cpp b/test/TestUtil.cpp index b470b8ce8..a8262238f 100644 --- a/test/TestUtil.cpp +++ b/test/TestUtil.cpp @@ -4,6 +4,13 @@ #include "TestUtil.h" +#if defined(ARDUINO) +#include +#else +#include +#include +#endif + void initializeTestEnvironment() { concurrency::hasBeenSetup = true; @@ -15,4 +22,13 @@ void initializeTestEnvironment() perhapsSetRTC(RTCQualityNTP, &tv); #endif concurrency::OSThread::setup(); +} + +void testDelay(unsigned long ms) +{ +#if defined(ARDUINO) + ::delay(ms); +#else + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +#endif } \ No newline at end of file diff --git a/test/TestUtil.h b/test/TestUtil.h index ce021e459..fb634184b 100644 --- a/test/TestUtil.h +++ b/test/TestUtil.h @@ -1,4 +1,7 @@ #pragma once // Initialize testing environment. -void initializeTestEnvironment(); \ No newline at end of file +void initializeTestEnvironment(); + +// Portable delay for tests (Arduino or host). +void testDelay(unsigned long ms); \ No newline at end of file diff --git a/test/test_admin_radio/test_main.cpp b/test/test_admin_radio/test_main.cpp new file mode 100644 index 000000000..9906bb94c --- /dev/null +++ b/test/test_admin_radio/test_main.cpp @@ -0,0 +1,814 @@ +/** + * Tests for the radio configuration validation and clamping functions + * introduced in the radio_interface_cherrypick branch. + * + * Targets: + * 1. getRegion() + * 2. RadioInterface::validateConfigRegion() + * 3. RadioInterface::validateConfigLora() + * 4. RadioInterface::clampConfigLora() + * 5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF) + * 6. Channel spacing calculation (placeholder for future protobuf changes) + */ + +#include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RadioInterface.h" +#include "TestUtil.h" +#include "modules/AdminModule.h" +#include + +#include "meshtastic/config.pb.h" + +class MockMeshService : public MeshService +{ + public: + void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); } +}; + +static MockMeshService *mockMeshService; + +// ----------------------------------------------------------------------- +// getRegion() tests +// ----------------------------------------------------------------------- +extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code); + +static void test_getRegion_returnsCorrectRegion_US() +{ + const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, r->code); + TEST_ASSERT_EQUAL_STRING("US", r->name); +} + +static void test_getRegion_returnsCorrectRegion_EU868() +{ + const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, r->code); + TEST_ASSERT_EQUAL_STRING("EU_868", r->name); +} + +static void test_getRegion_returnsCorrectRegion_LORA24() +{ + const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, r->code); + TEST_ASSERT_TRUE(r->wideLora); +} + +static void test_getRegion_unsetCodeReturnsUnsetEntry() +{ + const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); + TEST_ASSERT_EQUAL_STRING("UNSET", r->name); +} + +static void test_getRegion_unknownCodeFallsToUnset() +{ + // A code not in the table should iterate to the UNSET sentinel + const RegionInfo *r = getRegion((meshtastic_Config_LoRaConfig_RegionCode)255); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); +} + +// ----------------------------------------------------------------------- +// validateConfigRegion() tests +// ----------------------------------------------------------------------- + +static void test_validateConfigRegion_validRegionReturnsTrue() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + + // Ensure owner is not licensed (should not matter for non-licensed-only regions) + devicestate.owner.is_licensed = false; + + TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg)); +} + +static void test_validateConfigRegion_unsetRegionReturnsTrue() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + + devicestate.owner.is_licensed = false; + + // UNSET region has licensedOnly=false, so should pass + TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg)); +} + +// ----------------------------------------------------------------------- +// Shadow tables for testing (preset lists → profiles → regions → lookup) +// ----------------------------------------------------------------------- + +// A minimal preset list with only one entry +static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_SINGLE[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, + MODEM_PRESET_END, +}; + +// A preset list that includes all turbo variants only +static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_TURBO_ONLY[] = { + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, + MODEM_PRESET_END, +}; + +// A restricted list simulating a hypothetical tight-regulation region +static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_RESTRICTED[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, + MODEM_PRESET_END, +}; + +// Mirrors PROFILE_STD but with non-zero spacing/padding for testing +static const RegionProfile TEST_PROFILE_SPACED = { + TEST_PRESETS_SINGLE, + /* spacing */ 0.025f, + /* padding */ 0.010f, + /* audioPermitted */ true, + /* licensedOnly */ false, + /* textThrottle */ 0, + /* positionThrottle */ 0, + /* telemetryThrottle */ 0, + /* overrideSlot */ 0, +}; + +// A licensed-only profile for testing access control +static const RegionProfile TEST_PROFILE_LICENSED = { + TEST_PRESETS_RESTRICTED, + /* spacing */ 0.0f, + /* padding */ 0.0f, + /* audioPermitted */ false, + /* licensedOnly */ true, + /* textThrottle */ 5, + /* positionThrottle */ 10, + /* telemetryThrottle */ 10, + /* overrideSlot */ 3, +}; + +// Turbo-only profile +static const RegionProfile TEST_PROFILE_TURBO = { + TEST_PRESETS_TURBO_ONLY, + /* spacing */ 0.0f, + /* padding */ 0.0f, + /* audioPermitted */ true, + /* licensedOnly */ false, + /* textThrottle */ 0, + /* positionThrottle */ 0, + /* telemetryThrottle */ 0, + /* overrideSlot */ 0, +}; + +static const RegionInfo testRegions[] = { + // A wide US-like region with spacing + padding + {meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_US_SPACED"}, + + // A narrow band simulating tight EU regulation + {meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED, + "TEST_EU_LICENSED"}, + + // A wide-LoRa region with turbo-only presets + {meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO, + "TEST_LORA24_TURBO"}, + + // Sentinel — must be last + {meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_UNSET"}, +}; + +static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code) +{ + const RegionInfo *r = testRegions; + while (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (r->code == code) + return r; + r++; + } + return r; // Returns the UNSET sentinel +} + +// ----------------------------------------------------------------------- +// Shadow table tests +// ----------------------------------------------------------------------- + +static void test_shadowTable_spacedProfileHasNonZeroSpacing() +{ + const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_EQUAL_STRING("TEST_US_SPACED", r->name); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.025f, r->profile->spacing); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.010f, r->profile->padding); +} + +static void test_shadowTable_licensedProfileFlagsCorrect() +{ + const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_TRUE(r->profile->licensedOnly); + TEST_ASSERT_FALSE(r->profile->audioPermitted); + TEST_ASSERT_EQUAL(3, r->profile->overrideSlot); +} + +static void test_shadowTable_presetCountMatchesExpected() +{ + const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_EQUAL(1, spaced->getNumPresets()); + + const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(2, licensed->getNumPresets()); + + const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_EQUAL(2, turbo->getNumPresets()); +} + +static void test_shadowTable_defaultPresetIsFirstInList() +{ + const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, spaced->getDefaultPreset()); + + const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, licensed->getDefaultPreset()); + + const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, turbo->getDefaultPreset()); +} + +static void test_shadowTable_channelSpacingWithPadding() +{ + // Verify channel count when spacing + padding are non-zero + const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora); + float channelSpacing = r->profile->spacing + (r->profile->padding * 2) + (bw / 1000.0f); + + // spacing=0.025, padding=0.010*2=0.020, bw=250kHz=0.250 + // channelSpacing = 0.025 + 0.020 + 0.250 = 0.295 MHz + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.295f, channelSpacing); + + uint32_t numChannels = (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / channelSpacing) + 0.5f); + // (928 - 902 + 0.025) / 0.295 = 88.2 → 88 + TEST_ASSERT_EQUAL_UINT32(88, numChannels); +} + +static void test_shadowTable_turboOnlyOnWideLora() +{ + const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_TRUE(r->wideLora); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, r->getDefaultPreset()); + + // Verify wide-LoRa bandwidth for SHORT_TURBO + float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora); + TEST_ASSERT_FLOAT_WITHIN(0.1f, 1625.0f, bw); // 1625 kHz in wide mode +} + +static void test_shadowTable_unknownCodeFallsToSentinel() +{ + const RegionInfo *r = getTestRegion((meshtastic_Config_LoRaConfig_RegionCode)200); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); + TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name); +} + +// ----------------------------------------------------------------------- +// validateConfigLora() tests +// ----------------------------------------------------------------------- + +static void test_validateConfigLora_validPresetForUS() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_allStdPresetsValidForUS() +{ + meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, + }; + + for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) { + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = stdPresets[i]; + TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for US"); + } +} + +static void test_validateConfigLora_turboPresetsInvalidForEU868() +{ + // EU_868 has PRESETS_EU_868 which excludes SHORT_TURBO and LONG_TURBO + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = true; + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for EU_868"); + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO; + TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_TURBO should be invalid for EU_868"); +} + +static void test_validateConfigLora_validPresetsForEU868() +{ + meshtastic_Config_LoRaConfig_ModemPreset eu868Presets[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, + }; + + for (size_t i = 0; i < sizeof(eu868Presets) / sizeof(eu868Presets[0]); i++) { + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = true; + cfg.modem_preset = eu868Presets[i]; + TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for EU_868"); + } +} + +static void test_validateConfigLora_customBandwidthTooWideForEU868() +{ + // EU_868 spans 869.4 - 869.65 = 0.25 MHz = 250 kHz + // A 500 kHz custom BW should be rejected + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = false; + cfg.bandwidth = 500; + cfg.spread_factor = 11; + cfg.coding_rate = 5; + + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_customBandwidthFitsUS() +{ + // US spans 902 - 928 = 26 MHz, so 250 kHz BW fits easily + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = false; + cfg.bandwidth = 250; + cfg.spread_factor = 11; + cfg.coding_rate = 5; + + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_customBandwidthFitsEU868() +{ + // EU_868 spans 250 kHz, 125 kHz BW should fit + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = false; + cfg.bandwidth = 125; + cfg.spread_factor = 12; + cfg.coding_rate = 8; + + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_bogusPresetRejected() +{ + // A fabricated preset value not in any list should be rejected + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; + + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_unsetRegionOnlyAcceptsLongFast() +{ + // UNSET uses PROFILE_UNDEF which has only LONG_FAST + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + cfg.use_preset = true; + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_FAST should be valid for UNSET"); + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_FAST should be invalid for UNSET"); + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for UNSET"); +} + +static void test_validateConfigLora_allPresetsValidForLORA24() +{ + // LORA_24 uses PROFILE_STD (9 presets) with wideLora=true + meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, + }; + + for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) { + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; + cfg.use_preset = true; + cfg.modem_preset = stdPresets[i]; + TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for LORA_24"); + } +} + +// ----------------------------------------------------------------------- +// clampConfigLora() tests +// ----------------------------------------------------------------------- + +static void test_clampConfigLora_invalidPresetClampedToDefault() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = true; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; // not in EU_868 preset list + + RadioInterface::clampConfigLora(cfg); + + const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), cfg.modem_preset); +} + +static void test_clampConfigLora_validPresetUnchanged() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); +} + +static void test_clampConfigLora_customBwTooWideClampedToDefaultBw() +{ + // EU_868 span is 250kHz. A 500kHz custom BW should be clamped to default preset BW. + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = false; + cfg.bandwidth = 500; + cfg.spread_factor = 11; + cfg.coding_rate = 5; + + RadioInterface::clampConfigLora(cfg); + + const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + float expectedBw = modemPresetToBwKHz(eu868->getDefaultPreset(), eu868->wideLora); + TEST_ASSERT_FLOAT_WITHIN(0.01f, expectedBw, (float)cfg.bandwidth); +} + +static void test_clampConfigLora_customBwValidLeftUnchanged() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = false; + cfg.bandwidth = 125; + cfg.spread_factor = 12; + cfg.coding_rate = 8; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth); +} + +static void test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast() +{ + // UNSET uses PROFILE_UNDEF with only LONG_FAST; any other preset should clamp to it + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + cfg.use_preset = true; + cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); +} + +static void test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault() +{ + // LORA_24 uses PROFILE_STD; a bogus preset should clamp to LONG_FAST (first in PRESETS_STD) + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; + cfg.use_preset = true; + cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; + + RadioInterface::clampConfigLora(cfg); + + const RegionInfo *lora24 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_EQUAL(lora24->getDefaultPreset(), cfg.modem_preset); +} + +// ----------------------------------------------------------------------- +// RegionInfo preset list integrity tests +// ----------------------------------------------------------------------- + +static void test_presetsStd_hasNineEntries() +{ + // PROFILE_STD should have exactly 9 presets + const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_EQUAL(9, us->getNumPresets()); + TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets()); +} + +static void test_presetsEU868_hasSevenEntries() +{ + const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(7, eu->getNumPresets()); + TEST_ASSERT_EQUAL_PTR(PROFILE_EU868.presets, eu->getAvailablePresets()); +} + +static void test_presetsUndef_hasOneEntry() +{ + const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET); + TEST_ASSERT_EQUAL(1, unset->getNumPresets()); + TEST_ASSERT_EQUAL_PTR(PROFILE_UNDEF.presets, unset->getAvailablePresets()); +} + +static void test_defaultPresetIsInAvailablePresets() +{ + // For every region, the defaultPreset must appear in its own availablePresets list + const RegionInfo *r = regions; + while (true) { + bool found = false; + for (size_t i = 0; i < r->getNumPresets(); i++) { + if (r->getAvailablePresets()[i] == r->getDefaultPreset()) { + found = true; + break; + } + } + char msg[80]; + snprintf(msg, sizeof(msg), "Region %s defaultPreset not in availablePresets", r->name); + TEST_ASSERT_TRUE_MESSAGE(found, msg); + + if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + break; // UNSET is the sentinel, stop after it + r++; + } +} + +static void test_regionFieldsAreSane() +{ + // Basic sanity check: all regions have freqEnd > freqStart and a non-null name + const RegionInfo *r = regions; + while (true) { + char msg[80]; + snprintf(msg, sizeof(msg), "Region %s: freqEnd must be > freqStart", r->name); + TEST_ASSERT_TRUE_MESSAGE(r->freqEnd > r->freqStart, msg); + TEST_ASSERT_NOT_NULL(r->name); + TEST_ASSERT_TRUE_MESSAGE(r->getNumPresets() > 0, "numPresets must be > 0"); + TEST_ASSERT_NOT_NULL(r->getAvailablePresets()); + + if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + break; + r++; + } +} + +static void test_onlyLORA24HasWideLora() +{ + // Verify that LORA_24 is the only region with wideLora=true + const RegionInfo *r = regions; + while (true) { + char msg[80]; + if (r->code == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { + snprintf(msg, sizeof(msg), "Region %s should have wideLora=true", r->name); + TEST_ASSERT_TRUE_MESSAGE(r->wideLora, msg); + } else { + snprintf(msg, sizeof(msg), "Region %s should have wideLora=false", r->name); + TEST_ASSERT_FALSE_MESSAGE(r->wideLora, msg); + } + + if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + break; + r++; + } +} + +// ----------------------------------------------------------------------- +// Channel spacing calculation (placeholder for future protobuf updates) +// ----------------------------------------------------------------------- + +static void test_channelSpacingCalculation_US_LONG_FAST() +{ + // Current formula: channelSpacing = spacing + (padding * 2) + (bw / 1000) + // US: spacing=0, padding=0 + // LONG_FAST on non-wide region: bw=250 kHz + // channelSpacing = 0 + 0 + 0.250 = 0.250 MHz + // numChannels = round((928 - 902 + 0) / 0.250) = round(104) = 104 + const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora); + float channelSpacing = us->profile->spacing + (us->profile->padding * 2) + (bw / 1000.0f); + uint32_t numChannels = (uint32_t)(((us->freqEnd - us->freqStart + us->profile->spacing) / channelSpacing) + 0.5f); + + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing); + TEST_ASSERT_EQUAL_UINT32(104, numChannels); +} + +static void test_channelSpacingCalculation_EU868_LONG_FAST() +{ + // EU_868: freqStart=869.4, freqEnd=869.65, spacing=0, padding=0 + // LONG_FAST: bw=250 kHz => channelSpacing = 0.250 MHz + // numChannels = round((0.25 + 0) / 0.250) = 1 + const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, eu->wideLora); + float channelSpacing = eu->profile->spacing + (eu->profile->padding * 2) + (bw / 1000.0f); + uint32_t numChannels = (uint32_t)(((eu->freqEnd - eu->freqStart + eu->profile->spacing) / channelSpacing) + 0.5f); + + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing); + TEST_ASSERT_EQUAL_UINT32(1, numChannels); +} + +// Placeholder: when protobuf region definitions include non-zero padding/spacing, +// add tests here to verify the channel count and frequency calculations. +static void test_channelSpacingCalculation_placeholder() +{ + // TODO: Once protobuf RegionInfo entries have non-zero padding or spacing values, + // verify: + // - Channel count matches expected value for each (region, preset) pair + // - First channel frequency = freqStart + (bw/2000) + padding + // - Nth channel frequency = first + (n * channelSpacing) + // - overrideSlot, when non-zero, forces the channel_num + TEST_PASS_MESSAGE("Placeholder for future channel spacing tests with updated protobuf region fields"); +} + +// ----------------------------------------------------------------------- +// handleSetConfig fromOthers dispatch tests +// ----------------------------------------------------------------------- + +class AdminModuleTestShim : public AdminModule +{ + public: + using AdminModule::handleSetConfig; +}; + +static AdminModuleTestShim *testAdmin; + +static meshtastic_Config makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode region, bool usePreset, + meshtastic_Config_LoRaConfig_ModemPreset preset) +{ + meshtastic_Config c = meshtastic_Config_init_zero; + c.which_payload_variant = meshtastic_Config_lora_tag; + c.payload_variant.lora.region = region; + c.payload_variant.lora.use_preset = usePreset; + c.payload_variant.lora.modem_preset = preset; + return c; +} + +static void test_handleSetConfig_fromOthers_invalidPresetRejected() +{ + // Set up a known-good baseline in the global config + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + initRegion(); + + // Build an admin set_config with an invalid preset for EU_868 + meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); + + testAdmin->handleSetConfig(c, true); // fromOthers = true + + // fromOthers=true: invalid preset should be rejected, old preset preserved + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset); +} + +static void test_handleSetConfig_fromLocal_invalidPresetClamped() +{ + // Set up a known-good baseline + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + initRegion(); + + // Build an admin set_config with an invalid preset for EU_868 + meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); + + testAdmin->handleSetConfig(c, false); // fromOthers = false (local client) + + // fromOthers=false: invalid preset should be clamped to the region's default + const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), config.lora.modem_preset); +} + +static void test_handleSetConfig_fromOthers_validPresetAccepted() +{ + // Set up baseline + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + initRegion(); + + // Build an admin set_config with a valid preset for EU_868 + meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST); + + testAdmin->handleSetConfig(c, true); // fromOthers = true + + // Valid preset should be accepted regardless of fromOthers + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset); +} + +// ----------------------------------------------------------------------- +// Test runner +// ----------------------------------------------------------------------- + +void setUp(void) +{ + mockMeshService = new MockMeshService(); + service = mockMeshService; + testAdmin = new AdminModuleTestShim(); +} +void tearDown(void) +{ + service = nullptr; + delete mockMeshService; + mockMeshService = nullptr; + delete testAdmin; + testAdmin = nullptr; +} + +void setup() +{ + delay(10); + delay(2000); + + initializeTestEnvironment(); + + UNITY_BEGIN(); + + // getRegion() + RUN_TEST(test_getRegion_returnsCorrectRegion_US); + RUN_TEST(test_getRegion_returnsCorrectRegion_EU868); + RUN_TEST(test_getRegion_returnsCorrectRegion_LORA24); + RUN_TEST(test_getRegion_unsetCodeReturnsUnsetEntry); + RUN_TEST(test_getRegion_unknownCodeFallsToUnset); + + // validateConfigRegion() + RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue); + RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue); + + // Shadow table tests + RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing); + RUN_TEST(test_shadowTable_licensedProfileFlagsCorrect); + RUN_TEST(test_shadowTable_presetCountMatchesExpected); + RUN_TEST(test_shadowTable_defaultPresetIsFirstInList); + RUN_TEST(test_shadowTable_channelSpacingWithPadding); + RUN_TEST(test_shadowTable_turboOnlyOnWideLora); + RUN_TEST(test_shadowTable_unknownCodeFallsToSentinel); + + // validateConfigLora() + RUN_TEST(test_validateConfigLora_validPresetForUS); + RUN_TEST(test_validateConfigLora_allStdPresetsValidForUS); + RUN_TEST(test_validateConfigLora_turboPresetsInvalidForEU868); + RUN_TEST(test_validateConfigLora_validPresetsForEU868); + RUN_TEST(test_validateConfigLora_customBandwidthTooWideForEU868); + RUN_TEST(test_validateConfigLora_customBandwidthFitsUS); + RUN_TEST(test_validateConfigLora_customBandwidthFitsEU868); + RUN_TEST(test_validateConfigLora_bogusPresetRejected); + RUN_TEST(test_validateConfigLora_unsetRegionOnlyAcceptsLongFast); + RUN_TEST(test_validateConfigLora_allPresetsValidForLORA24); + + // clampConfigLora() + RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault); + RUN_TEST(test_clampConfigLora_validPresetUnchanged); + RUN_TEST(test_clampConfigLora_customBwTooWideClampedToDefaultBw); + RUN_TEST(test_clampConfigLora_customBwValidLeftUnchanged); + RUN_TEST(test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast); + RUN_TEST(test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault); + + // RegionInfo preset list integrity + RUN_TEST(test_presetsStd_hasNineEntries); + RUN_TEST(test_presetsEU868_hasSevenEntries); + RUN_TEST(test_presetsUndef_hasOneEntry); + RUN_TEST(test_defaultPresetIsInAvailablePresets); + RUN_TEST(test_regionFieldsAreSane); + RUN_TEST(test_onlyLORA24HasWideLora); + + // Channel spacing (current + placeholder) + RUN_TEST(test_channelSpacingCalculation_US_LONG_FAST); + RUN_TEST(test_channelSpacingCalculation_EU868_LONG_FAST); + RUN_TEST(test_channelSpacingCalculation_placeholder); + + // handleSetConfig fromOthers dispatch + RUN_TEST(test_handleSetConfig_fromOthers_invalidPresetRejected); + RUN_TEST(test_handleSetConfig_fromLocal_invalidPresetClamped); + RUN_TEST(test_handleSetConfig_fromOthers_validPresetAccepted); + + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_atak/test_main.cpp b/test/test_atak/test_main.cpp new file mode 100644 index 000000000..84078b300 --- /dev/null +++ b/test/test_atak/test_main.cpp @@ -0,0 +1,216 @@ +#include +#include + +#include "TestUtil.h" +#include "meshUtils.h" + +void setUp(void) +{ + // set stuff up here +} + +void tearDown(void) +{ + // clean stuff up here +} + +/** + * Test normal string without embedded nulls + * Should behave the same as strlen() for regular strings + */ +void test_normal_string(void) +{ + char test_str[32] = "Hello World"; + size_t expected = 11; // strlen("Hello World") + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(expected, result); +} + +/** + * Test empty string + * Should return 0 for empty string + */ +void test_empty_string(void) +{ + char test_str[32] = ""; + size_t expected = 0; + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(expected, result); +} + +/** + * Test string with only trailing nulls + * Common case - string followed by null padding + */ +void test_trailing_nulls(void) +{ + char test_str[32] = {0}; + strcpy(test_str, "Test"); + // test_str is now: "Test\0\0\0\0..." (4 chars + 28 nulls) + size_t expected = 4; + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(expected, result); +} + +/** + * Test string with embedded null byte + * This is the critical bug case - strlen() would truncate at first null + */ +void test_embedded_null(void) +{ + char test_str[32] = {0}; + // Create string "ABC\0XYZ" (embedded null after C) + test_str[0] = 'A'; + test_str[1] = 'B'; + test_str[2] = 'C'; + test_str[3] = '\0'; // embedded null + test_str[4] = 'X'; + test_str[5] = 'Y'; + test_str[6] = 'Z'; + // Rest is already null from initialization + + // strlen would return 3, but pb_string_length should return 7 + size_t strlen_result = strlen(test_str); + size_t pb_result = pb_string_length(test_str, sizeof(test_str)); + + TEST_ASSERT_EQUAL_size_t(3, strlen_result); // strlen stops at first null + TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds last non-null +} + +/** + * Test Android UID with embedded null bytes + * Real-world case from bug report: ANDROID-e7e455b40002429d + * The "00" in the UID represents 0x00 bytes that were truncating the string + */ +void test_android_uid_pattern(void) +{ + char test_str[32] = {0}; + // Simulate "ANDROID-e7e455b4" + 0x00 + 0x00 + "2429d" + const char part1[] = "ANDROID-e7e455b4"; + strcpy(test_str, part1); + size_t pos = strlen(part1); + test_str[pos] = '\0'; // embedded null + test_str[pos + 1] = '\0'; // another embedded null + strcpy(test_str + pos + 2, "2429d"); + + // The full UID should be 24 characters + size_t strlen_result = strlen(test_str); + size_t pb_result = pb_string_length(test_str, sizeof(test_str)); + + TEST_ASSERT_EQUAL_size_t(16, strlen_result); // strlen truncates to "ANDROID-e7e455b4" + TEST_ASSERT_EQUAL_size_t(23, pb_result); // pb_string_length gets full length +} + +/** + * Test string with multiple embedded nulls + * Edge case with several null bytes scattered through the string + */ +void test_multiple_embedded_nulls(void) +{ + char test_str[32] = {0}; + // Create "A\0B\0C\0D" (3 embedded nulls) + test_str[0] = 'A'; + test_str[1] = '\0'; + test_str[2] = 'B'; + test_str[3] = '\0'; + test_str[4] = 'C'; + test_str[5] = '\0'; + test_str[6] = 'D'; + + size_t strlen_result = strlen(test_str); + size_t pb_result = pb_string_length(test_str, sizeof(test_str)); + + TEST_ASSERT_EQUAL_size_t(1, strlen_result); // strlen stops at first null + TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds all chars +} + +/** + * Test buffer completely filled with non-null characters + * Edge case where string uses entire buffer + */ +void test_full_buffer(void) +{ + char test_str[8]; + // Fill entire buffer with 'X' + memset(test_str, 'X', sizeof(test_str)); + + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(8, result); +} + +/** + * Test buffer with all nulls + * Should return 0 + */ +void test_all_nulls(void) +{ + char test_str[32] = {0}; + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(0, result); +} + +/** + * Test single character followed by nulls + * Minimal non-empty case + */ +void test_single_char(void) +{ + char test_str[32] = {0}; + test_str[0] = 'X'; + + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(1, result); +} + +/** + * Test callsign field typical size + * Test with typical ATAK callsign field size (64 bytes) + */ +void test_callsign_field_size(void) +{ + char test_str[64] = {0}; + strcpy(test_str, "CALLSIGN-123"); + + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(12, result); +} + +/** + * Test with data at end of buffer + * String with embedded null and data at very end + */ +void test_data_at_buffer_end(void) +{ + char test_str[10] = {0}; + test_str[0] = 'A'; + test_str[1] = '\0'; + test_str[8] = 'Z'; // Data near end + test_str[9] = 'X'; // Data at end + + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(10, result); // Should find the 'X' at position 9 +} + +void setup() +{ + // NOTE!!! Wait for >2 secs + // if board doesn't support software reset via Serial.DTR/RTS + testDelay(10); + testDelay(2000); + + UNITY_BEGIN(); + RUN_TEST(test_normal_string); + RUN_TEST(test_empty_string); + RUN_TEST(test_trailing_nulls); + RUN_TEST(test_embedded_null); + RUN_TEST(test_android_uid_pattern); + RUN_TEST(test_multiple_embedded_nulls); + RUN_TEST(test_full_buffer); + RUN_TEST(test_all_nulls); + RUN_TEST(test_single_char); + RUN_TEST(test_callsign_field_size); + RUN_TEST(test_data_at_buffer_end); + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_default/test_main.cpp b/test/test_default/test_main.cpp new file mode 100644 index 000000000..9da367897 --- /dev/null +++ b/test/test_default/test_main.cpp @@ -0,0 +1,146 @@ +// Unit tests for Default::getConfiguredOrDefaultMsScaled +#include "Default.h" +#include "MeshRadio.h" +#include "TestUtil.h" +#include "meshUtils.h" +#include + +// Helper to compute expected ms using same logic as Default::congestionScalingCoefficient +static uint32_t computeExpectedMs(uint32_t defaultSeconds, uint32_t numOnlineNodes) +{ + uint32_t baseMs = Default::getConfiguredOrDefaultMs(0, defaultSeconds); + + // Routers (including ROUTER_LATE) don't scale + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + return baseMs; + } + + // Sensors and trackers don't scale + if ((config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) || + (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER)) { + return baseMs; + } + + if (numOnlineNodes <= 40) { + return baseMs; + } + + float bwKHz = + config.lora.use_preset ? modemPresetToBwKHz(config.lora.modem_preset, false) : bwCodeToKHz(config.lora.bandwidth); + + uint8_t sf = config.lora.spread_factor; + if (sf < 7) + sf = 7; + else if (sf > 12) + sf = 12; + + float throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 100.0f); +#if USERPREFS_EVENT_MODE + throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 25.0f); +#endif + + int nodesOverForty = (numOnlineNodes - 40); + float coeff = 1.0f + (nodesOverForty * throttlingFactor); + return static_cast(baseMs * coeff + 0.5f); +} + +void test_router_no_scaling() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; + // set some sane lora config so bootstrap paths are deterministic + config.lora.use_preset = false; + config.lora.spread_factor = 9; + config.lora.bandwidth = 250; + + uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 100); + uint32_t expected = computeExpectedMs(60, 100); + TEST_ASSERT_EQUAL_UINT32(expected, res); +} + +void test_client_below_threshold() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.use_preset = false; + config.lora.spread_factor = 9; + config.lora.bandwidth = 250; + + uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 40); + uint32_t expected = computeExpectedMs(60, 40); + TEST_ASSERT_EQUAL_UINT32(expected, res); +} + +void test_client_default_preset_scaling() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.use_preset = false; + config.lora.spread_factor = 9; // SF9 + config.lora.bandwidth = 250; // 250 kHz + + uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 50); + uint32_t expected = computeExpectedMs(60, 50); // nodesOverForty = 10 + TEST_ASSERT_EQUAL_UINT32(expected, res); +} + +void test_client_medium_fast_preset_scaling() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + // nodesOverForty = 30 -> test with nodes=70 + uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 70); + uint32_t expected = computeExpectedMs(60, 70); + // Allow ±1 ms tolerance for floating-point rounding + TEST_ASSERT_INT_WITHIN(1, expected, res); +} + +void test_router_uses_router_minimums() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; + + uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); + uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); + + TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, telemetry); + TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, position); +} + +void test_router_late_uses_router_minimums() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER_LATE; + + uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); + uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); + + TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, telemetry); + TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, position); +} + +void test_client_uses_public_channel_minimums() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + + uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); + uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); + + TEST_ASSERT_EQUAL_UINT32(30 * 60, telemetry); + TEST_ASSERT_EQUAL_UINT32(60 * 60, position); +} + +void setup() +{ + // Small delay to match other test mains + delay(10); + initializeTestEnvironment(); + UNITY_BEGIN(); + RUN_TEST(test_router_no_scaling); + RUN_TEST(test_client_below_threshold); + RUN_TEST(test_client_default_preset_scaling); + RUN_TEST(test_client_medium_fast_preset_scaling); + RUN_TEST(test_router_uses_router_minimums); + RUN_TEST(test_router_late_uses_router_minimums); + RUN_TEST(test_client_uses_public_channel_minimums); + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_http_content_handler/test_main.cpp b/test/test_http_content_handler/test_main.cpp new file mode 100644 index 000000000..af0a41cef --- /dev/null +++ b/test/test_http_content_handler/test_main.cpp @@ -0,0 +1,19 @@ +#include "TestUtil.h" +#include + +static void test_placeholder() +{ + TEST_ASSERT_TRUE(true); +} + +extern "C" { +void setup() +{ + initializeTestEnvironment(); + UNITY_BEGIN(); + RUN_TEST(test_placeholder); + exit(UNITY_END()); +} + +void loop() {} +} diff --git a/test/test_mesh_module/test_main.cpp b/test/test_mesh_module/test_main.cpp new file mode 100644 index 000000000..ec7b1808e --- /dev/null +++ b/test/test_mesh_module/test_main.cpp @@ -0,0 +1,116 @@ +#include "MeshModule.h" +#include "MeshTypes.h" +#include "TestUtil.h" +#include + +// Minimal concrete subclass for testing the base class helper +class TestModule : public MeshModule +{ + public: + TestModule() : MeshModule("TestModule") {} + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } + using MeshModule::currentRequest; + using MeshModule::isMultiHopBroadcastRequest; +}; + +static TestModule *testModule; +static meshtastic_MeshPacket testPacket; + +void setUp(void) +{ + testModule = new TestModule(); + memset(&testPacket, 0, sizeof(testPacket)); + TestModule::currentRequest = &testPacket; +} + +void tearDown(void) +{ + TestModule::currentRequest = NULL; + delete testModule; +} + +// Zero-hop broadcast (hop_limit == hop_start): should be allowed +static void test_zeroHopBroadcast_isAllowed() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 3; + testPacket.hop_limit = 3; // Not yet relayed + + TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); +} + +// Multi-hop broadcast (hop_limit < hop_start): should be blocked +static void test_multiHopBroadcast_isBlocked() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 7; + testPacket.hop_limit = 4; // Already relayed 3 hops + + TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); +} + +// Direct message (not broadcast): should always be allowed regardless of hops +static void test_directMessage_isAllowed() +{ + testPacket.to = 0x12345678; // Specific node + testPacket.hop_start = 7; + testPacket.hop_limit = 4; + + TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); +} + +// Broadcast with hop_limit == 0 (fully relayed): should be blocked +static void test_fullyRelayedBroadcast_isBlocked() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 3; + testPacket.hop_limit = 0; + + TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); +} + +// No current request: should not crash, should return false +static void test_noCurrentRequest_isAllowed() +{ + TestModule::currentRequest = NULL; + + TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); +} + +// Broadcast with hop_start == 0 (legacy or local): should be allowed +static void test_legacyPacket_zeroHopStart_isAllowed() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 0; + testPacket.hop_limit = 0; + + // hop_limit == hop_start, so not multi-hop + TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); +} + +// Single hop relayed broadcast (hop_limit = hop_start - 1): should be blocked +static void test_singleHopRelayedBroadcast_isBlocked() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 3; + testPacket.hop_limit = 2; + + TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); +} + +void setup() +{ + initializeTestEnvironment(); + + UNITY_BEGIN(); + RUN_TEST(test_zeroHopBroadcast_isAllowed); + RUN_TEST(test_multiHopBroadcast_isBlocked); + RUN_TEST(test_directMessage_isAllowed); + RUN_TEST(test_fullyRelayedBroadcast_isBlocked); + RUN_TEST(test_noCurrentRequest_isAllowed); + RUN_TEST(test_legacyPacket_zeroHopStart_isAllowed); + RUN_TEST(test_singleHopRelayedBroadcast_isBlocked); + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index a566dabf7..edf9a3983 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -289,13 +289,23 @@ class MQTTUnitTest : public MQTT mqtt = unitTest = new MQTTUnitTest(); mqtt->start(); + auto clearStartupOutput = []() { + pubsub->published_.clear(); + if (mockMeshService != nullptr) { + mockMeshService->messages_.clear(); + mockMeshService->notifications_.clear(); + } + }; + if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) { loopUntil([] { return true; }); // Loop once + clearStartupOutput(); return; } // Wait for MQTT to subscribe to all topics. TEST_ASSERT_TRUE(loopUntil( [] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); })); + clearStartupOutput(); } PubSubClient &getPubSub() { return pubSub; } }; @@ -334,7 +344,7 @@ void setUp(void) owner = meshtastic_User{.id = "!12345678"}; myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic localPosition = - meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7}; + meshtastic_Position{.has_latitude_i = true, .latitude_i = 700000000, .has_longitude_i = true, .longitude_i = 300000000}; router = mockRouter = new MockRouter(); service = mockMeshService = new MockMeshService(); @@ -808,16 +818,13 @@ void test_configEmptyIsValid(void) TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } -// Empty 'enabled' configuration is valid. +// Empty 'enabled' configuration is valid. A lightweight TCP check may be performed +// but does not affect the result. void test_configEnabledEmptyIsValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; - MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str()); - TEST_ASSERT_EQUAL(1883, client.port_); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Configuration with the default server is valid. @@ -836,38 +843,32 @@ void test_configWithDefaultServerAndInvalidPort(void) TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } -// isValidConfig connects to a custom host and port. +// Custom host and port is valid. TCP reachability is checked but does not block saving. void test_configCustomHostAndPort(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; - MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str()); - TEST_ASSERT_EQUAL(1234, client.port_); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } -// isValidConfig returns false if a connection cannot be established. -void test_configWithConnectionFailure(void) +// An unreachable server is still a valid config — settings always save. +// A warning notification is sent in non-test builds, but isValidConfig returns true. +void test_configWithUnreachableServerIsStillValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; - MockPubSubServer client; - client.refuseConnection_ = true; - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // isValidConfig returns true when tls_enabled is supported, or false otherwise. void test_configWithTLSEnabled(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; - MockPubSubServer client; #if MQTT_SUPPORTS_TLS - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); #else - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); #endif } @@ -917,7 +918,7 @@ void setup() RUN_TEST(test_configWithDefaultServer); RUN_TEST(test_configWithDefaultServerAndInvalidPort); RUN_TEST(test_configCustomHostAndPort); - RUN_TEST(test_configWithConnectionFailure); + RUN_TEST(test_configWithUnreachableServerIsStillValid); RUN_TEST(test_configWithTLSEnabled); exit(UNITY_END()); } @@ -930,4 +931,4 @@ void setup() UNITY_END(); } #endif -void loop() {} \ No newline at end of file +void loop() {} diff --git a/test/test_radio/test_main.cpp b/test/test_radio/test_main.cpp index fbe2b1b13..a7d3d32d2 100644 --- a/test/test_radio/test_main.cpp +++ b/test/test_radio/test_main.cpp @@ -1,10 +1,36 @@ #include "MeshRadio.h" +#include "MeshService.h" #include "RadioInterface.h" #include "TestUtil.h" #include #include "meshtastic/config.pb.h" +class MockMeshService : public MeshService +{ + public: + void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); } +}; + +static MockMeshService *mockMeshService; + +// Test shim to expose protected radio parameters set by applyModemConfig() +class TestableRadioInterface : public RadioInterface +{ + public: + TestableRadioInterface() : RadioInterface() {} + uint8_t getCr() const { return cr; } + uint8_t getSf() const { return sf; } + float getBw() const { return bw; } + + // Override reconfigure to call the base which invokes applyModemConfig() + bool reconfigure() override { return RadioInterface::reconfigure(); } + + // Stubs for pure virtual methods required by RadioInterface + uint32_t getPacketTime(uint32_t, bool) override { return 0; } + ErrorCode send(meshtastic_MeshPacket *p) override { return ERRNO_OK; } +}; + static void test_bwCodeToKHz_specialMappings() { TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31)); @@ -21,7 +47,7 @@ static void test_bwCodeToKHz_passthrough() TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250)); } -static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse() +static void test_validateConfigLora_noopWhenUsePresetFalse() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = false; @@ -30,55 +56,152 @@ static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse() cfg.bandwidth = 123; cfg.spread_factor = 8; - RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + RadioInterface::validateConfigLora(cfg); TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth); TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); } -static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion() +static void test_validateConfigLora_validPreset_nonWideRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; - RadioInterface::bootstrapLoRaConfigFromPreset(cfg); - - TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth); - TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor); + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } -static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion() +static void test_validateConfigLora_validPreset_wideRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; - RadioInterface::bootstrapLoRaConfigFromPreset(cfg); - - TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth); - TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor); + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } -static void test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan() +static void test_validateConfigLora_rejectsInvalidPresetForRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; - RadioInterface::bootstrapLoRaConfigFromPreset(cfg); - - TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); - TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth); - TEST_ASSERT_EQUAL_UINT32(11, cfg.spread_factor); + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); } -void setUp(void) {} -void tearDown(void) {} +static void test_clampConfigLora_invalidPresetClampedToDefault() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); +} + +static void test_clampConfigLora_validPresetUnchanged() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); +} + +// ----------------------------------------------------------------------- +// applyModemConfig() coding rate tests (via reconfigure) +// ----------------------------------------------------------------------- + +static TestableRadioInterface *testRadio; + +// After fresh flash: coding_rate=0, use_preset=true, modem_preset=LONG_FAST +// CR should come from the preset (5 for LONG_FAST), not from the zero default. +static void test_applyModemConfig_freshFlashCodingRateNotZero() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + // coding_rate is 0 (default after init_zero, same as fresh flash) + + testRadio->reconfigure(); + + // LONG_FAST preset has cr=5; must never be 0 + TEST_ASSERT_EQUAL_UINT8(5, testRadio->getCr()); + TEST_ASSERT_EQUAL_UINT8(11, testRadio->getSf()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 250.0f, testRadio->getBw()); +} + +// When coding_rate matches the preset exactly, should still use the preset value +static void test_applyModemConfig_codingRateMatchesPreset() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW; + config.lora.coding_rate = 8; // LONG_SLOW default is cr=8 + + testRadio->reconfigure(); + + TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr()); +} + +// Custom CR higher than preset should be used +static void test_applyModemConfig_customCodingRateHigherThanPreset() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + config.lora.coding_rate = 7; // LONG_FAST preset has cr=5, 7 > 5 + + testRadio->reconfigure(); + + TEST_ASSERT_EQUAL_UINT8(7, testRadio->getCr()); +} + +// Custom CR lower than preset: preset wins (higher is more robust) +static void test_applyModemConfig_customCodingRateLowerThanPreset() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW; + config.lora.coding_rate = 5; // LONG_SLOW preset has cr=8, 5 < 8 + + testRadio->reconfigure(); + + TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr()); +} + +void setUp(void) +{ + mockMeshService = new MockMeshService(); + service = mockMeshService; + + // RadioInterface computes slotTimeMsec during construction and expects myRegion to be valid. + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + initRegion(); + + testRadio = new TestableRadioInterface(); +} +void tearDown(void) +{ + delete testRadio; + testRadio = nullptr; + service = nullptr; + delete mockMeshService; + mockMeshService = nullptr; +} void setup() { @@ -90,10 +213,16 @@ void setup() UNITY_BEGIN(); RUN_TEST(test_bwCodeToKHz_specialMappings); RUN_TEST(test_bwCodeToKHz_passthrough); - RUN_TEST(test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse); - RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion); - RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion); - RUN_TEST(test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan); + RUN_TEST(test_validateConfigLora_noopWhenUsePresetFalse); + RUN_TEST(test_validateConfigLora_validPreset_nonWideRegion); + RUN_TEST(test_validateConfigLora_validPreset_wideRegion); + RUN_TEST(test_validateConfigLora_rejectsInvalidPresetForRegion); + RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault); + RUN_TEST(test_clampConfigLora_validPresetUnchanged); + RUN_TEST(test_applyModemConfig_freshFlashCodingRateNotZero); + RUN_TEST(test_applyModemConfig_codingRateMatchesPreset); + RUN_TEST(test_applyModemConfig_customCodingRateHigherThanPreset); + RUN_TEST(test_applyModemConfig_customCodingRateLowerThanPreset); exit(UNITY_END()); } diff --git a/test/test_traffic_management/test_main.cpp b/test/test_traffic_management/test_main.cpp new file mode 100644 index 000000000..ec54f2312 --- /dev/null +++ b/test/test_traffic_management/test_main.cpp @@ -0,0 +1,1160 @@ +#include "TestUtil.h" +#include + +#if defined(ARCH_PORTDUINO) +#define TM_TEST_ENTRY extern "C" +#else +#define TM_TEST_ENTRY +#endif + +#if HAS_TRAFFIC_MANAGEMENT + +#include "mesh/CryptoEngine.h" +#include "mesh/MeshService.h" +#include "mesh/NodeDB.h" +#include "mesh/Router.h" +#include "modules/TrafficManagementModule.h" +#include +#include +#include +#include +#include + +namespace +{ + +constexpr NodeNum kLocalNode = 0x11111111; +constexpr NodeNum kRemoteNode = 0x22222222; +constexpr NodeNum kTargetNode = 0x33333333; + +class MockNodeDB : public NodeDB +{ + public: + meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override + { + if (hasCachedNode && n == cachedNodeNum) + return &cachedNode; + return NodeDB::getMeshNode(n); + } + + void clearCachedNode() + { + hasCachedNode = false; + cachedNodeNum = 0; + cachedNode = meshtastic_NodeInfoLite_init_zero; + } + + void setCachedNode(NodeNum n) + { + clearCachedNode(); + hasCachedNode = true; + cachedNodeNum = n; + cachedNode.num = n; + cachedNode.has_user = true; + } + + private: + bool hasCachedNode = false; + NodeNum cachedNodeNum = 0; + meshtastic_NodeInfoLite cachedNode = meshtastic_NodeInfoLite_init_zero; +}; + +class MockRadioInterface : public RadioInterface +{ + public: + ErrorCode send(meshtastic_MeshPacket *p) override + { + packetPool.release(p); + return ERRNO_OK; + } + + uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override + { + (void)totalPacketLen; + (void)received; + return 0; + } +}; + +class MockRouter : public Router +{ + public: + ~MockRouter() + { + // Router allocates a global crypt lock in its constructor. + // Clean it up here so each test can build a fresh mock router. + delete cryptLock; + cryptLock = nullptr; + } + + ErrorCode send(meshtastic_MeshPacket *p) override + { + sentPackets.push_back(*p); + packetPool.release(p); + return ERRNO_OK; + } + + std::vector sentPackets; +}; + +class TrafficManagementModuleTestShim : public TrafficManagementModule +{ + public: + using TrafficManagementModule::alterReceived; + using TrafficManagementModule::handleReceived; + using TrafficManagementModule::resetEpoch; + using TrafficManagementModule::runOnce; + + bool ignoreRequestFlag() const { return ignoreRequest; } +}; + +MockNodeDB *mockNodeDB = nullptr; + +static void resetTrafficConfig() +{ + moduleConfig = meshtastic_LocalModuleConfig_init_zero; + moduleConfig.has_traffic_management = true; + moduleConfig.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero; + moduleConfig.traffic_management.enabled = true; + + config = meshtastic_LocalConfig_init_zero; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + + myNodeInfo.my_node_num = kLocalNode; + + router = nullptr; + service = nullptr; + + mockNodeDB->resetNodes(); + mockNodeDB->clearCachedNode(); + nodeDB = mockNodeDB; +} + +static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum port, NodeNum from, NodeNum to = NODENUM_BROADCAST) +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + packet.from = from; + packet.to = to; + packet.id = 0x1001; + packet.channel = 0; + packet.hop_start = 3; + packet.hop_limit = 3; + packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + packet.decoded.portnum = port; + packet.decoded.has_bitfield = true; + packet.decoded.bitfield = 0; + return packet; +} + +static meshtastic_MeshPacket makeUnknownPacket(NodeNum from, NodeNum to = NODENUM_BROADCAST) +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + packet.from = from; + packet.to = to; + packet.id = 0x2001; + packet.channel = 0; + packet.hop_start = 3; + packet.hop_limit = 3; + packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + packet.encrypted.size = 0; + return packet; +} + +static meshtastic_MeshPacket makePositionPacket(NodeNum from, int32_t lat, int32_t lon, NodeNum to = NODENUM_BROADCAST) +{ + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, from, to); + meshtastic_Position pos = meshtastic_Position_init_zero; + pos.has_latitude_i = true; + pos.has_longitude_i = true; + pos.latitude_i = lat; + pos.longitude_i = lon; + + packet.decoded.payload.size = + pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_Position_msg, &pos); + return packet; +} + +static meshtastic_MeshPacket makeNodeInfoPacket(NodeNum from, const char *longName, const char *shortName) +{ + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, from, NODENUM_BROADCAST); + + meshtastic_User user = meshtastic_User_init_zero; + snprintf(user.id, sizeof(user.id), "!%08x", from); + strncpy(user.long_name, longName, sizeof(user.long_name) - 1); + strncpy(user.short_name, shortName, sizeof(user.short_name) - 1); + + packet.decoded.payload.size = + pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_User_msg, &user); + return packet; +} + +/** + * Verify the module is a no-op when traffic management is disabled. + * Important so config toggles cannot accidentally change routing behavior. + */ +static void test_tm_moduleDisabled_doesNothing(void) +{ + moduleConfig.has_traffic_management = false; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + + ProcessMessage result = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_EQUAL_UINT32(0, stats.packets_inspected); + TEST_ASSERT_EQUAL_UINT32(0, stats.unknown_packet_drops); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); +} + +/** + * Verify unknown-packet dropping uses N+1 threshold semantics. + * Important to catch off-by-one regressions in drop decisions. + */ +static void test_tm_unknownPackets_dropOnNPlusOne(void) +{ + moduleConfig.traffic_management.drop_unknown_enabled = true; + moduleConfig.traffic_management.unknown_packet_threshold = 2; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); + + ProcessMessage r1 = module.handleReceived(packet); + ProcessMessage r2 = module.handleReceived(packet); + ProcessMessage r3 = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); + TEST_ASSERT_EQUAL_UINT32(3, stats.packets_inspected); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); +} + +/** + * Verify duplicate position broadcasts inside the dedup window are dropped. + * Important because this is the primary airtime-saving behavior. + */ +static void test_tm_positionDedup_dropsDuplicateWithinWindow(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_GREATER_THAN_UINT32(0, first.decoded.payload.size); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); +} + +/** + * Verify changed coordinates are forwarded even with dedup enabled. + * Important so real movement updates are never suppressed as duplicates. + */ +static void test_tm_positionDedup_allowsMovedPosition(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket moved = makePositionPacket(kRemoteNode, 384221234, -1210845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(moved); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify rate limiting drops only after exceeding the configured threshold. + * Important to protect threshold semantics from off-by-one regressions. + */ +static void test_tm_rateLimit_dropsOnlyAfterThreshold(void) +{ + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 3; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + + ProcessMessage r1 = module.handleReceived(packet); + ProcessMessage r2 = module.handleReceived(packet); + ProcessMessage r3 = module.handleReceived(packet); + ProcessMessage r4 = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r4)); + TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); +} + +/** + * Verify routing/admin traffic is exempt from rate limiting. + * Important because throttling control traffic can destabilize the mesh. + */ +static void test_tm_rateLimit_skipsRoutingAndAdminPorts(void) +{ + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 1; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket routingPacket = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode); + meshtastic_MeshPacket adminPacket = makeDecodedPacket(meshtastic_PortNum_ADMIN_APP, kRemoteNode); + + for (int i = 0; i < 4; i++) { + ProcessMessage rr = module.handleReceived(routingPacket); + ProcessMessage ar = module.handleReceived(adminPacket); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(rr)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(ar)); + } + + meshtastic_TrafficManagementStats stats = module.getStats(); + TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); +} + +/** + * Verify packets sourced from this node bypass dedup and rate limiting. + * Important so local transmissions are not accidentally self-throttled. + */ +static void test_tm_fromUs_bypassesPositionAndRateFilters(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 1; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket positionPacket = makePositionPacket(kLocalNode, 374221234, -1220845678); + meshtastic_MeshPacket textPacket = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode); + + ProcessMessage p1 = module.handleReceived(positionPacket); + ProcessMessage p2 = module.handleReceived(positionPacket); + ProcessMessage t1 = module.handleReceived(textPacket); + ProcessMessage t2 = module.handleReceived(textPacket); + + meshtastic_TrafficManagementStats stats = module.getStats(); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); + TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); +} + +/** + * Verify locally addressed packets are never dropped by transit shaping. + * Important so dedup/rate limiting do not suppress end-user delivery. + */ +static void test_tm_localDestination_bypassesTransitFilters(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 1; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket position1 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode); + meshtastic_MeshPacket position2 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode); + meshtastic_MeshPacket text1 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode); + meshtastic_MeshPacket text2 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode); + + ProcessMessage p1 = module.handleReceived(position1); + ProcessMessage p2 = module.handleReceived(position2); + ProcessMessage t1 = module.handleReceived(text1); + ProcessMessage t2 = module.handleReceived(text2); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); + TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); +} + +/** + * Verify router role clamps NodeInfo response hops to router-safe maximum. + * Important so large config values cannot widen response scope unexpectedly. + */ +static void test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; + mockNodeDB->setCachedNode(kTargetNode); + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.hop_start = 5; + request.hop_limit = 1; // 4 hops away; router clamp should cap max at 3 + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); +} + +/** + * Verify NodeInfo direct-response success path and reply packet fields. + * Important because this path consumes the request and generates a spoofed cached reply. + */ +static void test_tm_nodeinfo_directResponse_respondsFromCache(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.config_ok_to_mqtt = true; + mockNodeDB->setCachedNode(kTargetNode); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.id = 0x13572468; + request.hop_start = 3; + request.hop_limit = 3; // direct request (0 hops away) + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); + TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits); + TEST_ASSERT_EQUAL_UINT32(1, static_cast(mockRouter.sentPackets.size())); + + const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front(); + TEST_ASSERT_EQUAL_INT(meshtastic_PortNum_NODEINFO_APP, reply.decoded.portnum); + TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from); + TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to); + TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id); + TEST_ASSERT_FALSE(reply.decoded.want_response); + TEST_ASSERT_EQUAL_UINT8(0, reply.hop_limit); + TEST_ASSERT_EQUAL_UINT8(0, reply.hop_start); + TEST_ASSERT_EQUAL_UINT8(mockNodeDB->getLastByteOfNodeNum(kRemoteNode), reply.next_hop); + TEST_ASSERT_TRUE(reply.decoded.has_bitfield); + TEST_ASSERT_EQUAL_UINT8(BITFIELD_OK_TO_MQTT_MASK, reply.decoded.bitfield); +} + +/** + * Verify cached direct replies still preserve requester NodeInfo learning. + * Important so consuming the request does not skip NodeDB refresh for observers. + */ +static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + mockNodeDB->setCachedNode(kTargetNode); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeNodeInfoPacket(kRemoteNode, "requester-long", "rq"); + request.to = kTargetNode; + request.decoded.want_response = true; + request.id = 0x01020304; + request.hop_start = 3; + request.hop_limit = 3; + + ProcessMessage result = module.handleReceived(request); + meshtastic_NodeInfoLite *requestor = mockNodeDB->getMeshNode(kRemoteNode); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); + TEST_ASSERT_NOT_NULL(requestor); + TEST_ASSERT_TRUE(requestor->has_user); + TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name); + TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name); + TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel); +} + +/** + * Verify client role only answers direct (0-hop) NodeInfo requests. + * Important so clients do not answer relayed requests outside intended scope. + */ +static void test_tm_nodeinfo_clientClamp_skipsWhenNotDirect(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + mockNodeDB->setCachedNode(kTargetNode); + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.hop_start = 2; + request.hop_limit = 1; // 1 hop away; clients are clamped to max 0 + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); +} + +#if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) +/** + * Verify non-PSRAM builds require NodeDB for direct NodeInfo responses. + * Important because fallback should only happen through node-wide data when + * the dedicated PSRAM cache does not exist. + */ +static void test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + mockNodeDB->clearCachedNode(); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.hop_start = 3; + request.hop_limit = 3; + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); + TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); + TEST_ASSERT_EQUAL_UINT32(0, static_cast(mockRouter.sentPackets.size())); +} +#endif + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) +/** + * Verify PSRAM NodeInfo cache can answer requests without NodeDB and that + * shouldRespondToNodeInfo() uses cached bitfield metadata. + */ +static void test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.config_ok_to_mqtt = true; + mockNodeDB->clearCachedNode(); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket observed = makeNodeInfoPacket(kTargetNode, "target-long", "tg"); + observed.decoded.has_bitfield = true; + observed.decoded.bitfield = BITFIELD_WANT_RESPONSE_MASK; + observed.channel = 2; + observed.rx_time = 123456; + + ProcessMessage observedResult = module.handleReceived(observed); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(observedResult)); + + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.id = 0x24681357; + request.channel = 1; + request.hop_start = 3; + request.hop_limit = 3; + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); + TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits); + TEST_ASSERT_EQUAL_UINT32(1, static_cast(mockRouter.sentPackets.size())); + + const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front(); + TEST_ASSERT_TRUE(reply.decoded.has_bitfield); + TEST_ASSERT_EQUAL_UINT8(static_cast(BITFIELD_WANT_RESPONSE_MASK | BITFIELD_OK_TO_MQTT_MASK), reply.decoded.bitfield); + TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from); + TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to); + TEST_ASSERT_EQUAL_UINT8(request.channel, reply.channel); + TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id); +} + +/** + * Verify PSRAM cache misses do not fall back to NodeDB. + * Important so the dedicated PSRAM index stays logically separate from + * NodeInfoModule/NodeDB when PSRAM is available. + */ +static void test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + mockNodeDB->setCachedNode(kTargetNode); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.hop_start = 3; + request.hop_limit = 3; + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); + TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); + TEST_ASSERT_EQUAL_UINT32(0, static_cast(mockRouter.sentPackets.size())); +} +#endif + +/** + * Verify relayed telemetry broadcasts are hop-exhausted when enabled. + * Important to prevent further mesh propagation while still allowing one relay step. + */ +static void test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); + packet.hop_start = 5; + packet.hop_limit = 3; + + module.alterReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit); + TEST_ASSERT_EQUAL_UINT8(3, packet.hop_start); + TEST_ASSERT_TRUE(module.shouldExhaustHops(packet)); + TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); +} + +/** + * Verify hop exhaustion skips unicast and local-origin packets. + * Important to avoid mutating traffic that should retain normal forwarding behavior. + */ +static void test_tm_alterReceived_skipsLocalAndUnicast(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket unicast = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kTargetNode); + unicast.hop_start = 5; + unicast.hop_limit = 3; + module.alterReceived(unicast); + TEST_ASSERT_EQUAL_UINT8(3, unicast.hop_limit); + TEST_ASSERT_FALSE(module.shouldExhaustHops(unicast)); + + meshtastic_MeshPacket fromUs = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kLocalNode, NODENUM_BROADCAST); + fromUs.hop_start = 5; + fromUs.hop_limit = 3; + module.alterReceived(fromUs); + TEST_ASSERT_EQUAL_UINT8(3, fromUs.hop_limit); + TEST_ASSERT_FALSE(module.shouldExhaustHops(fromUs)); + + meshtastic_TrafficManagementStats stats = module.getStats(); + TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets); +} + +/** + * Verify position dedup window expires and later duplicates are allowed. + * Important so periodic identical reports can resume after cooldown. + */ +static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 1; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket third = makePositionPacket(kRemoteNode, 374221234, -1220845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + testDelay(1200); + ProcessMessage r3 = module.handleReceived(third); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); +} + +/** + * Verify interval=0 disables position deduplication. + * Important because this is an explicit configuration escape hatch. + */ +static void test_tm_positionDedup_intervalZero_neverDrops(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 0; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify precision values above 32 fall back to default precision. + * Important so invalid config uses the documented default behavior. + */ +static void test_tm_positionDedup_precisionAbove32_usesDefaultPrecision(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 99; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 384221234, -1210845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify precision=32 does not collapse all positions to one fingerprint. + * Important to prevent false duplicate drops at the full-precision boundary. + */ +static void test_tm_positionDedup_precision32_allowsDistinctPositions(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 32; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify invalid precision=0 is treated as full precision. + * Important so invalid config does not collapse all positions into one fingerprint. + */ +static void test_tm_positionDedup_precisionZero_allowsDistinctPositions(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 0; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify epoch reset invalidates stale position identity for dedup. + * Important so reset paths cannot leak prior packet identity into new windows. + */ +static void test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket afterReset = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 374221234, -1220845678); + + ProcessMessage r1 = module.handleReceived(first); + module.resetEpoch(millis()); + ProcessMessage r2 = module.handleReceived(afterReset); + ProcessMessage r3 = module.handleReceived(duplicate); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); +} + +/** + * Verify non-position cache state does not make the first fingerprint-0 position look duplicated. + * Important so unified cache entries from other features cannot leak into dedup decisions. + */ +static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 10; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000); + meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000); + + ProcessMessage seeded = module.handleReceived(text); + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(duplicate); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(seeded)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); +} + +/** + * Verify rate-limit counters reset after the window expires. + * Important so temporary bursts do not cause persistent throttling. + */ +static void test_tm_rateLimit_resetsAfterWindowExpires(void) +{ + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 1; + moduleConfig.traffic_management.rate_limit_max_packets = 1; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + + ProcessMessage r1 = module.handleReceived(packet); + ProcessMessage r2 = module.handleReceived(packet); + testDelay(1200); + ProcessMessage r3 = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); +} + +/** + * Verify rate-limit thresholds above 255 effectively clamp to 255. + * Important because counters are uint8_t and must not overflow behavior. + */ +static void test_tm_rateLimit_thresholdAbove255_clamps(void) +{ + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 300; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + + for (int i = 0; i < 255; i++) { + ProcessMessage result = module.handleReceived(packet); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + } + ProcessMessage dropped = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(dropped)); + TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); +} + +/** + * Verify unknown-packet tracking resets after its active window expires. + * Important so old unknown traffic does not trigger delayed drops. + */ +static void test_tm_unknownPackets_resetAfterWindowExpires(void) +{ + moduleConfig.traffic_management.drop_unknown_enabled = true; + moduleConfig.traffic_management.unknown_packet_threshold = 1; + moduleConfig.traffic_management.rate_limit_window_secs = 1; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); + + ProcessMessage r1 = module.handleReceived(packet); + ProcessMessage r2 = module.handleReceived(packet); + testDelay(1200); + ProcessMessage r3 = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); +} + +/** + * Verify unknown threshold values above 255 clamp to the counter ceiling. + * Important to align config semantics with saturating counter storage. + */ +static void test_tm_unknownPackets_thresholdAbove255_clamps(void) +{ + moduleConfig.traffic_management.drop_unknown_enabled = true; + moduleConfig.traffic_management.unknown_packet_threshold = 300; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); + + for (int i = 0; i < 255; i++) { + ProcessMessage result = module.handleReceived(packet); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + } + ProcessMessage dropped = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(dropped)); + TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); +} + +/** + * Verify relayed position broadcasts can also be hop-exhausted. + * Important because telemetry and position use separate exhaust flags. + */ +static void test_tm_alterReceived_exhaustsRelayedPositionBroadcast(void) +{ + moduleConfig.traffic_management.exhaust_hop_position = true; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makePositionPacket(kRemoteNode, 374221234, -1220845678, NODENUM_BROADCAST); + packet.hop_start = 5; + packet.hop_limit = 2; + + module.alterReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit); + TEST_ASSERT_EQUAL_UINT8(4, packet.hop_start); + TEST_ASSERT_TRUE(module.shouldExhaustHops(packet)); + TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); +} + +/** + * Verify hop exhaustion ignores undecoded/encrypted packets. + * Important so we never mutate packets that were not decoded by this module. + */ +static void test_tm_alterReceived_skipsUndecodedPackets(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode, NODENUM_BROADCAST); + packet.hop_start = 5; + packet.hop_limit = 3; + + module.alterReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_UINT8(5, packet.hop_start); + TEST_ASSERT_EQUAL_UINT8(3, packet.hop_limit); + TEST_ASSERT_FALSE(module.shouldExhaustHops(packet)); + TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets); +} + +/** + * Verify exhaustRequested is per-packet and resets on next handleReceived(). + * Important so a prior packet cannot leak hop-exhaust state into later packets. + */ +static void test_tm_alterReceived_resetExhaustFlagOnNextPacket(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); + telemetry.hop_start = 5; + telemetry.hop_limit = 3; + module.alterReceived(telemetry); + TEST_ASSERT_TRUE(module.shouldExhaustHops(telemetry)); + + meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + ProcessMessage result = module.handleReceived(text); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_FALSE(module.shouldExhaustHops(telemetry)); + TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); +} + +/** + * Verify exhaust requests are packet-scoped (from + id). + * Important so stale state from one packet cannot influence unrelated packets + * that pass through duplicate/rebroadcast paths before handleReceived(). + */ +static void test_tm_alterReceived_exhaustFlag_isPacketScoped(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket exhausted = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); + exhausted.id = 0x1010; + exhausted.hop_start = 5; + exhausted.hop_limit = 3; + module.alterReceived(exhausted); + + meshtastic_MeshPacket unrelated = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kTargetNode, NODENUM_BROADCAST); + unrelated.id = 0x2020; + unrelated.hop_start = 4; + unrelated.hop_limit = 0; + + TEST_ASSERT_TRUE(module.shouldExhaustHops(exhausted)); + TEST_ASSERT_FALSE(module.shouldExhaustHops(unrelated)); +} + +/** + * Verify runOnce() returns sleep-forever interval when module is disabled. + * Important to ensure the maintenance thread is effectively inert when off. + */ +static void test_tm_runOnce_disabledReturnsMaxInterval(void) +{ + moduleConfig.traffic_management.enabled = false; + TrafficManagementModuleTestShim module; + + int32_t interval = module.runOnce(); + + TEST_ASSERT_EQUAL_INT32(INT32_MAX, interval); +} + +/** + * Verify runOnce() returns the maintenance cadence when enabled. + * Important so periodic cache housekeeping continues at expected interval. + */ +static void test_tm_runOnce_enabledReturnsMaintenanceInterval(void) +{ + TrafficManagementModuleTestShim module; + + int32_t interval = module.runOnce(); + + TEST_ASSERT_EQUAL_INT32(60 * 1000, interval); +} + +} // namespace + +void setUp(void) +{ + resetTrafficConfig(); +} +void tearDown(void) {} + +TM_TEST_ENTRY void setup() +{ + delay(10); + delay(2000); + + initializeTestEnvironment(); + mockNodeDB = new MockNodeDB(); + nodeDB = mockNodeDB; + + UNITY_BEGIN(); + RUN_TEST(test_tm_moduleDisabled_doesNothing); + RUN_TEST(test_tm_unknownPackets_dropOnNPlusOne); + RUN_TEST(test_tm_positionDedup_dropsDuplicateWithinWindow); + RUN_TEST(test_tm_positionDedup_allowsMovedPosition); + RUN_TEST(test_tm_rateLimit_dropsOnlyAfterThreshold); + RUN_TEST(test_tm_rateLimit_skipsRoutingAndAdminPorts); + RUN_TEST(test_tm_fromUs_bypassesPositionAndRateFilters); + RUN_TEST(test_tm_localDestination_bypassesTransitFilters); + RUN_TEST(test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops); + RUN_TEST(test_tm_nodeinfo_directResponse_respondsFromCache); + RUN_TEST(test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo); + RUN_TEST(test_tm_nodeinfo_clientClamp_skipsWhenNotDirect); +#if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) + RUN_TEST(test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips); +#endif +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + RUN_TEST(test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield); + RUN_TEST(test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb); +#endif + RUN_TEST(test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast); + RUN_TEST(test_tm_alterReceived_skipsLocalAndUnicast); + RUN_TEST(test_tm_positionDedup_allowsDuplicateAfterIntervalExpires); + RUN_TEST(test_tm_positionDedup_intervalZero_neverDrops); + RUN_TEST(test_tm_positionDedup_precisionAbove32_usesDefaultPrecision); + RUN_TEST(test_tm_positionDedup_precision32_allowsDistinctPositions); + RUN_TEST(test_tm_positionDedup_precisionZero_allowsDistinctPositions); + RUN_TEST(test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset); + RUN_TEST(test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero); + RUN_TEST(test_tm_rateLimit_resetsAfterWindowExpires); + RUN_TEST(test_tm_rateLimit_thresholdAbove255_clamps); + RUN_TEST(test_tm_unknownPackets_resetAfterWindowExpires); + RUN_TEST(test_tm_unknownPackets_thresholdAbove255_clamps); + RUN_TEST(test_tm_alterReceived_exhaustsRelayedPositionBroadcast); + RUN_TEST(test_tm_alterReceived_skipsUndecodedPackets); + RUN_TEST(test_tm_alterReceived_resetExhaustFlagOnNextPacket); + RUN_TEST(test_tm_alterReceived_exhaustFlag_isPacketScoped); + RUN_TEST(test_tm_runOnce_disabledReturnsMaxInterval); + RUN_TEST(test_tm_runOnce_enabledReturnsMaintenanceInterval); + exit(UNITY_END()); +} + +TM_TEST_ENTRY void loop() {} + +#else + +void setUp(void) {} +void tearDown(void) {} + +TM_TEST_ENTRY void setup() +{ + initializeTestEnvironment(); + UNITY_BEGIN(); + exit(UNITY_END()); +} + +TM_TEST_ENTRY void loop() {} + +#endif diff --git a/test/test_transmit_history/test_main.cpp b/test/test_transmit_history/test_main.cpp new file mode 100644 index 000000000..3bd84b55c --- /dev/null +++ b/test/test_transmit_history/test_main.cpp @@ -0,0 +1,336 @@ +#include "TestUtil.h" +#include "TransmitHistory.h" +#include "gps/RTC.h" +#include +#include + +// Reset the singleton between tests +static void resetTransmitHistory() +{ + if (transmitHistory) { + delete transmitHistory; + transmitHistory = nullptr; + } + transmitHistory = TransmitHistory::getInstance(); +} + +void setUp(void) +{ + resetTransmitHistory(); +} + +void tearDown(void) {} + +static void test_setLastSentToMesh_stores_millis() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + + uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + TEST_ASSERT_NOT_EQUAL(0, result); + + // The stored millis value should be very close to current millis() + uint32_t diff = millis() - result; + TEST_ASSERT_LESS_OR_EQUAL(100, diff); // Within 100ms +} + +static void test_set_overwrites_previous_value() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); + uint32_t first = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); + + testDelay(50); + + transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); + uint32_t second = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); + + // The second value should be newer (larger millis) + TEST_ASSERT_GREATER_THAN(first, second); +} + +// --- Throttle integration --- + +static void test_throttle_blocks_within_interval() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + + // Should be within a 10-minute interval (just set it) + bool withinInterval = Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE(withinInterval); +} + +static void test_throttle_allows_after_interval() +{ + // Unknown key returns 0 — throttle should NOT block + uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + TEST_ASSERT_EQUAL_UINT32(0, lastMs); + + // When lastMs == 0, the module check `lastMs == 0 || !isWithinTimespan` allows sending + bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE(shouldSend); +} + +static void test_throttle_blocks_after_set_then_zero_does_not() +{ + // Set it — now throttle should block + transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); + uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); + bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000); + TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval) + + // Different key — should allow + uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); + bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000); + TEST_ASSERT_TRUE(otherShouldSend); +} + +// --- Multiple keys --- + +static void test_multiple_keys_stored_independently() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + uint32_t nodeInfoInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + testDelay(20); + transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); + uint32_t positionInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); + testDelay(20); + transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); + + uint32_t nodeInfo = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + uint32_t position = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); + uint32_t telemetry = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); + + // All should be non-zero + TEST_ASSERT_NOT_EQUAL(0, nodeInfo); + TEST_ASSERT_NOT_EQUAL(0, position); + TEST_ASSERT_NOT_EQUAL(0, telemetry); + + // Updating other keys should not overwrite earlier key timestamps + TEST_ASSERT_EQUAL_UINT32(nodeInfoInitial, nodeInfo); + TEST_ASSERT_EQUAL_UINT32(positionInitial, position); +} + +// --- Singleton --- + +static void test_getInstance_returns_same_instance() +{ + TransmitHistory *a = TransmitHistory::getInstance(); + TransmitHistory *b = TransmitHistory::getInstance(); + TEST_ASSERT_EQUAL_PTR(a, b); +} + +static void test_getInstance_creates_global() +{ + if (transmitHistory) { + delete transmitHistory; + transmitHistory = nullptr; + } + TEST_ASSERT_NULL(transmitHistory); + + TransmitHistory::getInstance(); + TEST_ASSERT_NOT_NULL(transmitHistory); +} + +// --- Persistence round-trip (loadFromDisk / saveToDisk) --- + +static void test_save_and_load_round_trip() +{ + // Set some values + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + testDelay(10); + transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); + + uint32_t nodeInfoEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + uint32_t positionEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP); + + // Force save + transmitHistory->saveToDisk(); + + // Reset and reload + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + // Epoch values should be restored (if RTC was available when set) + uint32_t restoredNodeInfo = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + uint32_t restoredPosition = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP); + + TEST_ASSERT_EQUAL_UINT32(nodeInfoEpoch, restoredNodeInfo); + TEST_ASSERT_EQUAL_UINT32(positionEpoch, restoredPosition); + + // After loadFromDisk, millis should be seeded (non-zero) for stored entries + uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + if (restoredNodeInfo > 0) { + // If epoch was stored (set seconds ago), epoch-conversion gives elapsed ≈ 0 s, + // so getLastSentToMeshMillis() should return a non-zero value. + TEST_ASSERT_NOT_EQUAL(0, restoredMillis); + } +} + +// --- Boot without RTC scenario --- + +// Crash-reboot protection: a send that happened moments before the reboot must still +// throttle after reload. This works because getLastSentToMeshMillis() reconstructs +// a millis()-relative timestamp from the stored epoch, and Throttle uses unsigned +// subtraction so the age survives wraparound even when uptime is near zero. +static void test_boot_after_recent_send_still_throttles() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + // Epoch was set seconds ago; reconstructed age is still within the 10-min window. + uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (epoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + TEST_ASSERT_NOT_EQUAL(0, result); + bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); + TEST_ASSERT_TRUE(withinInterval); +} + +// Regression test for issue #9901: +// A device powered off for longer than the throttle window must broadcast NodeInfo +// on its next boot — it must not be silenced because loadFromDisk() once treated +// every loaded entry as "just sent" by seeding lastMillis to millis() at boot. +static void test_boot_after_long_gap_allows_nodeinfo() +{ + if (getRTCQuality() <= RTCQualityNone) { + TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test"); + return; + } + + uint32_t now = getTime(); + + // Simulate: last NodeInfo sent 30 minutes ago (outside the 10-min throttle window) + transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (30 * 60)); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (restoredEpoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_FALSE_MESSAGE(throttled, "NodeInfo must not be throttled after a 30-min gap (#9901)"); +} + +// Complementary: a rapid reboot must still throttle (crash-loop protection), even +// though the reconstructed lastMs may wrap because current uptime is small. +static void test_boot_within_throttle_window_still_throttles() +{ + if (getRTCQuality() <= RTCQualityNone) { + TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test"); + return; + } + + uint32_t now = getTime(); + + // Simulate: last NodeInfo sent 5 minutes ago (inside the 10-min throttle window) + transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (5 * 60)); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (restoredEpoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE_MESSAGE(throttled, "NodeInfo must still be throttled when last send was within the 10-min window"); +} + +static void test_boot_without_time_source_still_throttles_recent_restart() +{ + setBootRelativeTimeForUnitTest(32); + transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32); + transmitHistory->saveToDisk(); + + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + + setBootRelativeTimeForUnitTest(31); + transmitHistory->loadFromDisk(); + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE_MESSAGE(throttled, "Recent no-RTC reboots should still suppress duplicate NodeInfo"); +} + +static void test_boot_without_time_source_expires_boot_relative_history() +{ + setBootRelativeTimeForUnitTest(32); + transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32); + transmitHistory->saveToDisk(); + + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + + setBootRelativeTimeForUnitTest(400); + transmitHistory->loadFromDisk(); + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, restoredMs, "Boot-relative history should only suppress near-term restarts"); +} + +void setup() +{ + initializeTestEnvironment(); + + UNITY_BEGIN(); + + RUN_TEST(test_setLastSentToMesh_stores_millis); + RUN_TEST(test_set_overwrites_previous_value); + + RUN_TEST(test_throttle_blocks_within_interval); + RUN_TEST(test_throttle_allows_after_interval); + RUN_TEST(test_throttle_blocks_after_set_then_zero_does_not); + + RUN_TEST(test_multiple_keys_stored_independently); + + // Singleton + RUN_TEST(test_getInstance_returns_same_instance); + RUN_TEST(test_getInstance_creates_global); + + // Persistence + RUN_TEST(test_save_and_load_round_trip); + RUN_TEST(test_boot_after_recent_send_still_throttles); + + // Issue #9901 regression tests + RUN_TEST(test_boot_after_long_gap_allows_nodeinfo); + RUN_TEST(test_boot_within_throttle_window_still_throttles); + + // No-RTC regression tests + RUN_TEST(test_boot_without_time_source_still_throttles_recent_restart); + RUN_TEST(test_boot_without_time_source_expires_boot_relative_history); + + exit(UNITY_END()); +} + +void loop() {} diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 9e916aae2..b81f09362 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -18,6 +18,7 @@ // "USERPREFS_CHANNEL_2_UPLINK_ENABLED": "false", // "USERPREFS_CONFIG_GPS_MODE": "meshtastic_Config_PositionConfig_GpsMode_ENABLED", // "USERPREFS_CONFIG_LORA_IGNORE_MQTT": "true", + // "USERPREFS_LORA_TX_DISABLED": "1", // If set, forces config.lora.tx_enabled=false during lora bootstrap // "USERPREFS_CONFIG_LORA_REGION": "meshtastic_Config_LoRaConfig_RegionCode_US", // "USERPREFS_CONFIG_OWNER_LONG_NAME": "My Long Name", // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", diff --git a/variants/esp32/betafpv_2400_tx_micro/platformio.ini b/variants/esp32/betafpv_2400_tx_micro/platformio.ini index 77a1f7043..c2e89b333 100644 --- a/variants/esp32/betafpv_2400_tx_micro/platformio.ini +++ b/variants/esp32/betafpv_2400_tx_micro/platformio.ini @@ -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.2 diff --git a/variants/esp32/betafpv_2400_tx_micro/variant.h b/variants/esp32/betafpv_2400_tx_micro/variant.h index 67699e7c8..8e875a3e6 100644 --- a/variants/esp32/betafpv_2400_tx_micro/variant.h +++ b/variants/esp32/betafpv_2400_tx_micro/variant.h @@ -14,7 +14,7 @@ #define LORA_CS 5 #define RF95_FAN_EN 17 -// #define LED_PIN 16 // This is a LED_WS2812 not a standard LED +// This is a LED_WS2812 not a standard LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected #define NEOPIXEL_DATA 16 // gpio pin used to send data to the neopixels diff --git a/variants/esp32/betafpv_900_tx_nano/variant.h b/variants/esp32/betafpv_900_tx_nano/variant.h index 7a4ae9190..6bee74d90 100644 --- a/variants/esp32/betafpv_900_tx_nano/variant.h +++ b/variants/esp32/betafpv_900_tx_nano/variant.h @@ -20,7 +20,7 @@ #define LORA_DIO2 #define LORA_DIO3 -#define LED_PIN 16 // green - blue is at 17 +#define LED_POWER 16 // green - blue is at 17 #define BUTTON_PIN 25 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/chatter2/platformio.ini b/variants/esp32/chatter2/platformio.ini index 4218e8503..a14e407a1 100644 --- a/variants/esp32/chatter2/platformio.ini +++ b/variants/esp32/chatter2/platformio.ini @@ -12,4 +12,4 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32/chatter2/variant.h b/variants/esp32/chatter2/variant.h index abcb1ce4d..e91e4dcef 100644 --- a/variants/esp32/chatter2/variant.h +++ b/variants/esp32/chatter2/variant.h @@ -23,8 +23,6 @@ #define SX126X_TXEN RADIOLIB_NC #define SX126X_RXEN RADIOLIB_NC -// Status -// #define LED_PIN 1 // External notification // FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the // app/preferences @@ -98,7 +96,6 @@ #define KB_LOAD 21 // load values from the switch and store in shift register #define KB_CLK 22 // clock pin for serial data out #define KB_DATA 23 // data pin -#define CANNED_MESSAGE_MODULE_ENABLE 1 ///////////////////////////////////////////////////////////////////////////////// // // diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini index 809599212..2ddc5a2db 100644 --- a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini @@ -10,3 +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 +build_src_filter = + ${esp32_base.build_src_filter} + +<../variants/esp32/diy/9m2ibr_aprs_lora_tracker> \ No newline at end of file diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.cpp b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.cpp new file mode 100644 index 000000000..ef90d5a54 --- /dev/null +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.cpp @@ -0,0 +1,8 @@ +#include "variant.h" +#include "Arduino.h" + +void earlyInitVariant() +{ + pinMode(USER_LED, OUTPUT); + digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); +} \ No newline at end of file diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h index 037933140..1f84fffa1 100644 --- a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h @@ -21,8 +21,8 @@ #define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client // LEDs -#define LED_PIN 13 // Tx LED -#define USER_LED 2 // Rx LED +#define LED_POWER 13 // Tx LED +#define USER_LED 2 // Rx LED // Buzzer #define PIN_BUZZER 33 diff --git a/variants/esp32/diy/hydra/variant.h b/variants/esp32/diy/hydra/variant.h index e5c10e26b..68194f869 100644 --- a/variants/esp32/diy/hydra/variant.h +++ b/variants/esp32/diy/hydra/variant.h @@ -15,7 +15,7 @@ #define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) #define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards #define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). -#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets) +#define LED_POWER 2 // add status LED (compatible with core-pcb and DIY targets) // Radio #define USE_SX1262 // E22-900M30S uses SX1262 diff --git a/variants/esp32/diy/v1/variant.h b/variants/esp32/diy/v1/variant.h index 8a2df3f2b..862969af0 100644 --- a/variants/esp32/diy/v1/variant.h +++ b/variants/esp32/diy/v1/variant.h @@ -15,7 +15,7 @@ #define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) #define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards #define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). -#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets) +#define LED_POWER 2 // add status LED (compatible with core-pcb and DIY targets) #define LORA_DIO0 26 // a No connect on the SX1262/SX1268 module #define LORA_RESET 23 // RST for SX1276, and for SX1262/SX1268 diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index bbbcd3cbe..b9d6f5c50 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -5,7 +5,10 @@ custom_esp32_kind = custom_mtjson_part = platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.12.0 + platformio/espressif32@6.13.0 +platform_packages = + # renovate: datasource=custom.pio depName=platformio/tool-mklittlefs packageName=platformio/tool/tool-mklittlefs + platformio/tool-mklittlefs@1.203.210628 extra_scripts = ${env.extra_scripts} @@ -24,14 +27,19 @@ board_build.filesystem = littlefs # Remove -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL for low level BLE logging. # See library directory for BLE logging possible values: .pio/libdeps/tbeam/NimBLE-Arduino/src/log_common/log_common.h # This overrides the BLE logging default of LOG_LEVEL_INFO (1) from: .pio/libdeps/tbeam/NimBLE-Arduino/src/esp_nimble_cfg.h -build_unflags = -fno-lto +build_unflags = + -fno-lto + # Keep explicit std unflags on ESP32; base-level unflags are not sufficient + # to prevent framework-injected C++11 fallback on this platform. + -std=c++11 + -std=gnu++11 build_flags = ${arduino_base.build_flags} -flto -Wall -Wextra -Isrc/platform/esp32 - -std=c++11 + -std=gnu++17 -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL @@ -59,13 +67,13 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/b0f3960b3e8444563280656d88e22b5899481884.zip + https://github.com/meshtastic/esp32_https_server/archive/0c71f380390ad483ff134ad938e07f6cf1226c5b.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino - h2zero/NimBLE-Arduino@^1.4.3 + h2zero/NimBLE-Arduino@1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index 20ce38fae..f40f1d064 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -4,25 +4,29 @@ extends = esp32_common custom_esp32_kind = esp32 +build_src_filter = + ${esp32_common.build_src_filter} + - + - + build_flags = ${esp32_common.build_flags} -DMESHTASTIC_EXCLUDE_AUDIO=1 -; Override lib_deps to use environmental_extra_no_bsec instead of environmental_extra -; BSEC library uses ~3.5KB DRAM which causes overflow on original ESP32 targets + -DMESHTASTIC_EXCLUDE_ACCELEROMETER=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_WEBSERVER=1 + -DMESHTASTIC_EXCLUDE_RANGETEST=1 + lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} + ${radiolib_base.lib_deps} ${environmental_base.lib_deps} ${environmental_extra_no_bsec.lib_deps} - ${radiolib_base.lib_deps} - # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/b0f3960b3e8444563280656d88e22b5899481884.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino - h2zero/NimBLE-Arduino@^1.4.3 - # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master - https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + h2zero/NimBLE-Arduino@1.4.3 + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto - rweather/Crypto@0.4.0 \ No newline at end of file + rweather/Crypto@0.4.0 diff --git a/variants/esp32/hackerboxes_esp32_io/variant.h b/variants/esp32/hackerboxes_esp32_io/variant.h index 06f0032ee..42ce2423e 100644 --- a/variants/esp32/hackerboxes_esp32_io/variant.h +++ b/variants/esp32/hackerboxes_esp32_io/variant.h @@ -3,7 +3,7 @@ // HACKBOX LoRa IO Kit // Uses a ESP-32-WROOM and a RA-01SH (SX1262) LoRa Board -#define LED_PIN 2 // LED +#define LED_POWER 2 // LED #define LED_STATE_ON 1 // State when LED is lit #define HAS_SCREEN 0 diff --git a/variants/esp32/heltec_v1/variant.h b/variants/esp32/heltec_v1/variant.h index d1338a28e..c4f6577a8 100644 --- a/variants/esp32/heltec_v1/variant.h +++ b/variants/esp32/heltec_v1/variant.h @@ -12,7 +12,7 @@ #define RESET_OLED 16 // If defined, this pin will be used to reset the display controller -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define USE_RF95 diff --git a/variants/esp32/heltec_v2.1/variant.h b/variants/esp32/heltec_v2.1/variant.h index 8ebccc54f..e4aeb363d 100644 --- a/variants/esp32/heltec_v2.1/variant.h +++ b/variants/esp32/heltec_v2.1/variant.h @@ -18,7 +18,7 @@ #define RESET_OLED 16 // If defined, this pin will be used to reset the display controller #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define USE_RF95 diff --git a/variants/esp32/heltec_v2/variant.h b/variants/esp32/heltec_v2/variant.h index 5c183818b..c35465f81 100644 --- a/variants/esp32/heltec_v2/variant.h +++ b/variants/esp32/heltec_v2/variant.h @@ -13,7 +13,7 @@ #define RESET_OLED 16 // If defined, this pin will be used to reset the display controller #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define USE_RF95 diff --git a/variants/esp32/heltec_wireless_bridge/platformio.ini b/variants/esp32/heltec_wireless_bridge/platformio.ini index 6f9de7a84..42a35697c 100644 --- a/variants/esp32/heltec_wireless_bridge/platformio.ini +++ b/variants/esp32/heltec_wireless_bridge/platformio.ini @@ -10,6 +10,7 @@ build_flags = -D BOARD_HAS_PSRAM -D RADIOLIB_EXCLUDE_LR11X0=1 -D RADIOLIB_EXCLUDE_SX128X=1 + -D RADIOLIB_EXCLUDE_LR2021=1 -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 diff --git a/variants/esp32/heltec_wireless_bridge/variant.h b/variants/esp32/heltec_wireless_bridge/variant.h index 5ad16d0e2..82cc83aa8 100644 --- a/variants/esp32/heltec_wireless_bridge/variant.h +++ b/variants/esp32/heltec_wireless_bridge/variant.h @@ -15,7 +15,7 @@ #undef GPS_TX_PIN // Green / Lora = PIN 22 / GPIO2, Yellow / Wifi = PIN 23 / GPIO0, Blue / BLE = PIN 25 / GPIO16 -#define LED_PIN 22 +#define LED_POWER 22 #define WIFI_LED 23 #define BLE_LED 25 diff --git a/variants/esp32/heltec_wsl_v2.1/variant.h b/variants/esp32/heltec_wsl_v2.1/variant.h index 3927a89d6..db374afb6 100644 --- a/variants/esp32/heltec_wsl_v2.1/variant.h +++ b/variants/esp32/heltec_wsl_v2.1/variant.h @@ -1,7 +1,7 @@ #define I2C_SCL SCL #define I2C_SDA SDA -#define LED_PIN LED +#define LED_POWER LED // active low, powers the Battery reader, but no lora antenna boost (?) // #define VEXT_ENABLE Vext diff --git a/variants/esp32/m5stack_core/platformio.ini b/variants/esp32/m5stack_core/platformio.ini index 4544d73b5..8fbbae895 100644 --- a/variants/esp32/m5stack_core/platformio.ini +++ b/variants/esp32/m5stack_core/platformio.ini @@ -30,10 +30,9 @@ build_flags = -DTFT_BL=32 -DSPI_FREQUENCY=40000000 -DSPI_READ_FREQUENCY=16000000 - -DDISABLE_ALL_LIBRARY_WARNINGS lib_ignore = m5stack-core lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini index af1535f59..e107bd893 100644 --- a/variants/esp32/m5stack_coreink/platformio.ini +++ b/variants/esp32/m5stack_coreink/platformio.ini @@ -19,7 +19,7 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 lib_ignore = diff --git a/variants/esp32/m5stack_coreink/variant.h b/variants/esp32/m5stack_coreink/variant.h index b1708352b..84a1e1966 100644 --- a/variants/esp32/m5stack_coreink/variant.h +++ b/variants/esp32/m5stack_coreink/variant.h @@ -11,11 +11,10 @@ // Green LED #define LED_STATE_ON 1 // State when LED is lit -#define LED_PIN 10 +#define LED_POWER 10 // PCF8563 RTC Module #define PCF8563_RTC 0x51 -#define HAS_RTC 1 // Wheel // Down 37 diff --git a/variants/esp32/nano-g1-explorer/platformio.ini b/variants/esp32/nano-g1-explorer/platformio.ini index 703bb9d09..6f57897a8 100644 --- a/variants/esp32/nano-g1-explorer/platformio.ini +++ b/variants/esp32/nano-g1-explorer/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Nano G1 Explorer custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D NANO_G1_EXPLORER diff --git a/variants/esp32/nano-g1/platformio.ini b/variants/esp32/nano-g1/platformio.ini index b0ebd191c..82d0f5e73 100644 --- a/variants/esp32/nano-g1/platformio.ini +++ b/variants/esp32/nano-g1/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Nano G1 custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D NANO_G1 diff --git a/variants/esp32/radiomaster_900_bandit_nano/variant.h b/variants/esp32/radiomaster_900_bandit_nano/variant.h index 1b6bba126..318401f92 100644 --- a/variants/esp32/radiomaster_900_bandit_nano/variant.h +++ b/variants/esp32/radiomaster_900_bandit_nano/variant.h @@ -37,7 +37,7 @@ /* LED PIN setup. */ -#define LED_PIN 15 +#define LED_POWER 15 /* Five way button when using ADC. diff --git a/variants/esp32/rak11200/pins_arduino.h b/variants/esp32/rak11200/pins_arduino.h index f383d54a7..263fbd5a0 100644 --- a/variants/esp32/rak11200/pins_arduino.h +++ b/variants/esp32/rak11200/pins_arduino.h @@ -6,8 +6,6 @@ #define LED_GREEN 12 #define LED_BLUE 2 -#define LED_BUILTIN LED_GREEN - static const uint8_t TX = 1; static const uint8_t RX = 3; diff --git a/variants/esp32/rak11200/platformio.ini b/variants/esp32/rak11200/platformio.ini index 63821a092..b48d638fb 100644 --- a/variants/esp32/rak11200/platformio.ini +++ b/variants/esp32/rak11200/platformio.ini @@ -16,4 +16,8 @@ build_flags = ${esp32_base.build_flags} -D RAK_11200 -I variants/esp32/rak11200 + -DMESHTASTIC_EXCLUDE_WEBSERVER=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_RANGETEST=1 + -DMESHTASTIC_EXCLUDE_MQTT=1 upload_speed = 115200 diff --git a/variants/esp32/rak11200/variant.h b/variants/esp32/rak11200/variant.h index 01edb8b73..a38ac83b7 100644 --- a/variants/esp32/rak11200/variant.h +++ b/variants/esp32/rak11200/variant.h @@ -6,8 +6,6 @@ #define LED_GREEN 12 #define LED_BLUE 2 -#define LED_BUILTIN LED_GREEN - static const uint8_t TX = 1; static const uint8_t RX = 3; @@ -45,7 +43,7 @@ static const uint8_t SCK = 33; #undef GPS_TX_PIN #define GPS_TX_PIN (TX1) -#define LED_PIN LED_BLUE +#define LED_POWER LED_BLUE #define PIN_VBAT WB_A0 #define BATTERY_PIN PIN_VBAT diff --git a/variants/esp32/station-g1/platformio.ini b/variants/esp32/station-g1/platformio.ini index ab7fcac2b..20e29764c 100644 --- a/variants/esp32/station-g1/platformio.ini +++ b/variants/esp32/station-g1/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Station G1 custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D STATION_G1 diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index 16a3d1845..96e9879ce 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -10,14 +10,12 @@ custom_meshtastic_images = tbeam.svg custom_meshtastic_tags = LilyGo extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam board_check = true build_flags = ${esp32_base.build_flags} -D TBEAM_V10 -I variants/esp32/tbeam - -DBOARD_HAS_PSRAM - -mfix-esp32-psram-cache-issue upload_speed = 921600 [env:tbeam-displayshield] @@ -31,5 +29,5 @@ lib_deps = ${env:tbeam.lib_deps} # renovate: datasource=github-tags depName=meshtastic-st7796 packageName=meshtastic/st7796 https://github.com/meshtastic/st7796/archive/1.0.5.zip - # renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 diff --git a/variants/esp32/tbeam/variant.h b/variants/esp32/tbeam/variant.h index 2d144a888..cca52cb9a 100644 --- a/variants/esp32/tbeam/variant.h +++ b/variants/esp32/tbeam/variant.h @@ -9,7 +9,7 @@ #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. #define LED_STATE_ON 0 // State when LED is lit -#define LED_PIN 4 // Newer tbeams (1.1) have an extra led on GPIO4 +#define LED_POWER 4 // Newer tbeams (1.1) have an extra led on GPIO4 // TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if // not found then probe for SX1262 @@ -49,7 +49,7 @@ #undef EXT_NOTIFY_OUT #undef LED_STATE_ON -#undef LED_PIN +#undef LED_POWER #define HAS_CST226SE 1 #define HAS_TOUCHSCREEN 1 @@ -57,7 +57,6 @@ #ifndef TOUCH_IRQ #define TOUCH_IRQ -1 #endif -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define USE_VIRTUAL_KEYBOARD 1 #define ST7796_NSS 25 diff --git a/variants/esp32/tbeam_v07/platformio.ini b/variants/esp32/tbeam_v07/platformio.ini index e2763fdec..1809ba56c 100644 --- a/variants/esp32/tbeam_v07/platformio.ini +++ b/variants/esp32/tbeam_v07/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_tags = LilyGo board_level = extra extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D TBEAM_V07 diff --git a/variants/esp32/tlora_v1/variant.h b/variants/esp32/tlora_v1/variant.h index 83e2c193e..ff9f4a8ef 100644 --- a/variants/esp32/tlora_v1/variant.h +++ b/variants/esp32/tlora_v1/variant.h @@ -5,7 +5,7 @@ #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost #define VEXT_ON_VALUE LOW -#define LED_PIN 2 // If defined we will blink this LED +#define LED_POWER 2 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. diff --git a/variants/esp32/tlora_v1_3/variant.h b/variants/esp32/tlora_v1_3/variant.h index 73cb31f27..2b0395d8a 100644 --- a/variants/esp32/tlora_v1_3/variant.h +++ b/variants/esp32/tlora_v1_3/variant.h @@ -7,7 +7,7 @@ #define RESET_OLED 16 // If defined, this pin will be used to reset the display controller #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 36 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/tlora_v2/variant.h b/variants/esp32/tlora_v2/variant.h index 8a7cf89ec..099fdc2ee 100644 --- a/variants/esp32/tlora_v2/variant.h +++ b/variants/esp32/tlora_v2/variant.h @@ -5,7 +5,7 @@ #define I2C_SCL 22 #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN \ 0 // If defined, this will be used for user button presses, if your board doesn't have a physical switch, you can wire one // between this pin and ground diff --git a/variants/esp32/tlora_v2_1_16/platformio.ini b/variants/esp32/tlora_v2_1_16/platformio.ini index d9cb8ed3b..a41c5016e 100644 --- a/variants/esp32/tlora_v2_1_16/platformio.ini +++ b/variants/esp32/tlora_v2_1_16/platformio.ini @@ -22,4 +22,4 @@ build_flags = ${env:tlora-v2-1-1_6.build_flags} -DBUTTON_PIN=0 -DPIN_BUZZER=25 - -DLED_PIN=-1 \ No newline at end of file + -DLED_POWER=-1 \ No newline at end of file diff --git a/variants/esp32/tlora_v2_1_16/variant.h b/variants/esp32/tlora_v2_1_16/variant.h index 9584dd68b..5488fddf4 100644 --- a/variants/esp32/tlora_v2_1_16/variant.h +++ b/variants/esp32/tlora_v2_1_16/variant.h @@ -8,10 +8,10 @@ #define I2C_SDA 21 // I2C pins for this board #define I2C_SCL 22 -#if defined(LED_PIN) && LED_PIN == -1 -#undef LED_PIN +#if defined(LED_POWER) && LED_POWER == -1 +#undef LED_POWER #else -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #endif #define USE_RF95 diff --git a/variants/esp32/tlora_v2_1_16_tcxo/platformio.ini b/variants/esp32/tlora_v2_1_16_tcxo/platformio.ini index a6b9d2254..3cb64c976 100644 --- a/variants/esp32/tlora_v2_1_16_tcxo/platformio.ini +++ b/variants/esp32/tlora_v2_1_16_tcxo/platformio.ini @@ -7,4 +7,4 @@ build_flags = -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 -D LORA_TCXO_GPIO=33 -upload_speed = 115200 \ No newline at end of file +upload_speed = 115200 diff --git a/variants/esp32/tlora_v2_1_18/variant.h b/variants/esp32/tlora_v2_1_18/variant.h index efc676992..1ab08c364 100644 --- a/variants/esp32/tlora_v2_1_18/variant.h +++ b/variants/esp32/tlora_v2_1_18/variant.h @@ -6,7 +6,7 @@ #define I2C_SDA 21 // I2C pins for this board #define I2C_SCL 22 -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 12 // If defined, this will be used for user button presses, #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/tlora_v3_3_0_tcxo/platformio.ini b/variants/esp32/tlora_v3_3_0_tcxo/platformio.ini index 1258fd8b7..d3669ce55 100644 --- a/variants/esp32/tlora_v3_3_0_tcxo/platformio.ini +++ b/variants/esp32/tlora_v3_3_0_tcxo/platformio.ini @@ -6,4 +6,4 @@ build_flags = -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 -D LORA_TCXO_GPIO=12 - -D BUTTON_PIN=0 \ No newline at end of file + -D BUTTON_PIN=0 diff --git a/variants/esp32/trackerd/variant.h b/variants/esp32/trackerd/variant.h index c4dfb9e93..8071ba99d 100644 --- a/variants/esp32/trackerd/variant.h +++ b/variants/esp32/trackerd/variant.h @@ -8,9 +8,8 @@ #define GPS_RX_PIN 9 #define GPS_TX_PIN 10 -#define LED_PIN 13 // 13 red, 2 blue, 15 red +#define LED_POWER 13 // 13 red, 2 blue, 15 red -// #define HAS_BUTTON 0 #define BUTTON_PIN 0 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/wiphone/platformio.ini b/variants/esp32/wiphone/platformio.ini index fcf36a23c..fbd77be75 100644 --- a/variants/esp32/wiphone/platformio.ini +++ b/variants/esp32/wiphone/platformio.ini @@ -11,7 +11,7 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander sparkfun/SX1509 IO Expander@3.0.6 # renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102 diff --git a/variants/esp32/wiphone/variant.h b/variants/esp32/wiphone/variant.h index 619ac622a..5baeb3936 100644 --- a/variants/esp32/wiphone/variant.h +++ b/variants/esp32/wiphone/variant.h @@ -26,7 +26,6 @@ #undef GPS_TX_PIN #define NO_GPS 1 #define HAS_GPS 0 -#define NO_SCREEN #define HAS_SCREEN 0 // Default SPI1 will be mapped to the display diff --git a/variants/esp32c3/ai-c3/variant.h b/variants/esp32c3/ai-c3/variant.h index 6c4f4d38a..a933de76b 100644 --- a/variants/esp32c3/ai-c3/variant.h +++ b/variants/esp32c3/ai-c3/variant.h @@ -4,7 +4,7 @@ #define I2C_SCL SCL #define BUTTON_PIN 9 // BOOT button -#define LED_PIN 30 // RGB LED +#define LED_POWER 30 // RGB LED #define USE_RF95 #define LORA_SCK 4 diff --git a/variants/esp32c3/hackerboxes_esp32c3_oled/variant.h b/variants/esp32c3/hackerboxes_esp32c3_oled/variant.h index 7432a9941..71090fbeb 100644 --- a/variants/esp32c3/hackerboxes_esp32c3_oled/variant.h +++ b/variants/esp32c3/hackerboxes_esp32c3_oled/variant.h @@ -3,7 +3,7 @@ // Hackerboxes LoRa ESP32-C3 OLED Kit // Uses a ESP32-C3 OLED Board and a RA-01SH (SX1262) LoRa Board -#define LED_PIN 8 // LED +#define LED_POWER 8 // LED #define LED_STATE_ON 1 // State when LED is lit #define HAS_SCREEN 0 diff --git a/variants/esp32c3/heltec_esp32c3/variant.h b/variants/esp32c3/heltec_esp32c3/variant.h index ca00c43fa..ed2f6f878 100644 --- a/variants/esp32c3/heltec_esp32c3/variant.h +++ b/variants/esp32c3/heltec_esp32c3/variant.h @@ -3,7 +3,7 @@ // LED pin on HT-DEV-ESP_V2 and HT-DEV-ESP_V3 // https://resource.heltec.cn/download/HT-CT62/HT-CT62_Reference_Design.pdf // https://resource.heltec.cn/download/HT-DEV-ESP/HT-DEV-ESP_V3_Sch.pdf -#define LED_PIN 2 // LED +#define LED_POWER 2 // LED #define LED_STATE_ON 1 // State when LED is lit #define HAS_SCREEN 0 diff --git a/variants/esp32c3/heltec_hru_3601/platformio.ini b/variants/esp32c3/heltec_hru_3601/platformio.ini index 8200b6e87..5eb1c9977 100644 --- a/variants/esp32c3/heltec_hru_3601/platformio.ini +++ b/variants/esp32c3/heltec_hru_3601/platformio.ini @@ -5,6 +5,3 @@ build_flags = ${esp32c3_base.build_flags} -D HELTEC_HRU_3601 -I variants/esp32c3/heltec_hru_3601 -lib_deps = ${esp32c3_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32c6/esp32c6.ini b/variants/esp32c6/esp32c6.ini index 9ee8591be..cdd9f9868 100644 --- a/variants/esp32c6/esp32c6.ini +++ b/variants/esp32c6/esp32c6.ini @@ -3,12 +3,14 @@ extends = esp32_common platform = # Do not renovate until we have switched to pioarduino tagged builds https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip +platform_packages = + # HACK: This release was automatically removed upstream + framework-arduinoespressif32 @ https://github.com/vidplace7/platform-espressif32/releases/download/meshtastic-esp32c6/framework-arduinoespressif32-all-release_v5.1-124d64e.zip build_flags = ${arduino_base.build_flags} -Wall -Wextra -Isrc/platform/esp32 - -std=c++11 -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING -DSERIAL_BUFFER_SIZE=4096 -DLIBPAX_ARDUINO diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini index ed26598d2..bf605ca61 100644 --- a/variants/esp32c6/m5stack_unitc6l/platformio.ini +++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini @@ -23,8 +23,6 @@ build_unflags = -D HAS_WIFI lib_deps = ${esp32c6_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@2.3.7 build_flags = @@ -43,4 +41,4 @@ lib_ignore = NonBlockingRTTTL libpax build_src_filter = - ${esp32c6_base.build_src_filter} +<../variants/esp32c6/m5stack_unitc6l> \ No newline at end of file + ${esp32c6_base.build_src_filter} +<../variants/esp32c6/m5stack_unitc6l> diff --git a/variants/esp32c6/m5stack_unitc6l/variant.h b/variants/esp32c6/m5stack_unitc6l/variant.h index d973aa281..1654ee590 100644 --- a/variants/esp32c6/m5stack_unitc6l/variant.h +++ b/variants/esp32c6/m5stack_unitc6l/variant.h @@ -50,3 +50,5 @@ void c6l_init(); #endif #define SCREEN_TRANSITION_FRAMERATE 10 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness + +#define SERIAL_PRINT_PORT 1 diff --git a/variants/esp32c6/tlora_c6/variant.h b/variants/esp32c6/tlora_c6/variant.h index 55635fe13..fcc5b9813 100644 --- a/variants/esp32c6/tlora_c6/variant.h +++ b/variants/esp32c6/tlora_c6/variant.h @@ -1,7 +1,7 @@ #define I2C_SDA 8 // I2C pins for this board #define I2C_SCL 9 -#define LED_PIN 7 // If defined we will blink this LED +#define LED_POWER 7 // If defined we will blink this LED #define LED_STATE_ON 0 // State when LED is lit #define USE_SX1262 @@ -19,3 +19,5 @@ #define SX126X_TXEN 14 #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define SERIAL_PRINT_PORT 1 diff --git a/variants/esp32s2/nugget_s2_lora/variant.h b/variants/esp32s2/nugget_s2_lora/variant.h index 2d123d603..9d88882e5 100644 --- a/variants/esp32s2/nugget_s2_lora/variant.h +++ b/variants/esp32s2/nugget_s2_lora/variant.h @@ -1,7 +1,7 @@ #define I2C_SDA 34 // I2C pins for this board #define I2C_SCL 36 -#define LED_PIN 15 // If defined we will blink this LED +#define LED_POWER 15 // If defined we will blink this LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 3 // How many neopixels are connected diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h index 1591f6395..5decc7eb2 100644 --- a/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h @@ -1,7 +1,7 @@ // EByte EoRA-Hub // Uses E80 (LR1121) LoRa module -#define LED_PIN 35 +#define LED_POWER 35 // Button - user interface #define BUTTON_PIN 0 // BOOT button diff --git a/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h b/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h index 5da99667b..85321cbe0 100644 --- a/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h +++ b/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h @@ -1,5 +1,5 @@ // LED - status indication -#define LED_PIN 37 +#define LED_POWER 37 // Button - user interface #define BUTTON_PIN 0 // This is the BOOT button, and it has its own pull-up resistor diff --git a/variants/esp32s3/EBYTE_ESP32-S3/variant.h b/variants/esp32s3/EBYTE_ESP32-S3/variant.h index 80fb26434..6dbe6231c 100644 --- a/variants/esp32s3/EBYTE_ESP32-S3/variant.h +++ b/variants/esp32s3/EBYTE_ESP32-S3/variant.h @@ -100,7 +100,7 @@ */ // Status -#define LED_PIN 1 +#define LED_POWER 1 #define LED_STATE_ON 1 // State when LED is lit // External notification // FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the diff --git a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h index ff4f883fe..c8e56426f 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h @@ -1,6 +1,3 @@ -// Status -#define LED_PIN 1 - #define PIN_BUTTON1 47 // 功能键 #define PIN_BUTTON2 4 // 电源键 #define ALT_BUTTON_PIN PIN_BUTTON2 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini index ee51018d4..9f8c3a871 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini @@ -31,6 +31,8 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino maxpromer/PCA9557-arduino@1.0.0 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 \ No newline at end of file diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp index 4b485a1a3..51a91bef0 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp @@ -1,12 +1,23 @@ #include "variant.h" #include -PCA9557 io(0x18, &Wire); +PCA9557 io(0x18, &Wire1); void earlyInitVariant() { - Wire.begin(48, 47); + Wire1.begin(48, 47); io.pinMode(PCA_PIN_EINK_EN, OUTPUT); io.pinMode(PCA_PIN_POWER_EN, OUTPUT); + io.pinMode(PCA_LED_POWER, OUTPUT); + io.pinMode(PCA_LED_NOTIFICATION, OUTPUT); + io.pinMode(PCA_LED_ENABLE, OUTPUT); + io.digitalWrite(PCA_PIN_POWER_EN, HIGH); + io.digitalWrite(PCA_LED_NOTIFICATION, LOW); + io.digitalWrite(PCA_LED_ENABLE, LOW); +} + +void variant_shutdown() +{ + io.digitalWrite(PCA_PIN_POWER_EN, LOW); } diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h index 77a64f717..2d02c7f27 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h @@ -8,15 +8,17 @@ // LED // Both of these are on the GPIO expander -#define PCA_LED_USER 1 // the Blue LED -#define PCA_LED_POWER 3 // the Red LED? Seems to have hardware logic to blink when USB is plugged in. +#define PCA_LED_NOTIFICATION 1 // the Blue LED +#define PCA_LED_ENABLE 2 // the power supply to the LEDs, in an OR arrangement with VBUS power +#define PCA_LED_POWER 3 // the Red LED? Seems to have hardware logic to blink when USB is plugged in. +#define POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING // USB_CHECK #define EXT_PWR_DETECT 12 #define BATTERY_PIN 8 #define ADC_CHANNEL ADC1_GPIO8_CHANNEL -#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_MULTIPLIER 2.0 // 2.0 + 10% for correction of display undervoltage. #define PIN_BUZZER 9 @@ -30,6 +32,9 @@ #define I2C_SCL 1 #define I2C_SDA 2 +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + // GPS pins #define GPS_SWITH 10 #define HAS_GPS 1 @@ -81,4 +86,7 @@ #define BUTTON_PIN PIN_BUTTON1 #define BUTTON_PIN_ALT PIN_BUTTON2 +#define ALT_BUTTON_WAKE + +#define SERIAL_PRINT_PORT 0 #endif diff --git a/variants/esp32s3/bpi_picow_esp32_s3/variant.h b/variants/esp32s3/bpi_picow_esp32_s3/variant.h index d8d9413d7..d3e573645 100644 --- a/variants/esp32s3/bpi_picow_esp32_s3/variant.h +++ b/variants/esp32s3/bpi_picow_esp32_s3/variant.h @@ -11,7 +11,7 @@ #define I2C_SDA 12 #define I2C_SCL 14 -#define LED_PIN 46 +#define LED_POWER 46 #define LED_STATE_ON 0 // State when LED is litted // #define BUTTON_PIN 15 // Pico OLED 1.3 User key 0 - removed User key 1 (17) diff --git a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini index 7a0bd31b4..7e37a0eb4 100644 --- a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini +++ b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini @@ -26,7 +26,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:crowpanel-esp32s3-4-epaper] extends = esp32s3_base @@ -56,7 +56,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:crowpanel-esp32s3-2-epaper] extends = esp32s3_base @@ -86,4 +86,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip diff --git a/variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h b/variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h index 360e33481..c9200b96b 100644 --- a/variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h +++ b/variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h @@ -26,7 +26,7 @@ // #define GPS_RX_PIN 44 // #define GPS_TX_PIN 43 -#define LED_PIN 41 +#define LED_POWER 41 #define BUTTON_PIN 2 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini index f44f26006..90e4910f4 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini @@ -11,9 +11,7 @@ upload_speed = 921600 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + zinggjm/GxEPD2@1.6.8 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h b/variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h index 024f912dd..54db932ea 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h @@ -11,7 +11,7 @@ #define I2C_SDA 18 // 1 // I2C pins for this board #define I2C_SCL 17 // 2 -// #define LED_PIN 38 // This is a RGB LED not a standard LED +// #define LED_POWER 38 // This is a RGB LED not a standard LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected #define NEOPIXEL_DATA 38 // gpio pin used to send data to the neopixels diff --git a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini index 4f759d1e6..60b030d42 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini @@ -8,10 +8,6 @@ board_build.f_cpu = 240000000L upload_protocol = esptool ;upload_port = /dev/ttyACM0 upload_speed = 921600 -lib_deps = - ${esp32s3_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h b/variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h index 8a3a39003..20e058058 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h +++ b/variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h @@ -11,7 +11,7 @@ #define I2C_SDA 18 // 1 // I2C pins for this board #define I2C_SCL 17 // 2 -// #define LED_PIN 38 // This is a RGB LED not a standard LED +// #define LED_POWER 38 // This is a RGB LED not a standard LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected #define NEOPIXEL_DATA 38 // gpio pin used to send data to the neopixels diff --git a/variants/esp32s3/dreamcatcher/platformio.ini b/variants/esp32s3/dreamcatcher/platformio.ini index c830346e0..64d1b993d 100644 --- a/variants/esp32s3/dreamcatcher/platformio.ini +++ b/variants/esp32s3/dreamcatcher/platformio.ini @@ -12,8 +12,8 @@ build_flags = -D ARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s3_base.lib_deps} - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/dreamcatcher/variant.h b/variants/esp32s3/dreamcatcher/variant.h index 7835979e1..963aff477 100644 --- a/variants/esp32s3/dreamcatcher/variant.h +++ b/variants/esp32s3/dreamcatcher/variant.h @@ -6,7 +6,7 @@ #define I2C_SDA1 45 #define I2C_SCL1 46 -#define LED_PIN 6 +#define LED_POWER 6 #define LED_STATE_ON 1 #define BUTTON_PIN 0 diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index e0f6f0760..1b91a02bb 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -41,8 +41,8 @@ build_flags = ${esp32s3_base.build_flags} -Os lib_deps = ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=TCA9534 packageName=hideakitai/library/TCA9534 @@ -84,6 +84,7 @@ custom_meshtastic_images = crowpanel_2_4.svg, crowpanel_2_8.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = crowpanel_small_esp32s3_base build_flags = @@ -119,6 +120,7 @@ custom_meshtastic_images = crowpanel_3_5.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = crowpanel_small_esp32s3_base board_level = pr @@ -158,6 +160,7 @@ custom_meshtastic_images = crowpanel_5_0.svg, crowpanel_7_0.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = crowpanel_large_esp32s3_base build_flags = diff --git a/variants/esp32s3/esp32-s3-pico/platformio.ini b/variants/esp32s3/esp32-s3-pico/platformio.ini index db0c038e6..64f50f80e 100644 --- a/variants/esp32s3/esp32-s3-pico/platformio.ini +++ b/variants/esp32s3/esp32-s3-pico/platformio.ini @@ -23,6 +23,4 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + zinggjm/GxEPD2@1.6.8 diff --git a/variants/esp32s3/esp32-s3-pico/variant.h b/variants/esp32s3/esp32-s3-pico/variant.h index bfcb6059d..65732171a 100644 --- a/variants/esp32s3/esp32-s3-pico/variant.h +++ b/variants/esp32s3/esp32-s3-pico/variant.h @@ -8,7 +8,6 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 0 // 17 -// #define LED_PIN PIN_LED // Board has RGB LED 21 #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected diff --git a/variants/esp32s3/hackaday-communicator/variant.h b/variants/esp32s3/hackaday-communicator/variant.h index a127f548f..cca408622 100644 --- a/variants/esp32s3/hackaday-communicator/variant.h +++ b/variants/esp32s3/hackaday-communicator/variant.h @@ -16,22 +16,12 @@ #define SLEEP_TIME 120 #define GPS_DEFAULT_NOT_PRESENT 1 -// #define GPS_RX_PIN 44 -// #define GPS_TX_PIN 43 - -// #define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage -// ratio of voltage divider = 2.0 (RD2=100k, RD3=100k) -// #define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. -// #define ADC_CHANNEL ADC1_GPIO4_CHANNEL // keyboard #define I2C_SDA 47 // I2C pins for this board #define I2C_SCL 14 -// #define KB_POWERON -1 // must be set to HIGH -// #define KB_SLAVE_ADDRESS TDECK_KB_ADDR // 0x55 // #define KB_BL_PIN 46 // not used for now #define KB_INT 13 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define TFT_DC 39 #define TFT_CS 41 @@ -44,11 +34,9 @@ #define LORA_MOSI 3 #define LORA_CS 17 -// #define LORA_DIO0 -1 // a No connect on the SX1262 module #define LORA_RESET 18 #define LORA_DIO1 16 // SX1262 IRQ #define LORA_DIO2 15 // SX1262 BUSY -// #define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled #define SX126X_CS LORA_CS #define SX126X_DIO1 LORA_DIO1 @@ -58,4 +46,5 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// #define LED_PIN 1 \ No newline at end of file +#define LED_NOTIFICATION 1 +#define LED_STATE_ON 0 diff --git a/variants/esp32s3/heltec_capsule_sensor_v3/variant.h b/variants/esp32s3/heltec_capsule_sensor_v3/variant.h index b30b7fc3e..3ee5545a8 100644 --- a/variants/esp32s3/heltec_capsule_sensor_v3/variant.h +++ b/variants/esp32s3/heltec_capsule_sensor_v3/variant.h @@ -1,5 +1,5 @@ -#define LED_PIN 33 -#define LED_PIN2 34 +#define LED_POWER 33 +#define LED_POWER2 34 #define EXT_PWR_DETECT 35 #define BUTTON_PIN 18 diff --git a/variants/esp32s3/heltec_sensor_hub/platformio.ini b/variants/esp32s3/heltec_sensor_hub/platformio.ini index ded0c22fe..ab99e51ed 100644 --- a/variants/esp32s3/heltec_sensor_hub/platformio.ini +++ b/variants/esp32s3/heltec_sensor_hub/platformio.ini @@ -7,7 +7,3 @@ build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_sensor_hub -D HELTEC_SENSOR_HUB - -lib_deps = ${esp32s3_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32s3/heltec_v3/variant.h b/variants/esp32s3/heltec_v3/variant.h index d760c3b7f..d2d904d9c 100644 --- a/variants/esp32s3/heltec_v3/variant.h +++ b/variants/esp32s3/heltec_v3/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN LED +#define LED_POWER LED #define USE_SSD1306 // Heltec_v3 has a SSD1306 display diff --git a/variants/esp32s3/heltec_v4/pins_arduino.h b/variants/esp32s3/heltec_v4/pins_arduino.h index d4485016d..32fd8a8e4 100644 --- a/variants/esp32s3/heltec_v4/pins_arduino.h +++ b/variants/esp32s3/heltec_v4/pins_arduino.h @@ -6,10 +6,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 35; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 4495a409f..5a5004a45 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -6,6 +6,7 @@ board_build.partitions = default_16MB.csv build_flags = ${esp32s3_base.build_flags} -D HELTEC_V4 + -D HAS_LORA_FEM=1 -I variants/esp32s3/heltec_v4 @@ -20,13 +21,14 @@ custom_meshtastic_images = heltec_v4.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = heltec_v4_base build_flags = ${heltec_v4_base.build_flags} -D HELTEC_V4_OLED -D USE_SSD1306 ; Heltec_v4 has an SSD1315 display (compatible with SSD1306 driver) - -D LED_PIN=35 + -D LED_POWER=35 -D RESET_OLED=21 -D I2C_SDA=17 -D I2C_SCL=18 @@ -66,7 +68,10 @@ build_flags = -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SCREEN=1 -D HAS_TFT=1 - -D RAM_SIZE=1560 + -D MAP_TILES_GREY ; required for 2MB PSRAM + -D RAM_SIZE=1432 + -D STBI_ARENA_SIZE=450000 + -D LV_CACHE_DEF_SIZE=0 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE @@ -81,9 +86,9 @@ build_flags = -D USE_PACKET_API -D LGFX_DRIVER=LGFX_HELTEC_V4_TFT -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_HELTEC_V4_TFT.h\" - -D VIEW_320x240 - -D MAP_FULL_REDRAW - -D DISPLAY_SIZE=320x240 ; landscape mode + -D VIEW_240x320 + -D DISPLAY_SET_RESOLUTION + -D DISPLAY_SIZE=240x320 ; portrait mode -D LGFX_PIN_SCK=17 -D LGFX_PIN_MOSI=33 -D LGFX_PIN_DC=16 @@ -124,11 +129,8 @@ build_flags = -D SCREEN_TOUCH_RST=TOUCH_RST_PIN lib_deps = ${heltec_v4_base.lib_deps} - ; ${device-ui_base.lib_deps} + ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.0 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master - https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip - ; TODO revert to official device-ui (when merged) - # renovate: datasource=git-refs depName=Quency-D_device-ui packageName=https://github.com/Quency-D/device-ui gitBranch=heltec-v4-tft - https://github.com/Quency-D/device-ui/archive/7c9870b8016641190b059bdd90fe16c1012a39eb.zip + https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip \ No newline at end of file diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h index 1c1168d94..72f55d09f 100644 --- a/variants/esp32s3/heltec_v4/variant.h +++ b/variants/esp32s3/heltec_v4/variant.h @@ -29,9 +29,17 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 +// Enable Traffic Management Module for Heltec V4 +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 1 +#endif +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 +#endif + // ---- GC1109 RF FRONT END CONFIGURATION ---- -// The Heltec V4 uses a GC1109 FEM chip with integrated PA and LNA -// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna +// The Heltec V4.2 uses a GC1109 FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> GC1109 PA -> Antenna // Measured net TX gain (non-linear due to PA compression): // +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) // +10dB at 16-17dBm input @@ -47,15 +55,31 @@ // CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) // CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 -#define USE_GC1109_PA -#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable -#define LORA_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) -#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) - // GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) -// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp // Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 and KCT8103L LDO power enable +#define LORA_GC1109_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) +#define LORA_GC1109_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) + +// ---- KCT8103L RF FRONT END CONFIGURATION ---- +// The Heltec V4.3 uses a KCT8103L FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna +// Control logic (from KCT8103L datasheet): +// Transmit PA: CSD=1, CTX=1, CPS=1 +// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) +// Receive bypass: CSD=1, CTX=1, CPS=0 +// Shutdown: CSD=0, CTX=X, CPS=X +// Pin mapping: +// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) +// CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 +// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) + +#define LORA_KCT8103L_PA_CSD 2 // CSD - KCT8103L chip enable (HIGH=on) +#define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) + #if HAS_TFT #define USE_TFTDISPLAY 1 #endif @@ -73,4 +97,4 @@ // Seems to be missing on this new board #define GPS_TX_PIN (38) // This is for bits going TOWARDS the CPU #define GPS_RX_PIN (39) // This is for bits going TOWARDS the GPS -#define GPS_THREAD_INTERVAL 50 \ No newline at end of file +#define GPS_THREAD_INTERVAL 50 diff --git a/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h b/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h index 1b1291424..fb0744bc3 100644 --- a/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -87,6 +88,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 @@ -114,4 +116,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/heltec_vision_master_e213/pins_arduino.h b/variants/esp32s3/heltec_vision_master_e213/pins_arduino.h index 56f5ef157..5cf3c6453 100644 --- a/variants/esp32s3/heltec_vision_master_e213/pins_arduino.h +++ b/variants/esp32s3/heltec_vision_master_e213/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant -#define BUILTIN_LED LED_BUILTIN // Backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_vision_master_e213/platformio.ini b/variants/esp32s3/heltec_vision_master_e213/platformio.ini index 4ace5a45a..bd1b73d2b 100644 --- a/variants/esp32s3/heltec_vision_master_e213/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e213/platformio.ini @@ -9,6 +9,7 @@ custom_meshtastic_images = heltec-vision-master-e213.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_ink_hud = true extends = esp32s3_base board = heltec_vision_master_e213 @@ -29,7 +30,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-vision-master-e213-inkhud] diff --git a/variants/esp32s3/heltec_vision_master_e213/variant.h b/variants/esp32s3/heltec_vision_master_e213/variant.h index 60f4e00cc..c9aaa2ee8 100644 --- a/variants/esp32s3/heltec_vision_master_e213/variant.h +++ b/variants/esp32s3/heltec_vision_master_e213/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 45 // LED is not populated on earliest board variant +#define LED_POWER 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 #define PIN_BUTTON2 21 // Second built-in button #define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event diff --git a/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h b/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h index 61b08c740..a90500b15 100644 --- a/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h @@ -24,6 +24,7 @@ Different NicheGraphics UIs and different hardware variants will each have their // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -84,6 +85,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 @@ -111,4 +113,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/heltec_vision_master_e290/pins_arduino.h b/variants/esp32s3/heltec_vision_master_e290/pins_arduino.h index 56f5ef157..5cf3c6453 100644 --- a/variants/esp32s3/heltec_vision_master_e290/pins_arduino.h +++ b/variants/esp32s3/heltec_vision_master_e290/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 45; // LED is not populated on earliest board variant -#define BUILTIN_LED LED_BUILTIN // Backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_vision_master_e290/platformio.ini b/variants/esp32s3/heltec_vision_master_e290/platformio.ini index e86746b67..9fdb023de 100644 --- a/variants/esp32s3/heltec_vision_master_e290/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e290/platformio.ini @@ -10,6 +10,7 @@ custom_meshtastic_images = heltec-vision-master-e290.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_ink_hud = true extends = esp32s3_base board = heltec_vision_master_e290 @@ -32,7 +33,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-vision-master-e290-inkhud] diff --git a/variants/esp32s3/heltec_vision_master_e290/variant.h b/variants/esp32s3/heltec_vision_master_e290/variant.h index d7bae7dc2..b32715e39 100644 --- a/variants/esp32s3/heltec_vision_master_e290/variant.h +++ b/variants/esp32s3/heltec_vision_master_e290/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 45 // LED is not populated on earliest board variant +#define LED_POWER 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 #define PIN_BUTTON2 21 // Second built-in button #define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event diff --git a/variants/esp32s3/heltec_vision_master_t190/pins_arduino.h b/variants/esp32s3/heltec_vision_master_t190/pins_arduino.h index eeef95ff1..b8a33f721 100644 --- a/variants/esp32s3/heltec_vision_master_t190/pins_arduino.h +++ b/variants/esp32s3/heltec_vision_master_t190/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 35; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_vision_master_t190/platformio.ini b/variants/esp32s3/heltec_vision_master_t190/platformio.ini index bbc518b39..3dab9f93c 100644 --- a/variants/esp32s3/heltec_vision_master_t190/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_t190/platformio.ini @@ -20,5 +20,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip + https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip upload_speed = 921600 diff --git a/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h b/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h index 445b57714..9e84a541e 100644 --- a/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h +++ b/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -87,6 +88,7 @@ void setupNicheGraphics() inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); @@ -107,4 +109,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/heltec_wireless_paper/pins_arduino.h b/variants/esp32s3/heltec_wireless_paper/pins_arduino.h index 3e36d98f5..886cab254 100644 --- a/variants/esp32s3/heltec_wireless_paper/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_paper/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_wireless_paper/platformio.ini b/variants/esp32s3/heltec_wireless_paper/platformio.ini index 673c834ea..acd619c48 100644 --- a/variants/esp32s3/heltec_wireless_paper/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper/platformio.ini @@ -9,6 +9,7 @@ custom_meshtastic_display_name = Heltec Wireless Paper custom_meshtastic_images = heltec-wireless-paper.svg custom_meshtastic_tags = Heltec custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_ink_hud = true extends = esp32s3_base board = heltec_wifi_lora_32_V3 @@ -29,7 +30,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-wireless-paper-inkhud] diff --git a/variants/esp32s3/heltec_wireless_paper/variant.h b/variants/esp32s3/heltec_wireless_paper/variant.h index bbfd54ada..7f57bb67f 100644 --- a/variants/esp32s3/heltec_wireless_paper/variant.h +++ b/variants/esp32s3/heltec_wireless_paper/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define BUTTON_PIN 0 // I2C diff --git a/variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h b/variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h index 2bb44161a..0c486eebc 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_paper_v1/pins_arduino.h @@ -3,10 +3,6 @@ #include -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t KEY_BUILTIN = 0; static const uint8_t TX = 43; diff --git a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini index 8543e414f..b34adfb17 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini @@ -26,5 +26,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 diff --git a/variants/esp32s3/heltec_wireless_paper_v1/variant.h b/variants/esp32s3/heltec_wireless_paper_v1/variant.h index 4505395c9..59dd485f6 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/variant.h +++ b/variants/esp32s3/heltec_wireless_paper_v1/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define BUTTON_PIN 0 // I2C diff --git a/variants/esp32s3/heltec_wireless_tracker/pins_arduino.h b/variants/esp32s3/heltec_wireless_tracker/pins_arduino.h index 1052af961..93fd5d9c2 100644 --- a/variants/esp32s3/heltec_wireless_tracker/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_tracker/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_wireless_tracker/platformio.ini b/variants/esp32s3/heltec_wireless_tracker/platformio.ini index c2dab0c93..33643c541 100644 --- a/variants/esp32s3/heltec_wireless_tracker/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker/platformio.ini @@ -24,4 +24,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/heltec_wireless_tracker/variant.h b/variants/esp32s3/heltec_wireless_tracker/variant.h index 3b19f5afd..b40e40011 100644 --- a/variants/esp32s3/heltec_wireless_tracker/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define _VARIANT_HELTEC_WIRELESS_TRACKER #define HELTEC_TRACKER_V1_X diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h b/variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h index 28b982012..7a1f43eee 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini index efeb63b8e..ab6592afb 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini @@ -22,4 +22,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h b/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h index df5ab4716..e7d3f93c1 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define HELTEC_TRACKER_V1_X diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h b/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h index 61c319109..d30e2fcb7 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h @@ -10,15 +10,11 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; -static const uint8_t SDA = 5; -static const uint8_t SCL = 6; +static const uint8_t SDA = 6; +static const uint8_t SCL = 17; static const uint8_t SS = 8; static const uint8_t MOSI = 10; diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini index 0b486618b..ebf0118bb 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini @@ -17,7 +17,8 @@ build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_wireless_tracker_v2 -D HELTEC_WIRELESS_TRACKER_V2 + -D HAS_LORA_FEM=1 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h index a5489173d..7c797a503 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define _VARIANT_HELTEC_WIRELESS_TRACKER @@ -73,29 +73,22 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// ---- GC1109 RF FRONT END CONFIGURATION ---- -// The Heltec Wireless Tracker V2 uses a GC1109 FEM chip with integrated PA and LNA -// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna -// Measured net TX gain (non-linear due to PA compression): -// +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) -// +10dB at 16-17dBm input -// +9dB at 18-19dBm input -// +7dB at 21dBm input (e.g., 21dBm in -> 28dBm out max) -// Control logic (from GC1109 datasheet): +// ---- KCT8103L RF FRONT END CONFIGURATION ---- +// The heltec_wireless_tracker_v2 uses a KCT8103L FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna +// Control logic (from KCT8103L datasheet): +// Transmit PA: CSD=1, CTX=1, CPS=1 +// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) +// Receive bypass: CSD=1, CTX=1, CPS=0 // Shutdown: CSD=0, CTX=X, CPS=X -// Receive LNA: CSD=1, CTX=0, CPS=X (17dB gain, 2dB NF) -// Transmit bypass: CSD=1, CTX=1, CPS=0 (~1dB loss, no PA) -// Transmit PA: CSD=1, CTX=1, CPS=1 (full PA enabled) // Pin mapping: -// CTX (pin 6) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) // CSD (pin 4) -> GPIO4: Chip enable (HIGH=on, LOW=shutdown) -// CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) +// CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 -#define USE_GC1109_PA -#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable -#define LORA_PA_EN 4 // CSD - GC1109 chip enable (HIGH=on) -#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) +// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) -// GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) -// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp -// Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 \ No newline at end of file +#define USE_KCT8103L_PA +#define LORA_PA_POWER 7 // VFEM_Ctrl - KCT8103L LDO power enable +#define LORA_KCT8103L_PA_CSD 4 // CSD - KCT8103L chip enable (HIGH=on) +#define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) \ No newline at end of file diff --git a/variants/esp32s3/heltec_wsl_v3/variant.h b/variants/esp32s3/heltec_wsl_v3/variant.h index c103b9172..c81f45d3b 100644 --- a/variants/esp32s3/heltec_wsl_v3/variant.h +++ b/variants/esp32s3/heltec_wsl_v3/variant.h @@ -1,7 +1,7 @@ #define I2C_SCL SCL #define I2C_SDA SDA -#define LED_PIN LED +#define LED_POWER LED #define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost #define VEXT_ON_VALUE LOW diff --git a/variants/esp32s3/link32_s3_v1/platformio.ini b/variants/esp32s3/link32_s3_v1/platformio.ini index acce3dafb..b11ffaad0 100644 --- a/variants/esp32s3/link32_s3_v1/platformio.ini +++ b/variants/esp32s3/link32_s3_v1/platformio.ini @@ -11,3 +11,4 @@ build_flags = -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 diff --git a/variants/esp32s3/m5stack_cardputer_adv/pins_arduino.h b/variants/esp32s3/m5stack_cardputer_adv/pins_arduino.h new file mode 100644 index 000000000..12581cde0 --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/pins_arduino.h @@ -0,0 +1,20 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define USB_VID 0x303a // USB JTAG/serial debug unit ID +#define USB_PID 0x1001 // USB JTAG/serial debug unit ID + +static const uint8_t SS = 5; +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; +static const uint8_t ADC = 10; +static const uint8_t TXD2 = 13; +static const uint8_t MOSI = 14; +static const uint8_t RXD2 = 15; +static const uint8_t MISO = 39; +static const uint8_t SCK = 40; + +#endif diff --git a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini new file mode 100644 index 000000000..3b378ed94 --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini @@ -0,0 +1,25 @@ +; M5stack Cardputer Advanced +[env:m5stack-cardputer-adv] +extends = esp32s3_base +board = m5stack-stamps3 +board_check = true +board_build.partitions = default_8MB.csv +upload_protocol = esptool +build_flags = + ${esp32s3_base.build_flags} + -D M5STACK_CARDPUTER_ADV + -D ARDUINO_USB_CDC_ON_BOOT=1 + -I variants/esp32s3/m5stack_cardputer_adv +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/m5stack_cardputer_adv> +lib_deps = + ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main + https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip + # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/m5stack_cardputer_adv/variant.cpp b/variants/esp32s3/m5stack_cardputer_adv/variant.cpp new file mode 100644 index 000000000..2bbe8e2e3 --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/variant.cpp @@ -0,0 +1,40 @@ +#include "AudioBoard.h" +#include "configuration.h" + +DriverPins PinsAudioBoardES8311; +AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); + +// M5stack Cardputer ADV specific init + +void lateInitVariant() +{ + // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); + // I2C: function, scl, sda + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + // I2S: function, mclk, bck, ws, data_out, data_in + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); + + // configure codec + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + board.begin(cfg); + + // extra ES8311 init + auto es8311_write_reg = [](uint8_t reg, uint8_t val) { + Wire.beginTransmission(0x18); // ES8311 i2c address + Wire.write(reg); + Wire.write(val); + Wire.endTransmission(); + }; + es8311_write_reg(0x00, 0x80); // reset, power on + es8311_write_reg(0x01, 0xB5); // MCLK = BCLK + es8311_write_reg(0x02, 0x18); // CLOCK_MANAGER/ MULT_PRE=3 + es8311_write_reg(0x0D, 0x01); // analog power up + es8311_write_reg(0x12, 0x00); // DAC power up + es8311_write_reg(0x13, 0x10); // enable HP drive + es8311_write_reg(0x32, 0xBF); // DAC volume (0dB) + es8311_write_reg(0x37, 0x08); // EQ bypass +} diff --git a/variants/esp32s3/m5stack_cardputer_adv/variant.h b/variants/esp32s3/m5stack_cardputer_adv/variant.h new file mode 100644 index 000000000..5fdb1436e --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/variant.h @@ -0,0 +1,90 @@ +#define USE_ST7789 + +#define ST7789_NSS 37 +#define ST7789_RS 34 // DC +#define ST7789_SDA 35 // MOSI +#define ST7789_SCK 36 +#define ST7789_RESET 33 +#define ST7789_MISO -1 +#define ST7789_BUSY -1 +// #define VTFT_CTRL 38 +#define VTFT_LEDA 38 +// #define ST7789_BL (32+6) +#define ST7789_SPI_HOST SPI2_HOST +// #define TFT_BL (32+6) +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 135 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define HAS_PHYSICAL_KEYBOARD 1 + +// Backlight is controlled to power rail on this board, this also powers the neopixel +// #define PIN_POWER_EN 38 + +#define BUTTON_PIN 0 + +#define I2C_SDA 8 +#define I2C_SCL 9 + +#define I2C_SDA1 2 +#define I2C_SCL1 1 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 40 +#define LORA_MISO 39 +#define LORA_MOSI 14 +#define LORA_CS 5 // NSS + +#define USE_SX1262 +#define LORA_DIO0 -1 +#define LORA_RESET 3 +#define LORA_RST 3 +#define LORA_DIO1 4 +#define LORA_DIO2 6 +#define LORA_DIO3 RADIOLIB_NC + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 15 +#define GPS_TX_PIN 13 +#define HAS_GPS 1 +#define GPS_BAUDRATE 115200 + +// audio codec ES8311 +#define HAS_I2S +#define DAC_I2S_BCK 41 +#define DAC_I2S_WS 43 +#define DAC_I2S_DOUT 42 +#define DAC_I2S_DIN 46 +#define DAC_I2S_MCLK 45 // dummy + +// TCA8418 keyboard +#define I2C_NO_RESCAN +#define KB_INT 11 + +#define HAS_NEOPIXEL +#define NEOPIXEL_COUNT 1 +#define NEOPIXEL_DATA 21 +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) + +#define BATTERY_PIN 10 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO10_CHANNEL +#define ADC_MULTIPLIER 2 * 1.02 // 100k + 100k, and add 2% to kick the voltage over the max voltage to show charging. + +// BMI270 6-axis IMU on internal I2C bus +#define HAS_BMI270 diff --git a/variants/esp32s3/m5stack_cores3/pins_arduino.h b/variants/esp32s3/m5stack_cores3/pins_arduino.h index 78e936990..ff7d35993 100644 --- a/variants/esp32s3/m5stack_cores3/pins_arduino.h +++ b/variants/esp32s3/m5stack_cores3/pins_arduino.h @@ -10,10 +10,7 @@ // Some boards have too low voltage on this pin (board design bug) // Use different pin with 3V and connect with 48 // and change this setup for the chosen pin (for example 38) -static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + 48; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN -#define RGB_BUILTIN LED_BUILTIN +#define RGB_BUILTIN SOC_GPIO_PIN_COUNT + 48 #define RGB_BRIGHTNESS 64 static const uint8_t TX = 43; diff --git a/variants/esp32s3/mesh-tab/pins_arduino.h b/variants/esp32s3/mesh-tab/pins_arduino.h index c995f638c..d980e1a49 100644 --- a/variants/esp32s3/mesh-tab/pins_arduino.h +++ b/variants/esp32s3/mesh-tab/pins_arduino.h @@ -49,10 +49,6 @@ static const uint8_t T14 = 14; static const uint8_t VBAT_SENSE = 2; static const uint8_t VBUS_SENSE = 34; -// User LED -#define LED_BUILTIN 13 -#define BUILTIN_LED LED_BUILTIN // backward compatibility - static const uint8_t RGB_DATA = 40; // RGB_BUILTIN and RGB_BRIGHTNESS can be used in new Arduino API neopixelWrite() #define RGB_BUILTIN (RGB_DATA + SOC_GPIO_PIN_COUNT) diff --git a/variants/esp32s3/mesh-tab/platformio.ini b/variants/esp32s3/mesh-tab/platformio.ini index 21d8fb432..a153ba9fb 100644 --- a/variants/esp32s3/mesh-tab/platformio.ini +++ b/variants/esp32s3/mesh-tab/platformio.ini @@ -12,6 +12,7 @@ build_flags = ${esp32s3_base.build_flags} -D CONFIG_ARDUHAL_ESP_LOG -D CONFIG_ARDUHAL_LOG_COLORS=1 -D CONFIG_DISABLE_HAL_LOCKS=1 + -D MESHTASTIC_EXCLUDE_SCREEN=1 -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 -D MESHTASTIC_EXCLUDE_BLUETOOTH=1 @@ -31,7 +32,10 @@ build_flags = ${esp32s3_base.build_flags} -D HAS_SCREEN=0 -D HAS_TFT=1 -D USE_PIN_BUZZER - -D RAM_SIZE=1024 + -D MAP_TILES_GREY ; required for 2MB PSRAM + -D RAM_SIZE=1432 + -D STBI_ARENA_SIZE=450000 + -D LV_CACHE_DEF_SIZE=0 -D LGFX_DRIVER_TEMPLATE -D LGFX_DRIVER=LGFX_GENERIC -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\" @@ -51,7 +55,7 @@ lib_deps = ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 [mesh_tab_xpt2046] extends = mesh_tab_base diff --git a/variants/esp32s3/mesh-tab/variant.h b/variants/esp32s3/mesh-tab/variant.h index 99204bba3..30042b90f 100644 --- a/variants/esp32s3/mesh-tab/variant.h +++ b/variants/esp32s3/mesh-tab/variant.h @@ -13,7 +13,7 @@ #define ADC_CHANNEL ADC1_GPIO4_CHANNEL // LED -#define LED_PIN 21 +#define LED_POWER 21 // Button #define BUTTON_PIN 0 diff --git a/variants/esp32s3/mini-epaper-s3/nicheGraphics.h b/variants/esp32s3/mini-epaper-s3/nicheGraphics.h new file mode 100644 index 000000000..0cbd6a192 --- /dev/null +++ b/variants/esp32s3/mini-epaper-s3/nicheGraphics.h @@ -0,0 +1,131 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +#include "graphics/niche/InkHUD/InkHUD.h" + +// Applets +#include "graphics/niche/InkHUD/Applet.h" +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" +#include "graphics/niche/InkHUD/SystemApplet.h" + +// Shared NicheGraphics components +#include "graphics/niche/Drivers/EInk/GDEW0102T4.h" +#include "graphics/niche/Inputs/TwoButtonExtended.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // Power-enable the E-Ink panel on this board before any SPI traffic. + pinMode(PIN_EINK_EN, OUTPUT); + digitalWrite(PIN_EINK_EN, HIGH); + delay(10); + + // Display uses HSPI on this board + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + Drivers::GDEW0102T4 *displayDriver = new Drivers::GDEW0102T4; + displayDriver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + // Tuned fast-refresh values reg30 reg50 reg82 lutW2 lutB2 = 11 F2 04 11 0D + displayDriver->setFastConfig({0x11, 0xF2, 0x04, 0x11, 0x0D}); + Drivers::EInk *driver = displayDriver; + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + inkhud->setDriver(driver); + // Slightly stricter FAST/FULL + inkhud->setDisplayResilience(5, 1.5); + inkhud->twoWayRocker = true; + + // Fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_6PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Small display defaults + inkhud->persistence->settings.rotation = 0; + inkhud->persistence->settings.userTiles.maxCount = 1; + inkhud->persistence->settings.userTiles.count = 1; + inkhud->persistence->settings.joystick.enabled = true; + inkhud->persistence->settings.joystick.aligned = true; + inkhud->persistence->settings.optionalMenuItems.nextTile = false; + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, false, false); // - + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false); // Activated, not autoshown + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, true); // Activated, Autoshown + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1), false, false); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, false, false); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Start running InkHUD + inkhud->begin(); + + // Enforce two-way rocker behavior regardless of persisted settings. + inkhud->persistence->settings.joystick.enabled = true; + inkhud->persistence->settings.joystick.aligned = true; + inkhud->persistence->settings.optionalMenuItems.nextTile = false; + + // Inputs + Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); + + // Center press (boot button) + buttons->setWiring(0, INPUTDRIVER_TWO_WAY_ROCKER_BTN, true); + // Match baseUI encoder long-press feel. + buttons->setTiming(0, 75, 300); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + // LEFT rocker pin is IO4; RIGHT rocker pin is IO3. + buttons->setTwoWayRockerWiring(INPUTDRIVER_TWO_WAY_ROCKER_LEFT, INPUTDRIVER_TWO_WAY_ROCKER_RIGHT, true); + buttons->setJoystickDebounce(50); + + // Two-way rocker behavior: + // - when a system applet is handling input (menu, tips, etc): LEFT=up, RIGHT=down + // - otherwise: LEFT=previous applet, RIGHT=next applet + buttons->setTwoWayRockerPressHandlers( + [inkhud]() { + bool systemHandlingInput = false; + for (const InkHUD::SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + systemHandlingInput = true; + break; + } + } + + if (systemHandlingInput) + inkhud->navUp(); + else + inkhud->prevApplet(); + }, + [inkhud]() { + bool systemHandlingInput = false; + for (const InkHUD::SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + systemHandlingInput = true; + break; + } + } + + if (systemHandlingInput) + inkhud->navDown(); + else + inkhud->nextApplet(); + }); + + buttons->start(); +} + +#endif diff --git a/variants/esp32s3/mini-epaper-s3/pins_arduino.h b/variants/esp32s3/mini-epaper-s3/pins_arduino.h new file mode 100644 index 000000000..afb2428a0 --- /dev/null +++ b/variants/esp32s3/mini-epaper-s3/pins_arduino.h @@ -0,0 +1,25 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303A +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 18; +static const uint8_t SCL = 9; + +// Default SPI (LoRa bus) +static const uint8_t SS = -1; +static const uint8_t MOSI = 17; +static const uint8_t MISO = 6; +static const uint8_t SCK = 8; + +// SD card SPI bus +#define SPI_MOSI (39) +#define SPI_SCK (41) +#define SPI_MISO (38) +#define SPI_CS (40) + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/mini-epaper-s3/platformio.ini b/variants/esp32s3/mini-epaper-s3/platformio.ini new file mode 100644 index 000000000..3e4802319 --- /dev/null +++ b/variants/esp32s3/mini-epaper-s3/platformio.ini @@ -0,0 +1,55 @@ +[env:mini-epaper-s3] +custom_meshtastic_hw_model = 125 +custom_meshtastic_hw_model_slug = MINI_EPAPER_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO Mini ePaper S3 E-Ink +custom_meshtastic_images = mini-epaper-s3.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = no +custom_meshtastic_has_mui = false + +extends = esp32s3_base +board = mini-epaper-s3 +board_check = true +upload_protocol = esptool + +build_flags = + ${esp32s3_base.build_flags} + -I variants/esp32s3/mini-epaper-s3 + -D MINI_EPAPER_S3 + -D USE_EINK + -D EINK_DISPLAY_MODEL=GxEPD2_102 + -D EINK_WIDTH=128 + -D EINK_HEIGHT=80 + -D USE_EINK_DYNAMICDISPLAY + -D EINK_LIMIT_FASTREFRESH=3 + -D EINK_BACKGROUND_USES_FAST + -D EINK_HASQUIRK_GHOSTING + +lib_deps = + ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 + +[env:mini-epaper-s3-inkhud] +extends = esp32s3_base, inkhud +board = mini-epaper-s3 +board_check = true +upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + ${inkhud.build_src_filter} +build_flags = + ${esp32s3_base.build_flags} + ${inkhud.build_flags} + -I variants/esp32s3/mini-epaper-s3 + -D MINI_EPAPER_S3 +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/esp32s3/mini-epaper-s3/variant.h b/variants/esp32s3/mini-epaper-s3/variant.h new file mode 100644 index 000000000..0b640f9cf --- /dev/null +++ b/variants/esp32s3/mini-epaper-s3/variant.h @@ -0,0 +1,57 @@ +#pragma once + +#define GPS_DEFAULT_NOT_PRESENT 1 + +// SD card (TF) +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS 40 +#define SD_SPI_FREQUENCY 25000000U + +// Built-in RTC (I2C) +#define PCF8563_RTC 0x51 +#define HAS_RTC 1 +#define I2C_SDA SDA +#define I2C_SCL SCL + +// Battery voltage monitoring +#define BATTERY_PIN 2 // A battery voltage measurement pin, voltage divider connected here to +// measure battery voltage ratio of voltage divider = 2.0 (assumption) +#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO2_CHANNEL + +// Display (E-Ink) +#define PIN_EINK_EN 42 +#define PIN_EINK_CS 13 +#define PIN_EINK_BUSY 10 +#define PIN_EINK_DC 12 +#define PIN_EINK_RES 11 +#define PIN_EINK_SCLK 14 +#define PIN_EINK_MOSI 15 +#define DISPLAY_FORCE_SMALL_FONTS + +// Two-Way Rocker input (left/right + boot as press) +#define INPUTDRIVER_TWO_WAY_ROCKER +#define INPUTDRIVER_ENCODER_TYPE 2 +#define INPUTDRIVER_TWO_WAY_ROCKER_RIGHT 3 +#define INPUTDRIVER_TWO_WAY_ROCKER_LEFT 4 +#define INPUTDRIVER_TWO_WAY_ROCKER_BTN 0 +#define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 150 + +// LoRa (SX1262) +#define USE_SX1262 + +#define LORA_DIO1 5 +#define LORA_SCK 8 +#define LORA_MISO 6 +#define LORA_MOSI 17 +#define LORA_CS 7 // CS not connected; IO7 is free +#define LORA_RESET 21 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 16 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/variants/esp32s3/nibble_esp32/variant.h b/variants/esp32s3/nibble_esp32/variant.h index 8ffbd9d59..8d75a4fbf 100644 --- a/variants/esp32s3/nibble_esp32/variant.h +++ b/variants/esp32s3/nibble_esp32/variant.h @@ -1,7 +1,7 @@ #define I2C_SDA 11 // I2C pins for this board #define I2C_SCL 10 -#define LED_PIN 1 // If defined we will blink this LED +#define LED_POWER 1 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/nugget_s3_lora/variant.h b/variants/esp32s3/nugget_s3_lora/variant.h index 8e6057d5b..633ed27f6 100644 --- a/variants/esp32s3/nugget_s3_lora/variant.h +++ b/variants/esp32s3/nugget_s3_lora/variant.h @@ -4,7 +4,7 @@ #define USE_SSD1306 #define DISPLAY_FLIP_SCREEN -#define LED_PIN 15 // If defined we will blink this LED +#define LED_POWER 15 // If defined we will blink this LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 3 // How many neopixels are connected @@ -12,7 +12,7 @@ #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use // Button A (44), B (43), R (12), U (13), L (11), D (18) -#define BUTTON_PIN 44 // If defined, this will be used for user button presses +#define BUTTON_PIN 43 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP #define USE_RF95 @@ -20,8 +20,19 @@ #define LORA_MISO 7 #define LORA_MOSI 8 #define LORA_CS 9 -#define LORA_DIO0 16 // a No connect on the SX1262 module +#define LORA_DIO0 16 #define LORA_RESET 4 #define LORA_DIO1 RADIOLIB_NC -#define LORA_DIO2 RADIOLIB_NC \ No newline at end of file +#define LORA_DIO2 RADIOLIB_NC + +// jk, its not really a trackball but we're gonna pretend! +#define HAS_TRACKBALL 1 +#define TB_UP 13 +#define TB_DOWN 18 +#define TB_LEFT 11 +#define TB_RIGHT 12 +#define TB_PRESS 44 // BUTTON_PIN +#define TB_DIRECTION FALLING + +#define ENABLE_AMBIENTLIGHTING diff --git a/variants/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini index bef7b19a0..6f218a126 100644 --- a/variants/esp32s3/picomputer-s3/platformio.ini +++ b/variants/esp32s3/picomputer-s3/platformio.ini @@ -6,6 +6,7 @@ custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Pi Computer S3 custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_mui = true extends = esp32s3_base board = bpi_picow_esp32_s3 @@ -24,7 +25,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 build_src_filter = ${esp32s3_base.build_src_filter} diff --git a/variants/esp32s3/picomputer-s3/variant.h b/variants/esp32s3/picomputer-s3/variant.h index 275da1b61..7b6218f87 100644 --- a/variants/esp32s3/picomputer-s3/variant.h +++ b/variants/esp32s3/picomputer-s3/variant.h @@ -52,8 +52,6 @@ // Picomputer gets a white on black display #define TFT_MESH_OVERRIDE COLOR565(255, 255, 255) -#define CANNED_MESSAGE_MODULE_ENABLE 1 - #define INPUTBROKER_MATRIX_TYPE 1 #define KEYS_COLS \ diff --git a/variants/esp32s3/rak3312/pins_arduino.h b/variants/esp32s3/rak3312/pins_arduino.h index dc5d30d61..600c619cf 100644 --- a/variants/esp32s3/rak3312/pins_arduino.h +++ b/variants/esp32s3/rak3312/pins_arduino.h @@ -24,9 +24,6 @@ static const uint8_t SCK = 13; #define SPI_MISO (10) #define SPI_CS (12) -// LEDs -#define LED_BUILTIN LED_GREEN - #ifdef _VARIANT_RAK3112_ /* * Serial interfaces diff --git a/variants/esp32s3/rak3312/platformio.ini b/variants/esp32s3/rak3312/platformio.ini index 113c2f527..87e5b63ff 100644 --- a/variants/esp32s3/rak3312/platformio.ini +++ b/variants/esp32s3/rak3312/platformio.ini @@ -9,6 +9,7 @@ custom_meshtastic_images = rak_3312.svg custom_meshtastic_tags = RAK custom_meshtastic_requires_dfu = false custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = false extends = esp32s3_base board = wiscore_rak3312 diff --git a/variants/esp32s3/rak3312/variant.h b/variants/esp32s3/rak3312/variant.h index 1f8eb9e39..ee0fff524 100644 --- a/variants/esp32s3/rak3312/variant.h +++ b/variants/esp32s3/rak3312/variant.h @@ -22,10 +22,9 @@ #define LED_BLUE 45 #define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE +#define LED_NOTIFICATION LED_BLUE -#define LED_CONN LED_BLUE -#define LED_PIN LED_GREEN +#define LED_POWER LED_GREEN #define ledOff(pin) pinMode(pin, INPUT) #define LED_STATE_ON 1 // State when LED is litted diff --git a/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h index 15a26e991..b51cd214e 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h +++ b/variants/esp32s3/rak_wismesh_tap_v2/pins_arduino.h @@ -22,7 +22,4 @@ static const uint8_t SCK = 13; #define SPI_MISO (10) #define SPI_CS (12) -// LEDs -#define LED_BUILTIN LED_GREEN - #endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini index e4ff9812c..7847410ae 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -1,6 +1,27 @@ ; rak_wismeshtap2 rak3112 +[ft5x06] +build_flags = + -D LGFX_TOUCH=FT5x06 + -D LGFX_TOUCH_I2C_FREQ=100000 + -D LGFX_TOUCH_I2C_PORT=0 + -D LGFX_TOUCH_I2C_ADDR=0x38 + -D LGFX_TOUCH_I2C_SDA=9 + -D LGFX_TOUCH_I2C_SCL=40 + -D LGFX_TOUCH_RST=-1 + -D LGFX_TOUCH_INT=39 + +[env:rak_wismesh_tap_v2] +custom_meshtastic_hw_model = 116 +custom_meshtastic_hw_model_slug = WISMESH_TAP_V2 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK WisMesh Tap V2 +custom_meshtastic_images = rak-wismesh-tap-v2.svg +custom_meshtastic_tags = RAK +custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_mui = true -[rak_wismeshtap_s3] extends = esp32s3_base board = wiscore_rak3312 board_check = true @@ -16,25 +37,14 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 - -[ft5x06] -extends = mesh_tab_base -build_flags = - -D LGFX_TOUCH=FT5x06 - -D LGFX_TOUCH_I2C_FREQ=100000 - -D LGFX_TOUCH_I2C_PORT=0 - -D LGFX_TOUCH_I2C_ADDR=0x38 - -D LGFX_TOUCH_I2C_SDA=9 - -D LGFX_TOUCH_I2C_SCL=40 - -D LGFX_TOUCH_RST=-1 - -D LGFX_TOUCH_INT=39 + lovyan03/LovyanGFX@1.2.19 [env:rak_wismesh_tap_v2-tft] -extends = rak_wismeshtap_s3 +extends = env:rak_wismesh_tap_v2 build_flags = - ${rak_wismeshtap_s3.build_flags} + ${env:rak_wismesh_tap_v2.build_flags} + -D MESHTASTIC_EXCLUDE_WEBSERVER=1 -D CONFIG_ARDUHAL_ESP_LOG -D CONFIG_ARDUHAL_LOG_COLORS=1 -D CONFIG_DISABLE_HAL_LOCKS=1 @@ -69,9 +79,9 @@ build_flags = -D VIEW_320x240 -D USE_PACKET_API ${ft5x06.build_flags} - -D LGFX_SCREEN_WIDTH=240 - -D LGFX_SCREEN_HEIGHT=320 - -D DISPLAY_SIZE=320x240 ; landscape mode + -D LGFX_SCREEN_WIDTH=240 ; native panel width (portrait) + -D LGFX_SCREEN_HEIGHT=320 ; native panel height (portrait) + -D DISPLAY_SIZE=320x240 ; UI runs in landscape mode -D LGFX_PANEL=ST7789 -D LGFX_ROTATION=1 -D LGFX_TOUCH_X_MIN=0 @@ -83,7 +93,7 @@ build_flags = -D MAP_FULL_REDRAW=1 lib_deps = - ${rak_wismeshtap_s3.lib_deps} + ${env:rak_wismesh_tap_v2.lib_deps} ${device-ui_base.lib_deps} diff --git a/variants/esp32s3/rak_wismesh_tap_v2/variant.h b/variants/esp32s3/rak_wismesh_tap_v2/variant.h index 2fc056557..90cb12053 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/variant.h +++ b/variants/esp32s3/rak_wismesh_tap_v2/variant.h @@ -30,10 +30,9 @@ #define LED_BLUE 45 #define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE +#define LED_NOTIFICATION LED_BLUE -#define LED_CONN LED_BLUE -#define LED_PIN LED_GREEN +#define LED_POWER LED_GREEN #define ledOff(pin) pinMode(pin, INPUT) #define LED_STATE_ON 1 // State when LED is litted @@ -47,10 +46,8 @@ #define SPI_MISO (10) #define SPI_CS (12) -#define HAS_BUTTON 1 #define BUTTON_PIN 0 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define USE_VIRTUAL_KEYBOARD 1 #define BATTERY_PIN 1 diff --git a/variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h b/variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h index 300f0e0f5..88c233491 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h +++ b/variants/esp32s3/seeed-sensecap-indicator/pins_arduino.h @@ -3,8 +3,6 @@ #include -// static const uint8_t LED_BUILTIN = -1; - // static const uint8_t TX = 43; // static const uint8_t RX = 44; diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini index 70a10e0d4..bb52f801b 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini +++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Seeed SenseCAP Indicator custom_meshtastic_images = seeed-sensecap-indicator.svg custom_meshtastic_tags = Seeed custom_meshtastic_partition_scheme = 8MB - = true +custom_meshtastic_has_mui = true extends = esp32s3_base platform_packages = @@ -37,8 +37,8 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} ; TODO switch back to official LovyanGFX https://github.com/mverch67/LovyanGFX/archive/4c76238c1344162a234ae917b27651af146d6fb2.zip - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/seeed_xiao_s3/variant.h b/variants/esp32s3/seeed_xiao_s3/variant.h index d8dcbc8d4..cbdbf8eb8 100644 --- a/variants/esp32s3/seeed_xiao_s3/variant.h +++ b/variants/esp32s3/seeed_xiao_s3/variant.h @@ -30,7 +30,7 @@ Expansion Board Infomation : https://www.seeedstudio.com/Seeeduino-XIAO-Expansio L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-Seeed-Studio-XIAO-p-5864.html */ -#define LED_PIN 48 +#define LED_POWER 48 #define LED_STATE_ON 1 // State when LED is lit #define BUTTON_PIN 21 // This is the Program Button @@ -50,7 +50,6 @@ L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-S #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 #define HAS_GPS 1 -#define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX PIN_GPS_TX #define PIN_SERIAL1_TX PIN_GPS_RX diff --git a/variants/esp32s3/station-g2/pins_arduino.h b/variants/esp32s3/station-g2/pins_arduino.h old mode 100755 new mode 100644 diff --git a/variants/esp32s3/station-g2/platformio.ini b/variants/esp32s3/station-g2/platformio.ini old mode 100755 new mode 100644 index 091b35f00..4efb21a00 --- a/variants/esp32s3/station-g2/platformio.ini +++ b/variants/esp32s3/station-g2/platformio.ini @@ -21,11 +21,11 @@ upload_protocol = esptool upload_speed = 921600 build_unflags = ${esp32s3_base.build_unflags} - -DARDUINO_USB_MODE=1 + -DARDUINO_USB_MODE=0 build_flags = ${esp32s3_base.build_flags} -D STATION_G2 -I variants/esp32s3/station-g2 -DBOARD_HAS_PSRAM -DSTATION_G2 - -DARDUINO_USB_MODE=0 + -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/station-g2/variant.h b/variants/esp32s3/station-g2/variant.h old mode 100755 new mode 100644 index 8f0b4b220..2d65a042c --- a/variants/esp32s3/station-g2/variant.h +++ b/variants/esp32s3/station-g2/variant.h @@ -40,6 +40,14 @@ Board Information: https://wiki.uniteng.com/en/meshtastic/station-g2 #define SX126X_MAX_POWER 19 #endif +// Enable Traffic Management Module for Station G2 +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 1 +#endif +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 +#endif + /* #define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO4_CHANNEL diff --git a/variants/esp32s3/t-beam-1w/platformio.ini b/variants/esp32s3/t-beam-1w/platformio.ini index 9abf895db..b14f2fe3c 100644 --- a/variants/esp32s3/t-beam-1w/platformio.ini +++ b/variants/esp32s3/t-beam-1w/platformio.ini @@ -8,6 +8,7 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Beam 1W custom_meshtastic_images = tbeam-1w.svg custom_meshtastic_tags = LilyGo +custom_meshtastic_has_mui = false extends = esp32s3_base board = t-beam-1w diff --git a/variants/esp32s3/t-beam-1w/variant.h b/variants/esp32s3/t-beam-1w/variant.h index dbe1620e2..52e99320e 100644 --- a/variants/esp32s3/t-beam-1w/variant.h +++ b/variants/esp32s3/t-beam-1w/variant.h @@ -67,7 +67,7 @@ #endif // LED -#define LED_PIN 18 +#define LED_POWER 18 #define LED_STATE_ON 1 // HIGH = ON // Battery ADC @@ -76,6 +76,8 @@ #define BATTERY_SENSE_SAMPLES 30 #define ADC_MULTIPLIER 2.9333 +#define OCV_ARRAY 7950, 7850, 7750, 7580, 7440, 7310, 7150, 7005, 6860, 6685, 6000 + // NTC temperature sensor #define NTC_PIN 14 diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index 5ba82d045..93ef8babf 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -9,6 +9,7 @@ custom_meshtastic_images = tdeck_pro.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = false extends = esp32s3_base board = t-deck-pro @@ -33,7 +34,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.8 # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index 456bc351a..8fa7e1740 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -44,7 +44,6 @@ // TCA8418 keyboard #define KB_BL_PIN 42 -#define CANNED_MESSAGE_MODULE_ENABLE 1 // microphone PCM5102A #define PCM5102A_SCK 47 diff --git a/variants/esp32s3/t-deck/pins_arduino.h b/variants/esp32s3/t-deck/pins_arduino.h index cb429d776..c358b988e 100644 --- a/variants/esp32s3/t-deck/pins_arduino.h +++ b/variants/esp32s3/t-deck/pins_arduino.h @@ -6,8 +6,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -// static const uint8_t LED_BUILTIN = -1; - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index 54ffe43fe..1b3599464 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -10,6 +10,7 @@ custom_meshtastic_images = t-deck.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = esp32s3_base board = t-deck @@ -28,9 +29,9 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + lovyan03/LovyanGFX@1.2.19 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/t-deck/variant.h b/variants/esp32s3/t-deck/variant.h index ab5b74870..5d885579a 100644 --- a/variants/esp32s3/t-deck/variant.h +++ b/variants/esp32s3/t-deck/variant.h @@ -61,7 +61,6 @@ #define KB_POWERON 10 // must be set to HIGH #define KB_SLAVE_ADDRESS TDECK_KB_ADDR // 0x55 #define KB_BL_PIN 46 // not used for now -#define CANNED_MESSAGE_MODULE_ENABLE 1 // trackball #define HAS_TRACKBALL 1 diff --git a/variants/esp32s3/t-eth-elite/variant.h b/variants/esp32s3/t-eth-elite/variant.h index b7ac05872..8f2748c36 100644 --- a/variants/esp32s3/t-eth-elite/variant.h +++ b/variants/esp32s3/t-eth-elite/variant.h @@ -12,7 +12,7 @@ #define HAS_SCREEN 1 // Allow for OLED Screens on I2C Header of shield -#define LED_PIN 38 // If defined we will blink this LED +#define LED_POWER 38 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses, #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/t-watch-s3/pins_arduino.h b/variants/esp32s3/t-watch-s3/pins_arduino.h index 35f0e933e..f4585ace8 100644 --- a/variants/esp32s3/t-watch-s3/pins_arduino.h +++ b/variants/esp32s3/t-watch-s3/pins_arduino.h @@ -3,8 +3,6 @@ #include -// static const uint8_t LED_BUILTIN = -1; - // static const uint8_t TX = 43; // static const uint8_t RX = 44; diff --git a/variants/esp32s3/t-watch-s3/platformio.ini b/variants/esp32s3/t-watch-s3/platformio.ini index 9c7a642b2..352396818 100644 --- a/variants/esp32s3/t-watch-s3/platformio.ini +++ b/variants/esp32s3/t-watch-s3/platformio.ini @@ -22,12 +22,12 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/t-watch-s3/variant.h b/variants/esp32s3/t-watch-s3/variant.h index 216dda589..df275c31d 100644 --- a/variants/esp32s3/t-watch-s3/variant.h +++ b/variants/esp32s3/t-watch-s3/variant.h @@ -45,7 +45,6 @@ // PCF8563 RTC Module #define PCF8563_RTC 0x51 -#define HAS_RTC 1 #define I2C_SDA 10 // For QMC6310 sensors and screens #define I2C_SCL 11 // For QMC6310 sensors and screens diff --git a/variants/esp32s3/t5s3_epaper/nicheGraphics.h b/variants/esp32s3/t5s3_epaper/nicheGraphics.h new file mode 100644 index 000000000..699a82de0 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/nicheGraphics.h @@ -0,0 +1,123 @@ +/* + +Most of the Meshtastic firmware uses preprocessor macros throughout the code to support different hardware variants. +NicheGraphics attempts a different approach: + +Per-device config takes place in this setupNicheGraphics() method +(And a small amount in platformio.ini) + +This file sets up InkHUD for Heltec VM-E290. +Different NicheGraphics UIs and different hardware variants will each have their own setup procedure. + +*/ + +#pragma once + +#include "configuration.h" +#include "mesh/MeshModule.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +// #include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/WindowManager.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" +#include "graphics/niche/Inputs/TwoButton.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + // E-Ink Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::DEPG0290BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(7, 1.5); + + // Prepare fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Init settings, and customize defaults + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + + // Setup backlight + // Note: AUX button behavior configured further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + // Optional arguments for defaults: + // - is activated? + // - is autoshown? + // - is foreground on a specific tile (index)? + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet); + // inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + + // Start running InkHUD + inkhud->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component + + // Setup the main user button (0) + buttons->setWiring(0, BUTTON_PIN); + buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); }); + buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); }); + + // Setup the aux button (1) + // Bonus feature of VME290 + buttons->setWiring(1, BUTTON_PIN_SECONDARY); + buttons->setHandlerShortPress(1, []() { InkHUD::InkHUD::getInstance()->nextTile(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/pins_arduino.h b/variants/esp32s3/t5s3_epaper/pins_arduino.h new file mode 100644 index 000000000..4978cff2a --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/pins_arduino.h @@ -0,0 +1,43 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +#if defined(T5_S3_EPAPER_PRO_V1) +// The default Wire will be mapped to RTC, Touch, BQ25896, and BQ27220 +static const uint8_t SDA = 6; +static const uint8_t SCL = 5; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 46; +static const uint8_t MOSI = 17; +static const uint8_t MISO = 8; +static const uint8_t SCK = 18; + +#define SPI_MOSI (17) +#define SPI_SCK (18) +#define SPI_MISO (8) +#define SPI_CS (16) + +#else // T5_S3_EPAPER_PRO_V2 +// The default Wire will be mapped to RTC, Touch, PCA9535, BQ25896, and BQ27220 +static const uint8_t SDA = 39; +static const uint8_t SCL = 40; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 46; +static const uint8_t MOSI = 13; +static const uint8_t MISO = 21; +static const uint8_t SCK = 14; + +#define SPI_MOSI (13) +#define SPI_SCK (14) +#define SPI_MISO (21) +#define SPI_CS (12) + +#endif + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini new file mode 100644 index 000000000..bad36706c --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -0,0 +1,61 @@ +[t5s3_epaper_base] +extends = esp32s3_base +board = t5-epaper-s3 +board_build.partition = default_16MB.csv +board_check = true +upload_protocol = esptool +build_flags = -fno-strict-aliasing + ${esp32_base.build_flags} + -I variants/esp32s3/t5s3_epaper + -D T5_S3_EPAPER_PRO + -D USE_EINK + -D USE_EINK_PARALLELDISPLAY + -D PRIVATE_HW + -D TOUCH_THRESHOLD_X=60 + -D TOUCH_THRESHOLD_Y=40 + -D TIME_LONG_PRESS=500 +; -D EINK_LIMIT_GHOSTING_PX=5000 + -D EPD_FULLSLOW_PERIOD=100 + -D FAST_EPD_PARTIAL_UPDATE_BUG ; use rect area update instead of partial + +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/t5s3_epaper> +lib_deps = + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 + https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip + https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip + +[env:t5s3_epaper_inkhud] +extends = t5s3_epaper_base, inkhud +board_level = extra # inkhud port is incomplete +board_check = false +build_flags = + ${t5s3_epaper_base.build_flags} + ${inkhud.build_flags} + -D SDCARD_USE_SPI1 + -D T5_S3_EPAPER_PRO_V2 +build_src_filter = + ${t5s3_epaper_base.build_src_filter} + ${inkhud.build_src_filter} +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${t5s3_epaper_base.lib_deps} + + +[env:t5s3-epaper-v1] ; H752 +extends = t5s3_epaper_base +build_flags = + ${t5s3_epaper_base.build_flags} + -D T5_S3_EPAPER_PRO_V1 + -D GPS_DEFAULT_NOT_PRESENT=1 + +[env:t5s3-epaper-v2] ; H752-01 +extends = t5s3_epaper_base +build_flags = + ${t5s3_epaper_base.build_flags} + -D T5_S3_EPAPER_PRO_V2 + -D SDCARD_USE_SPI1 + -D GPS_POWER_TOGGLE diff --git a/variants/esp32s3/t5s3_epaper/variant.cpp b/variants/esp32s3/t5s3_epaper/variant.cpp new file mode 100644 index 000000000..e10d7c347 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/variant.cpp @@ -0,0 +1,47 @@ +#include "configuration.h" + +#ifdef T5_S3_EPAPER_PRO + +#include "TouchDrvGT911.hpp" +#include "Wire.h" +#include "input/TouchScreenImpl1.h" + +TouchDrvGT911 touch; + +bool readTouch(int16_t *x, int16_t *y) +{ + if (!digitalRead(GT911_PIN_INT)) { + int16_t raw_x; + int16_t raw_y; + if (touch.getPoint(&raw_x, &raw_y)) { + // rotate 90° for landscape + *x = raw_y; + *y = EPD_WIDTH - 1 - raw_x; + LOG_DEBUG("touched(%d/%d)", *x, *y); + return true; + } + } + return false; +} + +void earlyInitVariant() +{ + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(BOARD_BL_EN, OUTPUT); +} + +// T5-S3-ePaper Pro specific (late-) init +void lateInitVariant(void) +{ + touch.setPins(GT911_PIN_RST, GT911_PIN_INT); + if (touch.begin(Wire, GT911_SLAVE_ADDRESS_L, GT911_PIN_SDA, GT911_PIN_SCL)) { + touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch); + touchScreenImpl1->init(); + } else { + LOG_ERROR("Failed to find touch controller!"); + } +} +#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/variant.h b/variants/esp32s3/t5s3_epaper/variant.h new file mode 100644 index 000000000..c2c001373 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/variant.h @@ -0,0 +1,92 @@ + +// Display (E-Ink) ED047TC1 - 8bit parallel +#define EPD_WIDTH 960 +#define EPD_HEIGHT 540 + +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define USE_VIRTUAL_KEYBOARD 1 + +#if defined(T5_S3_EPAPER_PRO_V1) +#define BOARD_BL_EN 40 +#else +#define BOARD_BL_EN 11 +#endif + +#define I2C_SDA SDA +#define I2C_SCL SCL + +#define HAS_TOUCHSCREEN 1 +#define GT911_PIN_SDA SDA +#define GT911_PIN_SCL SCL +#if defined(T5_S3_EPAPER_PRO_V1) +#define GT911_PIN_INT 15 +#define GT911_PIN_RST 41 +#else +#define GT911_PIN_INT 3 +#define GT911_PIN_RST 9 +#endif + +#define PCF85063_RTC 0x51 +#define HAS_RTC 1 +#define PCF85063_INT 2 + +#define USE_POWERSAVE +#define SLEEP_TIME 120 + +// GPS +#if !defined(T5_S3_EPAPER_PRO_V1) +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#endif + +#if defined(T5_S3_EPAPER_PRO_V1) +#define BUTTON_PIN 48 +#define PIN_BUTTON2 0 +#define ALT_BUTTON_PIN PIN_BUTTON2 +#else +#define BUTTON_PIN 0 +#endif + +// SD card +#define HAS_SDCARD +#define SDCARD_CS SPI_CS +#define SD_SPI_FREQUENCY 75000000U + +// battery charger BQ25896 +#define HAS_PPM 1 +#define XPOWERS_CHIP_BQ25896 + +// battery quality management BQ27220 +#define HAS_BQ27220 1 +#define BQ27220_I2C_SDA SDA +#define BQ27220_I2C_SCL SCL +#define BQ27220_DESIGN_CAPACITY 1500 + +// LoRa +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK SCK +#define LORA_MISO MISO +#define LORA_MOSI MOSI +#define LORA_CS 46 + +#define LORA_DIO0 -1 +#if defined(T5_S3_EPAPER_PRO_V1) +#define LORA_RESET 43 +#define LORA_DIO1 3 // SX1262 IRQ +#define LORA_DIO2 44 // SX1262 BUSY +#define LORA_DIO3 +#else +#define LORA_RESET 1 +#define LORA_DIO1 10 // SX1262 IRQ +#define LORA_DIO2 47 // SX1262 BUSY +#define LORA_DIO3 +#endif + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 2.4 diff --git a/variants/esp32s3/tbeam-s3-core/variant.h b/variants/esp32s3/tbeam-s3-core/variant.h index 1f900fcae..9ce4aade9 100644 --- a/variants/esp32s3/tbeam-s3-core/variant.h +++ b/variants/esp32s3/tbeam-s3-core/variant.h @@ -55,7 +55,6 @@ // PCF8563 RTC Module #define PCF8563_RTC 0x51 -#define HAS_RTC 1 // Specify the PMU as Wire1. In the t-beam-s3 core, PCF8563 and PMU share the bus #define PMU_USE_WIRE1 diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 7b4fc5312..832f9d7d7 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -10,6 +10,7 @@ custom_meshtastic_images = lilygo-tlora-pager.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = esp32s3_base board = t-deck-pro ; same as T-Deck Pro @@ -26,16 +27,15 @@ build_flags = ${esp32s3_base.build_flags} -D T_LORA_PAGER -D BOARD_HAS_PSRAM -D HAS_SDCARD - -D SDCARD_USE_SPI1 -D ENABLE_ROTARY_PULLUP -D ENABLE_BUTTON_PULLUP -D ROTARY_BUXTRONICS lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + lovyan03/LovyanGFX@1.2.19 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library @@ -45,7 +45,7 @@ lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver - https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.0.zip + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip # TODO renovate https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip # TODO renovate diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index 8020f7198..28359c786 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -40,7 +40,6 @@ // PCF85063 RTC Module #define PCF85063_RTC 0x51 -#define HAS_RTC 1 // Rotary #define ROTARY_A (40) @@ -61,7 +60,6 @@ #define I2C_NO_RESCAN #define KB_BL_PIN 46 #define KB_INT 6 -#define CANNED_MESSAGE_MODULE_ENABLE 1 // audio codec ES8311 #define HAS_I2S diff --git a/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h b/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h index 8f5e63653..73cc2e235 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h +++ b/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -67,6 +68,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 @@ -86,4 +88,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini index 256cdc0d0..8f764bbe6 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini @@ -8,6 +8,7 @@ custom_meshtastic_display_name = LILYGO T-LoRa T3-S3 E-Ink custom_meshtastic_images = tlora-t3s3-epaper.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true +custom_meshtastic_has_ink_hud = true extends = esp32s3_base board = tlora-t3s3-v1 @@ -31,7 +32,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:tlora-t3s3-epaper-inkhud] extends = esp32s3_base, inkhud diff --git a/variants/esp32s3/tlora_t3s3_epaper/variant.h b/variants/esp32s3/tlora_t3s3_epaper/variant.h index 1ed505420..0f4875fc4 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/variant.h +++ b/variants/esp32s3/tlora_t3s3_epaper/variant.h @@ -22,7 +22,7 @@ #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 -#define LED_PIN 37 +#define LED_POWER 37 #define BUTTON_PIN 0 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/tlora_t3s3_v1/variant.h b/variants/esp32s3/tlora_t3s3_v1/variant.h index babe44a58..02e2a0e42 100644 --- a/variants/esp32s3/tlora_t3s3_v1/variant.h +++ b/variants/esp32s3/tlora_t3s3_v1/variant.h @@ -14,7 +14,7 @@ #define I2C_SDA1 43 #define I2C_SCL1 44 -#define LED_PIN 37 // If defined we will blink this LED +#define LED_POWER 37 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses, #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/tracksenger/internal/pins_arduino.h b/variants/esp32s3/tracksenger/internal/pins_arduino.h index 1052af961..93fd5d9c2 100644 --- a/variants/esp32s3/tracksenger/internal/pins_arduino.h +++ b/variants/esp32s3/tracksenger/internal/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/tracksenger/internal/variant.h b/variants/esp32s3/tracksenger/internal/variant.h index 2287dfe0b..f9a20c901 100644 --- a/variants/esp32s3/tracksenger/internal/variant.h +++ b/variants/esp32s3/tracksenger/internal/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define HELTEC_TRACKER_V1_X @@ -78,7 +78,6 @@ // keyboard changes #define PIN_BUZZER 43 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define INPUTBROKER_MATRIX_TYPE 1 diff --git a/variants/esp32s3/tracksenger/lcd/pins_arduino.h b/variants/esp32s3/tracksenger/lcd/pins_arduino.h index 1052af961..93fd5d9c2 100644 --- a/variants/esp32s3/tracksenger/lcd/pins_arduino.h +++ b/variants/esp32s3/tracksenger/lcd/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/tracksenger/lcd/variant.h b/variants/esp32s3/tracksenger/lcd/variant.h index f42a5b19f..029f7753b 100644 --- a/variants/esp32s3/tracksenger/lcd/variant.h +++ b/variants/esp32s3/tracksenger/lcd/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define HELTEC_TRACKER_V1_X @@ -102,7 +102,6 @@ // keyboard changes #define PIN_BUZZER 43 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define INPUTBROKER_MATRIX_TYPE 1 diff --git a/variants/esp32s3/tracksenger/oled/pins_arduino.h b/variants/esp32s3/tracksenger/oled/pins_arduino.h index 1052af961..93fd5d9c2 100644 --- a/variants/esp32s3/tracksenger/oled/pins_arduino.h +++ b/variants/esp32s3/tracksenger/oled/pins_arduino.h @@ -11,10 +11,6 @@ #define USB_VID 0x303a #define USB_PID 0x1001 -static const uint8_t LED_BUILTIN = 18; -#define BUILTIN_LED LED_BUILTIN // backward compatibility -#define LED_BUILTIN LED_BUILTIN - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/tracksenger/oled/variant.h b/variants/esp32s3/tracksenger/oled/variant.h index 85cc019c4..1f1fbbaa1 100644 --- a/variants/esp32s3/tracksenger/oled/variant.h +++ b/variants/esp32s3/tracksenger/oled/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define HELTEC_TRACKER_V1_X @@ -79,7 +79,6 @@ // keyboard changes #define PIN_BUZZER 43 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define INPUTBROKER_MATRIX_TYPE 1 diff --git a/variants/esp32s3/tracksenger/platformio.ini b/variants/esp32s3/tracksenger/platformio.ini index 419a3539b..c006cf835 100644 --- a/variants/esp32s3/tracksenger/platformio.ini +++ b/variants/esp32s3/tracksenger/platformio.ini @@ -22,7 +22,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 [env:tracksenger-lcd] custom_meshtastic_hw_model = 48 @@ -48,7 +48,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 + lovyan03/LovyanGFX@1.2.19 [env:tracksenger-oled] custom_meshtastic_hw_model = 48 diff --git a/variants/esp32s3/unphone/pins_arduino.h b/variants/esp32s3/unphone/pins_arduino.h index 74067359f..8a62e3d42 100644 --- a/variants/esp32s3/unphone/pins_arduino.h +++ b/variants/esp32s3/unphone/pins_arduino.h @@ -6,9 +6,6 @@ #define USB_VID 0x16D0 #define USB_PID 0x1178 -#define LED_BUILTIN 13 -#define BUILTIN_LED LED_BUILTIN // backward compatibility - static const uint8_t TX = 43; static const uint8_t RX = 44; diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index 28be1f3e1..3c342e2ac 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -37,11 +37,9 @@ build_src_filter = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.0 - # TODO renovate - https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + lovyan03/LovyanGFX@1.2.19 + # TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 + https://gitlab.com/hamishcunningham/unphonelibrary/-/archive/meshtastic/unphonelibrary-meshtastic.zip [env:unphone-tft] board_level = extra diff --git a/variants/esp32s3/unphone/variant.h b/variants/esp32s3/unphone/variant.h index 6f0710d62..268eedea5 100644 --- a/variants/esp32s3/unphone/variant.h +++ b/variants/esp32s3/unphone/variant.h @@ -54,7 +54,7 @@ #define SD_SPI_FREQUENCY 25000000 -#define LED_PIN 13 // the red part of the RGB LED +#define LED_POWER 13 // the red part of the RGB LED #define LED_STATE_ON 0 // State when LED is lit #define ALT_BUTTON_PIN 21 // Button 3 - square - top button in landscape mode diff --git a/variants/native/portduino-buildroot/variant.h b/variants/native/portduino-buildroot/variant.h index affd83051..ac6421b14 100644 --- a/variants/native/portduino-buildroot/variant.h +++ b/variants/native/portduino-buildroot/variant.h @@ -1,6 +1,5 @@ #define HAS_SCREEN 1 #define USE_TFTDISPLAY 1 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE portduino_config.maxtophone #define MAX_NUM_NODES portduino_config.MaxNodes diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 55905481a..87d8431a3 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/f566d364204416cdbf298e349213f7d551f793d9.zip + https://github.com/meshtastic/platform-native/archive/71ed55bb95feb3c43ebde1ec1e2e17643a424c04.zip framework = arduino build_src_filter = @@ -27,20 +27,22 @@ lib_deps = # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@1.2.7 - # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main + lovyan03/LovyanGFX@1.2.19 + ; # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main https://github.com/pine64/libch341-spi-userspace/archive/23c42319a69cffcb65868e3c72e6bed83974a393.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip - # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 - adafruit/Adafruit BME680 Library@^2.0.5 + # renovate: datasource=github-tags depName=Adafruit_BME680 packageName=adafruit/Adafruit_BME680 + https://github.com/adafruit/Adafruit_BME680/archive/refs/tags/2.0.6.zip build_flags = ${arduino_base.build_flags} -D ARCH_PORTDUINO -fPIC + -D_FORTIFY_SOURCE=2 + -fstack-protector-all -Wstack-protector --param ssp-buffer-size=4 -Isrc/platform/portduino -DRADIOLIB_EEPROM_UNSUPPORTED -DPORTDUINO_LINUX_HARDWARE @@ -53,9 +55,9 @@ build_flags = -li2c -luv -std=gnu17 - -std=c++17 - + -std=gnu++17 lib_ignore = Adafruit NeoPixel Adafruit ST7735 and ST7789 Library + Adafruit SSD1306 SD diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h index 972443450..c23d17b8d 100644 --- a/variants/native/portduino/variant.h +++ b/variants/native/portduino/variant.h @@ -2,10 +2,17 @@ #define HAS_SCREEN 1 #endif #define USE_TFTDISPLAY 1 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE portduino_config.maxtophone #define MAX_NUM_NODES portduino_config.MaxNodes // RAK12002 RTC Module #define RV3028_RTC (uint8_t)0b1010010 + +// Enable Traffic Management Module for native/portduino +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 1 +#endif +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 +#endif diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini index 093c3732d..fd159a6d2 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -12,5 +12,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.8 debug_tool = jlink diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h index 2318450eb..8baf25e87 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/variant.h @@ -50,9 +50,6 @@ extern "C" { #define RGBLED_BLUE (0 + 12) // Blue of RGB P0.12 #define RGBLED_CA // comment out this line if you have a common cathode type, as defined use common anode logic -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h b/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h index f64de9d07..242e5ae49 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -71,13 +72,14 @@ void setupNicheGraphics() // Pick applets // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); @@ -115,4 +117,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini index a4687669b..c169de8f7 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini @@ -8,6 +8,7 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = ThinkNode M1 custom_meshtastic_images = thinknode_m1.svg custom_meshtastic_tags = Elecrow +custom_meshtastic_has_ink_hud = true extends = nrf52840_base board = ThinkNode-M1 @@ -33,7 +34,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM khoih-prog/nRF52_PWM@1.0.1 ;upload_protocol = fs diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp index 1560cde73..216b62dcb 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp @@ -32,15 +32,8 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - - pinMode(PIN_LED3, OUTPUT); - ledOff(PIN_LED3); + // pinMode(PIN_LED1, OUTPUT); + // ledOff(PIN_LED1); } void variant_shutdown() diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h index cde0f49c1..4ae462758 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h @@ -41,23 +41,18 @@ extern "C" { #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) -#define PIN_LED2 -1 -#define PIN_LED3 -1 - // LED -#define PIN_LED1 (32 + 6) // red -#define LED_POWER (32 + 4) -#define USER_LED (0 + 13) // green +// #define PIN_LED1 (32 + 6) +#define LED_POWER (32 + 4) // red +#define LED_NOTIFICATION (0 + 13) // green +#define POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING + // USB_CHECK #define EXT_PWR_DETECT (32 + 3) #define ADC_V (0 + 8) -#define LED_RED PIN_LED3 -#define LED_BLUE PIN_LED1 -#define LED_GREEN PIN_LED2 -#define LED_BUILTIN LED_BLUE -#define LED_CONN PIN_GREEN -#define LED_STATE_ON 0 // State when LED is lit // LED灯亮时的状态 +// #define LED_BLUE PIN_LED1 +#define LED_STATE_ON 1 // State when LED is lit // LED灯亮时的状态 #define PIN_BUZZER (0 + 6) /* * Buttons @@ -159,6 +154,8 @@ External serial flash WP25R1635FZUIL0 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN +#define SERIAL_PRINT_PORT 0 + /* * SPI Interfaces */ @@ -169,8 +166,6 @@ External serial flash WP25R1635FZUIL0 #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) -#define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini index 6751dd4ef..b6956c0f1 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini @@ -24,5 +24,5 @@ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM khoih-prog/nRF52_PWM@1.0.1 - ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - ; lewisxhe/SensorLib@0.3.4 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp index 9769e3edd..45a64ad3b 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp @@ -37,8 +37,8 @@ void initVariant() digitalWrite(KEY_POWER, HIGH); pinMode(RGB_POWER, OUTPUT); digitalWrite(RGB_POWER, HIGH); - pinMode(green_LED_PIN, OUTPUT); - digitalWrite(green_LED_PIN, LED_STATE_OFF); + pinMode(LED_GREEN, OUTPUT); + digitalWrite(LED_GREEN, LED_STATE_OFF); pinMode(LED_BLUE, OUTPUT); pinMode(PIN_POWER_USB, INPUT); pinMode(PIN_POWER_DONE, INPUT); @@ -63,8 +63,8 @@ void initVariant() // called from main-nrf52.cpp during the cpuDeepSleep() function void variant_shutdown() { - digitalWrite(red_LED_PIN, HIGH); - digitalWrite(green_LED_PIN, HIGH); + digitalWrite(LED_RED, HIGH); + digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_BLUE, HIGH); digitalWrite(PIN_EN1, LOW); @@ -81,8 +81,8 @@ void variant_shutdown() if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || pin == LR1110_SPI_MOSI_PIN || pin == LR1110_SPI_SCK_PIN || pin == LR1110_SPI_NSS_PIN || pin == LR1110_BUSY_PIN || - pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == green_LED_PIN || - pin == red_LED_PIN || pin == LED_BLUE) { + pin == LR1110_NRESET_PIN || pin == LR1110_IRQ_PIN || pin == GPS_TX_PIN || pin == GPS_RX_PIN || pin == LED_GREEN || + pin == LED_RED || pin == LED_BLUE) { continue; } pinMode(pin, OUTPUT); diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h index 29a6c85fd..fa127ae3e 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h @@ -50,15 +50,13 @@ extern "C" { #define EEPROM_POWER 7 // LED -#define red_LED_PIN 33 -#define LED_POWER red_LED_PIN -#define LED_CHARGE LED_POWER // Signals the Status LED Module to handle this LED -#define green_LED_PIN 35 -#define PIN_LED2 green_LED_PIN +#define LED_RED 33 +#define LED_POWER LED_RED +#define LED_GREEN 35 +#define LED_NOTIFICATION LED_GREEN #define LED_BLUE 37 #define LED_PAIRING LED_BLUE // Signals the Status LED Module to handle this LED -#define LED_BUILTIN -1 #define LED_STATE_ON LOW #define LED_STATE_OFF HIGH @@ -113,9 +111,10 @@ extern "C" { #define LR11X0_DIO3_TCXO_VOLTAGE 3.3 #define LR11X0_DIO_AS_RF_SWITCH +#define SERIAL_PRINT_PORT 0 + // PCF8563 RTC Module -// REVISIT https://github.com/meshtastic/firmware/pull/9084 -// #define PCF8563_RTC 0x51 +#define PCF8563_RTC 0x51 #ifdef __cplusplus } diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini index 9a2b3a467..f96e6038c 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini @@ -12,4 +12,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M4> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library + lewisxhe/PCF8563_Library@1.0.1 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp index 5c4b6215b..999f326db 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.cpp @@ -32,9 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - pinMode(LED_PAIRING, OUTPUT); ledOff(LED_PAIRING); diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h index faca5b075..2cfe948e3 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/variant.h @@ -40,9 +40,8 @@ extern "C" { #define NUM_ANALOG_OUTPUTS (0) // LEDs -#define LED_BUILTIN -1 #define LED_BLUE -1 -#define PIN_LED2 (32 + 9) +#define LED_NOTIFICATION (32 + 9) #define LED_PAIRING (13) #define Battery_LED_1 (15) @@ -135,6 +134,8 @@ static const uint8_t A0 = PIN_A0; #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini index a31615545..fa09a4463 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini @@ -21,5 +21,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6> lib_deps = ${nrf52840_base.lib_deps} - ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib - ; lewisxhe/SensorLib@0.3.4 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp index 4ce8ecdf0..a43755c06 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp @@ -32,9 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - pinMode(LED_CHARGE, OUTPUT); - ledOff(LED_CHARGE); - pinMode(LED_PAIRING, OUTPUT); ledOff(LED_PAIRING); @@ -48,7 +45,7 @@ void variant_shutdown() // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. for (int pin = 0; pin < 48; pin++) { if (pin == PIN_GPS_EN || pin == ADC_CTRL || pin == PIN_BUTTON1 || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI || - pin == PIN_SPI_SCK) { + pin == PIN_SPI_SCK || pin == SX126X_CS || pin == SX126X_RESET || pin == SX126X_BUSY || pin == SX126X_DIO1) { continue; } pinMode(pin, OUTPUT); diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h index e46391207..2ebb79031 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h @@ -40,11 +40,10 @@ extern "C" { #define NUM_ANALOG_OUTPUTS (0) // LEDs -#define LED_BUILTIN -1 #define LED_BLUE -1 -#define LED_CHARGE (12) +#define LED_POWER (12) #define LED_PAIRING (7) -#define PIN_LED2 LED_PAIRING +#define LED_NOTIFICATION LED_PAIRING #define LED_STATE_ON HIGH #define LED_STATE_OFF LOW @@ -121,8 +120,7 @@ static const uint8_t A0 = PIN_A0; #define PIN_SERIAL2_TX (24) // PCF8563 RTC Module -// REVISIT https://github.com/meshtastic/firmware/pull/9084 -// #define PCF8563_RTC 0x51 +#define PCF8563_RTC 0x51 // SPI #define SPI_INTERFACES_COUNT 1 diff --git a/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp b/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp index 35dc1d39b..5972861d6 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp +++ b/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp @@ -32,9 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/ME25LS01-4Y10TD/variant.h b/variants/nrf52840/ME25LS01-4Y10TD/variant.h index e772069da..1d1af37ad 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/variant.h +++ b/variants/nrf52840/ME25LS01-4Y10TD/variant.h @@ -50,8 +50,7 @@ extern "C" { #define PIN_LED1 (32 + 7) // P1.07 Blue D2 -#define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini index 5951a37a3..39b5dfbd4 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS0 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.8 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = nrfutil ;upload_port = /dev/ttyACM1 diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp index 35dc1d39b..5972861d6 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp @@ -32,9 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h index 797394ce6..a5bb53a33 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h @@ -50,8 +50,7 @@ extern "C" { #define PIN_LED1 (32 + 7) // P1.07 Blue D2 -#define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/MS24SF1/variant.h b/variants/nrf52840/MS24SF1/variant.h index d26dcebc2..a41b3a350 100644 --- a/variants/nrf52840/MS24SF1/variant.h +++ b/variants/nrf52840/MS24SF1/variant.h @@ -50,8 +50,7 @@ extern "C" { #define PIN_LED1 (-1) -#define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini index ecb630bb7..ebea1ce97 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini @@ -15,6 +15,6 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.8 debug_tool = jlink ;upload_port = /dev/ttyACM4 \ No newline at end of file diff --git a/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp b/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp index 8c6bf039c..04cda84ac 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp +++ b/variants/nrf52840/MakePython_nRF52840_eink/variant.cpp @@ -32,7 +32,4 @@ void initVariant() // LED1 & LED2 pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); } diff --git a/variants/nrf52840/MakePython_nRF52840_eink/variant.h b/variants/nrf52840/MakePython_nRF52840_eink/variant.h index 00c8dc199..64cca4916 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/variant.h +++ b/variants/nrf52840/MakePython_nRF52840_eink/variant.h @@ -27,15 +27,10 @@ extern "C" { // LEDs #define PIN_LED1 (32 + 10) // LED P1.15 -#define PIN_LED2 (-1) // - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 -#define LED_STATE_ON 0 // State when LED is litted +#define LED_STATE_ON 0 // State when LED is lit /* * Buttons diff --git a/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp b/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp index 8c6bf039c..04cda84ac 100644 --- a/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp +++ b/variants/nrf52840/MakePython_nRF52840_oled/variant.cpp @@ -32,7 +32,4 @@ void initVariant() // LED1 & LED2 pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); } diff --git a/variants/nrf52840/MakePython_nRF52840_oled/variant.h b/variants/nrf52840/MakePython_nRF52840_oled/variant.h index 28d941171..2f035e7da 100644 --- a/variants/nrf52840/MakePython_nRF52840_oled/variant.h +++ b/variants/nrf52840/MakePython_nRF52840_oled/variant.h @@ -27,15 +27,10 @@ extern "C" { // LEDs #define PIN_LED1 (32 + 10) // LED P1.15 -#define PIN_LED2 (-1) // - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 -#define LED_STATE_ON 0 // State when LED is litted +#define LED_STATE_ON 0 // State when LED is lit /* * Buttons diff --git a/variants/nrf52840/TWC_mesh_v4/platformio.ini b/variants/nrf52840/TWC_mesh_v4/platformio.ini index d93f179c2..c529caa0b 100644 --- a/variants/nrf52840/TWC_mesh_v4/platformio.ini +++ b/variants/nrf52840/TWC_mesh_v4/platformio.ini @@ -9,5 +9,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mes lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.8 debug_tool = jlink diff --git a/variants/nrf52840/TWC_mesh_v4/variant.h b/variants/nrf52840/TWC_mesh_v4/variant.h index 6a6f541e6..875040b5f 100644 --- a/variants/nrf52840/TWC_mesh_v4/variant.h +++ b/variants/nrf52840/TWC_mesh_v4/variant.h @@ -34,9 +34,6 @@ extern "C" { // #define PIN_LED1 (32 + 9) Green // #define PIN_LED1 (0 + 12) Blue -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 diff --git a/variants/nrf52840/canaryone/variant.h b/variants/nrf52840/canaryone/variant.h index 61d1e8df9..3f45b618a 100644 --- a/variants/nrf52840/canaryone/variant.h +++ b/variants/nrf52840/canaryone/variant.h @@ -52,9 +52,6 @@ extern "C" { #define LED_BLUE PIN_LED1 -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED3 - #define LED_STATE_ON 0 // State when LED is lit /* @@ -170,6 +167,8 @@ static const uint8_t A0 = PIN_A0; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h index 8f30a244f..0a01b613e 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -72,7 +73,8 @@ void setupNicheGraphics() inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background @@ -92,4 +94,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md index 4ffe625cc..5d3d90c72 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md @@ -53,7 +53,7 @@ Making your own node based on this design is straightforward. There are various The E80 from CDEbyte is the most obtainable module at present, and has been selected as the default option. -Naturally, CDEbyte have chosen to ignore the generic Semtech impelementation of the RF switching logic and have supplied confusing and contradictory documentation, which is explained below. +Naturally, CDEbyte have chosen to ignore the generic Semtech implementation of the RF switching logic and have supplied confusing and contradictory documentation, which is explained below. tl;dr: The E80 is chosen as the default. **If you wish to use another module, the table in `rfswitch.h` must be adjusted accordingly.** diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h index 71508c037..ac7ef57c4 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h @@ -12,9 +12,15 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { - // mode DIO5 DIO6 DIO7 - {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, - {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, + // clang-format off + // mode DIO5 DIO6 DIO7 + {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, + {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, + {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, + {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, + END_OF_MODE_TABLE, + // clang-format on }; diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h index 63af1fe79..8e10141f5 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h @@ -81,7 +81,6 @@ NRF52 PRO MICRO PIN ASSIGNMENT // LED #define PIN_LED1 (0 + 15) // P0.15 -#define LED_BUILTIN PIN_LED1 // Actually red #define LED_BLUE PIN_LED1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md index 194c53434..7fbf83d7c 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md @@ -1,43 +1,21 @@ # XIAO nRF52840 + XIAO Wio SX1262 -For a mere doubling in price you too can swap out the XIAO ESP32C3 for a XIAO nRF52840, stack the Wio SX1262 radio board either above or underneath the nRF52840, solder the pins, and achieve a massive improvement in battery life! +For a mere doubling in price you too can swap out the XIAO ESP32S3 for a XIAO nRF52840, stack the Wio SX1262 radio board either above or underneath the nRF52840, solder the pins, and achieve a massive improvement in battery life! -I'm not really sure why else you would want to as the ESP32C3 is perfectly cromulent, easily connects to the Wio SX1262 via the B2B connector and has an onboard IPEX connector for the included Bluetooth antenna. So you'll also lose BT range, but you will also have working ADC for the battery in Meshtastic and also have an ESP32C3 to use for something else! +I'm not really sure why else you would want to as the ESP32S3 is perfectly cromulent, easily connects to the Wio SX1262 via the B2B connector and has an onboard IPEX connector for the included Bluetooth antenna. So you'll also lose BT range, but you will also have working ADC for the battery in Meshtastic and also have an ESP32S3 to use for something else! If you're still reading you are clearly gonna do it anyway, so...mount the Wio SX1262 either on top or underneath depending on your preference. The `variant.h` will work with either configuration though it does map the Wio SX1262's button to nRF52840 Pin `D5` as it can still be used as a user button and it's nice to be able to gracefully shutdown a node by holding it down for 5 seconds. If you do decide to wire up the button, orient it so looking straight-down at the Wio SX1262 the radio chip is at the bottom, button in the middle and the hole is at the top - the **left** side of the button should be soldered to `GND` (e.g. the 2nd pin down the top on the **right** row of pins) and the **right** side of the button should be soldered to `D5` (e.g. the 2nd pin up from the button on the **left** row of pins.). This mirrors the original wiring and wiring it in reverse could end up connecting GND to voltage and that's no beuno. -Serial Pins remain available on `D6` (TX) and `D7` (RX) should you want to use them, The same pins could be repurposed for `i2c` if you would like to have that instead of serial, in `variant.h` you would just need to change: +Serial Pins remain available on `D6` (TX) and `D7` (RX) should you want to use them, and I2C has been mapped to NFC1 (SDA, D30) and NFC2 (SCL, D31) -```c++ -// RX and TX pins -#define PIN_SERIAL1_RX (6) -#define PIN_SERIAL1_TX (7) +The same pins could be reordered if you would like to have a different arrangement, in `variant.h` you would just need to change the relevant lines: + +```cpp +#define GPS_TX_PIN D6 // This is data from the MCU +#define GPS_RX_PIN D7 // This is data from the GNSS module + +#define PIN_WIRE_SDA D6 +#define PIN_WIRE_SCL D7 ``` - -to - -```c++ -// RX and TX pins -#define PIN_SERIAL1_RX (-1) -#define PIN_SERIAL1_TX (-1) -``` - -and - -```c++ -#define PIN_WIRE_SDA (-1) -#define PIN_WIRE_SCL (-1) -// #define PIN_WIRE_SDA (6) -// #define PIN_WIRE_SCL (7) -``` - -to - -```c++ -#define PIN_WIRE_SDA (6) -#define PIN_WIRE_SCL (7) -``` - -If you wanted both serial and i2c you could even go so far as to use the pads for the PDM mic which is missing on the non-sense board (`P1.00` / `P0.16`)... or move up to the nRF52840 Plus which has even more pins available but hasn't been checked/confirmed if it follows the same pin mapping as the non-plus. diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini index 10eab2aa4..81076bd55 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini @@ -1,13 +1,17 @@ -; Seeed XIAO nRF52840 + XIAO Wio SX1262 DIY -[env:seeed-xiao-nrf52840-wio-sx1262] -board = xiao_ble_sense -extends = nrf52840_base +; Seeed Xiao BLE but using the B2B from ESP32S3 variant +[env:seeed_xiao_nrf52840_btb] +extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${nrf52840_base.build_flags} - -Ivariants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262 - -D PRIVATE_HW + -Ivariants/nrf52840/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 + -DPRIVATE_HW ; Define private hardware + -DSEEED_XIAO_NRF_WIO_BTB ; Define Seeed XIAO nRF Wio B2B + -USEEED_XIAO_NRF52840_KIT ; Remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld -build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262> +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp deleted file mode 100644 index 300f69d0b..000000000 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "variant.h" -#include "nrf.h" -#include "wiring_constants.h" -#include "wiring_digital.h" - -const uint32_t g_ADigitalPinMap[] = { - // D0 .. D13 - 2, // D0 is P0.02 (A0) - 3, // D1 is P0.03 (A1) - 28, // D2 is P0.28 (A2) - 29, // D3 is P0.29 (A3) - 4, // D4 is P0.04 (A4,SDA) - 5, // D5 is P0.05 (A5,SCL) - 43, // D6 is P1.11 (TX) - 44, // D7 is P1.12 (RX) - 45, // D8 is P1.13 (SCK) - 46, // D9 is P1.14 (MISO) - 47, // D10 is P1.15 (MOSI) - - // LEDs - 26, // D11 is P0.26 (LED RED) - 6, // D12 is P0.06 (LED BLUE) - 30, // D13 is P0.30 (LED GREEN) - 14, // D14 is P0.14 (READ_BAT) - - // LSM6DS3TR - 40, // D15 is P1.08 (6D_PWR) - 27, // D16 is P0.27 (6D_I2C_SCL) - 7, // D17 is P0.07 (6D_I2C_SDA) - 11, // D18 is P0.11 (6D_INT1) - - // MIC - 42, // 17,//42, // D19 is P1.10 (MIC_PWR) - 32, // 26,//32, // D20 is P1.00 (PDM_CLK) - 16, // 25,//16, // D21 is P0.16 (PDM_DATA) - - // BQ25100 - 13, // D22 is P0.13 (HICHG) - 17, // D23 is P0.17 (~CHG) - - // - 21, // D24 is P0.21 (QSPI_SCK) - 25, // D25 is P0.25 (QSPI_CSN) - 20, // D26 is P0.20 (QSPI_SIO_0 DI) - 24, // D27 is P0.24 (QSPI_SIO_1 DO) - 22, // D28 is P0.22 (QSPI_SIO_2 WP) - 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) - - // NFC - 9, // D30 is P0.09 (NFC1) - 10, // D31 is P0.10 (NFC2) - - // VBAT - 31, // D32 is P0.10 (VBAT) -}; - -void initVariant() -{ - // Set BQ25101 ISET to 100mA instead of 50mA - pinMode(HICHG, OUTPUT); - digitalWrite(HICHG, LOW); -} diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h deleted file mode 100644 index 277377d71..000000000 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h +++ /dev/null @@ -1,187 +0,0 @@ -// basically xiao_ble with pins remapped for: -// Seeed XIAO nRF52840 : https://www.seeedstudio.com/Seeed-XIAO-BLE-nRF52840-p-5201.html -// Seeed Wio SX1626 : https://www.seeedstudio.com/Wio-SX1262-with-XIAO-ESP32S3-p-5982.html - -#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ -#define _SEEED_XIAO_NRF52840_SENSE_H_ - -/** Master clock frequency */ -#define VARIANT_MCK (64000000ul) - -#define USE_LFXO // Board uses 32khz crystal for LF - -/*---------------------------------------------------------------------------- - * Headers - *----------------------------------------------------------------------------*/ - -#include "WVariant.h" - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -#define PINS_COUNT (33) -#define NUM_DIGITAL_PINS (33) -#define NUM_ANALOG_INPUTS (8) // A6 is used for battery, A7 is analog reference -#define NUM_ANALOG_OUTPUTS (0) - -// LEDs -// ---- -#define LED_RED 11 -#define LED_BLUE 12 -#define LED_GREEN 13 - -#define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE -#define PIN_LED3 LED_RED - -#define PIN_LED PIN_LED1 -#define LED_PWR (PINS_COUNT) - -#define LED_BUILTIN PIN_LED -#define LED_STATE_ON 1 // State when LED is lit - -// XIAO Wio-SX1262 Shield User button -#define PIN_BUTTON1 5 -#define BUTTON_NEED_PULLUP - -// Digital Pins -// ------------ -#define D0 (0ul) -#define D1 (1ul) -#define D2 (2ul) -#define D3 (3ul) -#define D4 (4ul) -#define D5 (5ul) -#define D6 (6ul) -#define D7 (7ul) -#define D8 (8ul) -#define D9 (9ul) -#define D10 (10ul) - -// Analog Pins -// ----------- -#define PIN_A0 (0) -#define PIN_A1 (1) -#define PIN_A2 (2) -#define PIN_A3 (3) -#define PIN_A4 (4) -#define PIN_A5 (5) -#define PIN_VBAT (32) -#define VBAT_ENABLE (14) - -static const uint8_t A0 = PIN_A0; -static const uint8_t A1 = PIN_A1; -static const uint8_t A2 = PIN_A2; -static const uint8_t A3 = PIN_A3; -static const uint8_t A4 = PIN_A4; -static const uint8_t A5 = PIN_A5; -#define ADC_RESOLUTION 12 - -// Other Pins -// ---------- -#define PIN_NFC1 (30) -#define PIN_NFC2 (31) - -// RX and TX pins -#define PIN_SERIAL1_RX (-1) -#define PIN_SERIAL1_TX (-1) -// complains if not defined -#define PIN_SERIAL2_RX (-1) -#define PIN_SERIAL2_TX (-1) - -// 4 is used as RF_SW and 5 for USR button so... -#define PIN_WIRE_SDA (6) -#define PIN_WIRE_SCL (7) - -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; - -// SPI SX1262 -// ---------- -#define SPI_SX1262 -#ifdef SPI_SX1262 -#define SPI_INTERFACES_COUNT 1 - -#define PIN_SPI_MISO (9) -#define PIN_SPI_MOSI (10) -#define PIN_SPI_SCK (8) - -static const uint8_t SS = D3; -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK; - -// supported modules list -#define USE_SX1262 - -// common pinouts for SX126X modules -#define SX126X_CS D3 -#define SX126X_DIO1 D0 -#define SX126X_BUSY D1 -#define SX126X_RESET D2 - -// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_RXEN 38 -#define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#endif - -// Wire Interfaces -// ------------------- -#define WIRE_INTERFACES_COUNT 1 // 2 - -// Sense version has IMU and PDM Mic -// #define XIAO_SENSE -#ifndef XIAO_SENSE -// 6 DoF IMU -#define PIN_LSM6DS3TR_C_POWER (15) -#define PIN_LSM6DS3TR_C_INT1 (18) -// PDM Interfaces -// --------------- -#define PIN_PDM_PWR (19) -#define PIN_PDM_CLK (20) -#define PIN_PDM_DIN (21) -#endif - -// QSPI Pins -// --------- -#define PIN_QSPI_SCK (24) -#define PIN_QSPI_CS (25) -#define PIN_QSPI_IO0 (26) -#define PIN_QSPI_IO1 (27) -#define PIN_QSPI_IO2 (28) -#define PIN_QSPI_IO3 (29) - -// On-board QSPI Flash -// ------------------- -#define EXTERNAL_FLASH_DEVICES P25Q16H -#define EXTERNAL_FLASH_USE_QSPI - -// Battery -// ------- -// P0_14 = 14 Reads battery voltage from divider on signal board. -// PIN_VBAT is reading voltage divider on XIAO and is program pin 32 / or P0.31 -#define ADC_CTRL VBAT_ENABLE -#define ADC_CTRL_ENABLED LOW -#define BATTERY_SENSE_RESOLUTION_BITS 10 -#define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED -#define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge - -// The battery sense is hooked to pin A0 (5) -#define BATTERY_PIN PIN_VBAT // PIN_A0 - -// ratio of voltage divider = 3.0 (R17=1M, R18=510k) -#define ADC_MULTIPLIER 3 // 3.0 + a bit for being optimistic - -#ifdef __cplusplus -} -#endif - -/*---------------------------------------------------------------------------- - * Arduino objects - C++ only - *----------------------------------------------------------------------------*/ - -#endif \ No newline at end of file diff --git a/variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini b/variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini index a5d0aaf8f..c923bbdb7 100644 --- a/variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini +++ b/variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini @@ -6,7 +6,8 @@ build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DEBYTE_E22 -DEBYTE_E22_900M30S -build_unflags = -DGPS_L76K + -USEEED_XIAO_NRF52840_KIT ; remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define ; Seeed XIAO nRF52840 + EBYTE E22-900M33S - Pinout matching Wio-SX1262 (SKU 113010003) [env:seeed_xiao_nrf52840_e22_900m33s] @@ -16,4 +17,5 @@ build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DEBYTE_E22 -DEBYTE_E22_900M33S -build_unflags = -DGPS_L76K + -USEEED_XIAO_NRF52840_KIT ; remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define \ No newline at end of file diff --git a/variants/nrf52840/diy/xiao_ble/README.md b/variants/nrf52840/diy/xiao_ble/README.md index fe6dcba2d..efc1236a8 100644 --- a/variants/nrf52840/diy/xiao_ble/README.md +++ b/variants/nrf52840/diy/xiao_ble/README.md @@ -116,7 +116,7 @@ _(none)_ 1. Double press the XIAO nrf52840's `reset` button to put it in bootloader mode, and a USB volume named `XIAO SENSE` will appear 2. Copy the `firmware.uf2` file to the `XIAO SENSE` volume (refer to the last step of [Build Meshtastic](#2-build-meshtastic)) 3. The XIAO nrf52840's red LED will flash for several seconds as the firmware is copied -4. Once Meshtastic firmware succesfully boots, the: +4. Once Meshtastic firmware successfully boots, the: 1. Green LED will turn on 2. Red LED will flash several times to indicate flash memory writes during initial settings file creation 3. Green LED will blink every second once the firmware is running normally @@ -135,7 +135,7 @@ _(none)_ - If you don't see any specific error message, but the boot process is stuck or not proceeding as expected, this might also mean there is a conflict in `variant.h`. If you have made any changes to the pin mapping, ensure they do not result in a conflict. If all else fails, try reverting your changes and using the known-good configuration included here. - The above might also mean something is wired incorrectly. Try reverting to one of the known-good example wirings in section 4. - If the E22 gets hot to the touch: - - The power amplifier is likely running continually. Disconnect it and the XIAO from power immediately, and double check wiring and pin mapping. In my experimentation this occurred in cases where TXEN was inadvertenly high (usually due to a pin mapping conflict). + - The power amplifier is likely running continually. Disconnect it and the XIAO from power immediately, and double check wiring and pin mapping. In my experimentation this occurred in cases where TXEN was inadvertently high (usually due to a pin mapping conflict). ## 5. Notes diff --git a/variants/nrf52840/diy/xiao_ble/platformio.ini b/variants/nrf52840/diy/xiao_ble/platformio.ini index 6c764ea78..42f53f1bf 100644 --- a/variants/nrf52840/diy/xiao_ble/platformio.ini +++ b/variants/nrf52840/diy/xiao_ble/platformio.ini @@ -2,9 +2,55 @@ [env:xiao_ble] extends = env:seeed_xiao_nrf52840_kit board_level = extra -build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/seeed_xiao_nrf52840_kit + -Isrc/platform/nrf52/softdevice + -Isrc/platform/nrf52/softdevice/nrf52 -D PRIVATE_HW -DXIAO_BLE_LEGACY_PINOUT -DEBYTE_E22 - -DEBYTE_E22_900M30S -build_unflags = -DGPS_L76K + -USEEED_XIAO_NRF52840_KIT ; remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink + +; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +[env:xiao_ble_30db] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/seeed_xiao_nrf52840_kit + -Isrc/platform/nrf52/softdevice + -Isrc/platform/nrf52/softdevice/nrf52 + -DPRIVATE_HW ; Define private hardware + -DXIAO_BLE_LEGACY_PINOUT ; Set legacy pinout + -DEBYTE_E22_900M30S ; Set 30db module + -USEEED_XIAO_NRF52840_KIT ; Remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink + +; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +[env:xiao_ble_33db] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/seeed_xiao_nrf52840_kit + -Isrc/platform/nrf52/softdevice + -Isrc/platform/nrf52/softdevice/nrf52 + -DPRIVATE_HW ; Define private hardware + -DXIAO_BLE_LEGACY_PINOUT ; Set legacy pinout + -DEBYTE_E22_900M33S ; Set 33db module + -USEEED_XIAO_NRF52840_KIT ; Remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/nrf52840/dls_Minimesh_Lite/variant.h b/variants/nrf52840/dls_Minimesh_Lite/variant.h index f5163619b..32c16f06d 100644 --- a/variants/nrf52840/dls_Minimesh_Lite/variant.h +++ b/variants/nrf52840/dls_Minimesh_Lite/variant.h @@ -44,7 +44,6 @@ extern "C" { // LED #define PIN_LED1 (0 + 15) -#define LED_BUILTIN PIN_LED1 // Actually red #define LED_BLUE PIN_LED1 #define LED_STATE_ON 1 diff --git a/variants/nrf52840/feather_diy/variant.h b/variants/nrf52840/feather_diy/variant.h index 1c0979f82..a816e7867 100644 --- a/variants/nrf52840/feather_diy/variant.h +++ b/variants/nrf52840/feather_diy/variant.h @@ -49,12 +49,10 @@ extern "C" { #define PIN_LED1 (32 + 15) // P1.15 3 #define PIN_LED2 (32 + 10) // P1.10 4 -#define LED_BUILTIN PIN_LED1 - #define LED_GREEN PIN_LED2 // Actually red #define LED_BLUE PIN_LED1 -#define LED_STATE_ON 1 // State when LED is litted +#define LED_STATE_ON 1 // State when LED is lit #define BUTTON_PIN (32 + 2) // P1.02 7 diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini index e7eede80f..cecca3d81 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini +++ b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini @@ -11,4 +11,5 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/gat562_mesh_trial_tracker> diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp b/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h index 6337ac70c..b5632a7fb 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h @@ -46,13 +46,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -63,8 +60,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -264,8 +259,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -// #define HAS_RTC 1 - // #define HAS_ETHERNET 1 // #define RAK_4631 1 diff --git a/variants/nrf52840/heltec_mesh_node_t096/platformio.ini b/variants/nrf52840/heltec_mesh_node_t096/platformio.ini new file mode 100644 index 000000000..e1bdd529d --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/platformio.ini @@ -0,0 +1,34 @@ +; First prototype nrf52840/sx1262 device +[env:heltec-mesh-node-t096] +custom_meshtastic_hw_model = 127 +custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T096 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Mesh Node 096 +custom_meshtastic_images = heltec-mesh-node-t096.svg, heltec-mesh-node-t096-case.svg +custom_meshtastic_tags = Heltec + +extends = nrf52840_base +board = heltec_mesh_node_t096 +board_level = pr +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/heltec_mesh_node_t096 + -D HAS_LORA_FEM=1 + -D HELTEC_MESH_NODE_T096 + -D USE_TFTDISPLAY=1 + -D USER_SETUP_LOADED + -D ST7735_DRIVER + -D ST7735_REDTAB160x80 + -D TFT_SPI_PORT=SPI1 + -D TFT_CS=ST7735_CS ; Chip select control + -D TFT_DC=ST7735_RS ; Data Command control pin + -D TFT_RST=ST7735_RESET ; Reset pin + -D TFT_BL=ST7735_BL ; LED back-light + -D TFT_BACKLIGHT_ON=LOW +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t096> +lib_deps = + ${nrf52840_base.lib_deps} + bodmer/TFT_eSPI@2.5.43 ; renovate: datasource=platformio-registry depName=bodmer/TFT_eSPI diff --git a/variants/nrf52840/heltec_mesh_node_t096/variant.cpp b/variants/nrf52840/heltec_mesh_node_t096/variant.cpp new file mode 100644 index 000000000..29158e8ba --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/variant.cpp @@ -0,0 +1,76 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); +} + +void variant_shutdown() +{ + nrf_gpio_cfg_default(VEXT_ENABLE); + nrf_gpio_cfg_default(ST7735_CS); + nrf_gpio_cfg_default(ST7735_RS); + nrf_gpio_cfg_default(ST7735_SDA); + nrf_gpio_cfg_default(ST7735_SCK); + nrf_gpio_cfg_default(ST7735_RESET); + nrf_gpio_cfg_default(ST7735_BL); + + nrf_gpio_cfg_default(PIN_LED1); + + // nrf_gpio_cfg_default(LORA_PA_POWER); + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, LOW); + + nrf_gpio_cfg_default(LORA_KCT8103L_PA_CSD); + nrf_gpio_cfg_default(LORA_KCT8103L_PA_CTX); + + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, LOW); + + nrf_gpio_cfg_default(SX126X_CS); + nrf_gpio_cfg_default(SX126X_DIO1); + nrf_gpio_cfg_default(SX126X_BUSY); + nrf_gpio_cfg_default(SX126X_RESET); + + nrf_gpio_cfg_default(PIN_SPI_MISO); + nrf_gpio_cfg_default(PIN_SPI_MOSI); + nrf_gpio_cfg_default(PIN_SPI_SCK); + + nrf_gpio_cfg_default(PIN_GPS_PPS); + nrf_gpio_cfg_default(PIN_GPS_RESET); + nrf_gpio_cfg_default(PIN_GPS_EN); + nrf_gpio_cfg_default(GPS_TX_PIN); + nrf_gpio_cfg_default(GPS_RX_PIN); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t096/variant.h b/variants/nrf52840/heltec_mesh_node_t096/variant.h new file mode 100644 index 000000000..04e22af26 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/variant.h @@ -0,0 +1,202 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_HELTEC_NRF_ +#define _VARIANT_HELTEC_NRF_ +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define VEXT_ENABLE (0 + 26) +#define VEXT_ON_VALUE HIGH + +// ST7735S TFT LCD +#define ST7735_CS (0 + 22) +#define ST7735_RS (0 + 15) // DC +#define ST7735_SDA (0 + 17) // MOSI +#define ST7735_SCK (0 + 20) +#define ST7735_RESET (0 + 13) +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define ST7735_BL (32 + 12) +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define SCREEN_ROTATE +#define TFT_HEIGHT 160 +#define TFT_WIDTH 80 +#define TFT_OFFSET_X 24 +#define TFT_OFFSET_Y 0 +#define TFT_INVERT false +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +#define DISPLAY_FORCE_SMALL_FONTS + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (0 + 28) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_STATE_ON 1 // State when LED is lit + +// #define HAS_NEOPIXEL // Enable the use of neopixels +// #define NEOPIXEL_COUNT 2 // How many neopixels are connected +// #define NEOPIXEL_DATA 14 // gpio pin used to send data to the neopixels +// #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 10) +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO + +/* +No longer populated on PCB +*/ +#define PIN_SERIAL2_RX (0 + 9) +#define PIN_SERIAL2_TX (0 + 10) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 2 + +// I2C bus 0 +#define PIN_WIRE_SDA (0 + 7) // SDA +#define PIN_WIRE_SCL (0 + 8) // SCL + +// I2C bus 1 +#define PIN_WIRE1_SDA (0 + 4) // SDA (secondary bus) +#define PIN_WIRE1_SCL (0 + 27) // SCL (secondary bus) + +/* + * Lora radio + */ + +#define USE_SX1262 +#define SX126X_CS (0 + 5) // FIXME - we really should define LORA_CS instead +#define LORA_CS (0 + 5) +#define SX126X_DIO1 (0 + 21) +#define SX126X_BUSY (0 + 19) +#define SX126X_RESET (0 + 16) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// ---- KCT8103L RF FRONT END CONFIGURATION ---- +// The heltec_wireless_tracker_v2 uses a KCT8103L FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna +// Control logic (from KCT8103L datasheet): +// Transmit PA: CSD=1, CTX=1, CPS=1 +// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) +// Receive bypass: CSD=1, CTX=1, CPS=0 +// Shutdown: CSD=0, CTX=X, CPS=X +// Pin mapping: +// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO12: Chip enable (HIGH=on, LOW=shutdown) +// CTX (pin 6) -> GPIO41: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO30 +// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) + +#define USE_KCT8103L_PA +#define LORA_PA_POWER (0 + 30) // VFEM_Ctrl - KCT8103L LDO power enable +#define LORA_KCT8103L_PA_CSD (0 + 12) // CSD - KCT8103L chip enable (HIGH=on) +#define LORA_KCT8103L_PA_CTX \ + (32 + 9) // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 14) +#define PIN_SPI_MOSI (0 + 11) +#define PIN_SPI_SCK (32 + 8) + +#define PIN_SPI1_MISO \ + ST7735_MISO // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO +#define PIN_SPI1_MOSI ST7735_SDA +#define PIN_SPI1_SCK ST7735_SCK + +/* + * GPS pins + */ +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 +#define PIN_GPS_RESET (32 + 14) // An output to reset UC6580 GPS. As per datasheet, low for > 100ms will reset the UC6580 +#define GPS_RESET_MODE LOW +#define PIN_GPS_EN (0 + 6) +#define GPS_EN_ACTIVE LOW +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define PIN_GPS_PPS (32 + 11) +#define GPS_TX_PIN (0 + 25) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (0 + 23) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +#define ADC_CTRL (32 + 15) +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN (0 + 3) +#define ADC_RESOLUTION 14 + +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (4.916F) + +// rf52840 AIN1 = Pin 3 +#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_1 + +// We have AIN1 with a VBAT divider so AIN1 = VBAT * (100/490) +// We have the device going deep sleep under 3.1V, which is AIN1 = 0.63V +// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN2 = 0.67V +// Ratio 0.67/3.3 = 0.20, so we can pick a bit higher, 2/8 VDD, which means +// VBAT=4.04V +#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8 + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h index b6be70ff4..ad17e7457 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -73,7 +74,8 @@ void setupNicheGraphics() inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background @@ -93,4 +95,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h index 14170d5f3..2802e4c1d 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h @@ -30,7 +30,6 @@ extern "C" { #define PIN_LED1 (32 + 3) // green (confirmed on 1.0 board) #define LED_BLUE PIN_LED1 // fake for bluefruit library #define LED_GREEN PIN_LED1 -#define LED_BUILTIN LED_GREEN #define LED_STATE_ON 0 // State when LED is lit #define HAS_NEOPIXEL // Enable the use of neopixels @@ -138,8 +137,6 @@ No longer populated on PCB #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER diff --git a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini index a39872205..c9f998240 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini @@ -23,4 +23,4 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip + https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index cf14b5e04..e7385c4bb 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -74,7 +74,6 @@ extern "C" { #define PIN_LED1 (32 + 3) // green (confirmed on 1.0 board) #define LED_BLUE PIN_LED1 // fake for bluefruit library #define LED_GREEN PIN_LED1 -#define LED_BUILTIN LED_GREEN #define LED_STATE_ON 0 // State when LED is lit #define HAS_NEOPIXEL // Enable the use of neopixels @@ -193,8 +192,6 @@ No longer populated on PCB #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER @@ -226,7 +223,6 @@ No longer populated on PCB // VBAT=4.04V #define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8 -#define HAS_RTC 0 #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h index 10f628d56..187022ea7 100644 --- a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -67,6 +68,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 @@ -87,4 +89,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/heltec_mesh_pocket/platformio.ini b/variants/nrf52840/heltec_mesh_pocket/platformio.ini index 646304a5a..c9b599382 100644 --- a/variants/nrf52840/heltec_mesh_pocket/platformio.ini +++ b/variants/nrf52840/heltec_mesh_pocket/platformio.ini @@ -15,6 +15,7 @@ custom_meshtastic_display_name = Heltec Mesh Pocket custom_meshtastic_actively_supported = true custom_meshtastic_variant = 5000mAh custom_meshtastic_key = heltec_mesh_pocket +custom_meshtastic_has_ink_hud = true # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} @@ -38,7 +39,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud @@ -78,6 +79,7 @@ custom_meshtastic_display_name = Heltec Mesh Pocket custom_meshtastic_actively_supported = true custom_meshtastic_variant = 10000mAh custom_meshtastic_key = heltec_mesh_pocket +custom_meshtastic_has_ink_hud = true # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} @@ -101,7 +103,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud diff --git a/variants/nrf52840/heltec_mesh_pocket/variant.h b/variants/nrf52840/heltec_mesh_pocket/variant.h index 7ec9b88ea..a86faf575 100644 --- a/variants/nrf52840/heltec_mesh_pocket/variant.h +++ b/variants/nrf52840/heltec_mesh_pocket/variant.h @@ -26,8 +26,6 @@ extern "C" { #define LED_RED PIN_LED1 #define LED_BLUE PIN_LED1 #define LED_GREEN PIN_LED1 -#define LED_BUILTIN LED_BLUE -#define LED_CONN LED_BLUE #define LED_STATE_ON 0 // State when LED is lit /* @@ -100,8 +98,6 @@ No longer populated on PCB #define PIN_SPI_MOSI (0 + 5) #define PIN_SPI_SCK (0 + 4) -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER diff --git a/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h b/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h index 125f50590..0f4131916 100644 --- a/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -67,6 +68,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 @@ -87,4 +89,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini index 69264f0df..1b6f59a68 100644 --- a/variants/nrf52840/heltec_mesh_solar/platformio.ini +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -16,7 +16,7 @@ lib_deps = # renovate: datasource=git-refs depName=NMIoT-meshsolar packageName=https://github.com/NMIoT/meshsolar gitBranch=main https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson - bblanchon/ArduinoJson@6.21.5 + bblanchon/ArduinoJson@6.21.6 [env:heltec-mesh-solar] custom_meshtastic_hw_model = 108 @@ -68,7 +68,7 @@ build_flags = ${heltec_mesh_solar_base.build_flags} lib_deps = ${heltec_mesh_solar_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-solar-inkhud] extends = heltec_mesh_solar_base, inkhud @@ -132,4 +132,4 @@ build_flags = ${heltec_mesh_solar_base.build_flags} lib_deps = ${heltec_mesh_solar_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip + https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h index 112bcd8b3..8d4db4bea 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.h +++ b/variants/nrf52840/heltec_mesh_solar/variant.h @@ -39,16 +39,15 @@ extern "C" { #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) -#define PIN_LED1 (0 + 4) // green (confirmed on 1.0 board) -#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define PIN_LED1 (32 + 15) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library #define LED_GREEN PIN_LED1 -#define LED_BUILTIN LED_GREEN #define LED_STATE_ON 0 // State when LED is lit -#define HAS_NEOPIXEL // Enable the use of neopixels -#define NEOPIXEL_COUNT 1 // How many neopixels are connected -#define NEOPIXEL_DATA (32 + 15) // gpio pin used to send data to the neopixels -#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use +// #define HAS_NEOPIXEL // Enable the use of neopixels +// #define NEOPIXEL_COUNT 1 // How many neopixels are connected +// #define NEOPIXEL_DATA (32 + 15) // gpio pin used to send data to the neopixels +// #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use /* * Buttons @@ -60,8 +59,8 @@ extern "C" { /* No longer populated on PCB */ -#define PIN_SERIAL2_RX (0 + 9) -#define PIN_SERIAL2_TX (0 + 10) +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) /* * I2C @@ -133,15 +132,21 @@ No longer populated on PCB #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER +// Hardware watchdog +#define HAS_HARDWARE_WATCHDOG +#define HARDWARE_WATCHDOG_DONE (0 + 9) +#define HARDWARE_WATCHDOG_WAKE (0 + 10) +#define HARDWARE_WATCHDOG_TIMEOUT_MS (6 * 60 * 1000) // 6 minute watchdog + #define BQ4050_SDA_PIN (32 + 1) // I2C data line pin #define BQ4050_SCL_PIN (32 + 0) // I2C clock line pin #define BQ4050_EMERGENCY_SHUTDOWN_PIN (32 + 3) // Emergency shutdown pin +#define SERIAL_PRINT_PORT 0 + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini index e2631affe..f3dc6185c 100644 --- a/variants/nrf52840/meshlink/platformio.ini +++ b/variants/nrf52840/meshlink/platformio.ini @@ -12,6 +12,7 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlink> debug_tool = jlink @@ -30,6 +31,7 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 -D USE_EINK -D EINK_DISPLAY_MODEL=GxEPD2_213_B74 -D EINK_WIDTH=250 @@ -47,8 +49,8 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlin lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds -;upload_protocol = jlink \ No newline at end of file +;upload_protocol = jlink diff --git a/variants/nrf52840/meshlink/variant.h b/variants/nrf52840/meshlink/variant.h index d1dba574f..00107ac34 100644 --- a/variants/nrf52840/meshlink/variant.h +++ b/variants/nrf52840/meshlink/variant.h @@ -31,10 +31,8 @@ extern "C" { // LEDs #define PIN_LED1 (24) // Built in white led for status #define LED_BLUE PIN_LED1 -#define LED_BUILTIN PIN_LED1 -#define LED_STATE_ON 0 // State when LED is litted -#define LED_INVERTED 1 +#define LED_STATE_ON 0 // State when LED is lit // Testing USB detection // #define NRF_APM @@ -54,6 +52,7 @@ extern "C" { */ #define PIN_SERIAL1_RX (32 + 8) #define PIN_SERIAL1_TX (7) +#define SERIAL_PRINT_PORT 0 /* * SPI Interfaces @@ -150,4 +149,4 @@ static const uint8_t SCK = PIN_SPI_SCK; /*---------------------------------------------------------------------------- * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/meshtiny/variant.cpp b/variants/nrf52840/meshtiny/variant.cpp index 2e8b00e4b..aae3da9f2 100644 --- a/variants/nrf52840/meshtiny/variant.cpp +++ b/variants/nrf52840/meshtiny/variant.cpp @@ -32,12 +32,12 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 + // LED1 & LED_BLUE pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); + pinMode(LED_BLUE, OUTPUT); + ledOff(LED_BLUE); // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); diff --git a/variants/nrf52840/meshtiny/variant.h b/variants/nrf52840/meshtiny/variant.h index 8d634ba60..4289fef4c 100644 --- a/variants/nrf52840/meshtiny/variant.h +++ b/variants/nrf52840/meshtiny/variant.h @@ -47,13 +47,9 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 #define LED_STATE_ON 1 // State when LED is litted @@ -66,8 +62,6 @@ extern "C" { #define INPUTDRIVER_ENCODER_BTN 28 #define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 150 -#define CANNED_MESSAGE_MODULE_ENABLE 1 - /* * Buzzer - PWM */ diff --git a/variants/nrf52840/monteops_hw1/variant.cpp b/variants/nrf52840/monteops_hw1/variant.cpp index 75cca1dc3..81ae9f482 100644 --- a/variants/nrf52840/monteops_hw1/variant.cpp +++ b/variants/nrf52840/monteops_hw1/variant.cpp @@ -35,7 +35,4 @@ void initVariant() // LED1 & LED2 pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); } diff --git a/variants/nrf52840/monteops_hw1/variant.h b/variants/nrf52840/monteops_hw1/variant.h index 97536b169..a7fa7125b 100644 --- a/variants/nrf52840/monteops_hw1/variant.h +++ b/variants/nrf52840/monteops_hw1/variant.h @@ -50,13 +50,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) // Connected to WWAN host LED (if present) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) // Connected to WWAN host LED (if present) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -67,8 +64,6 @@ extern "C" { // #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -213,8 +208,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (1.73F) -// #define HAS_RTC 1 - #define HAS_ETHERNET 1 #define PIN_ETHERNET_RESET 21 diff --git a/variants/nrf52840/muzi_base/variant.cpp b/variants/nrf52840/muzi_base/variant.cpp index da01de974..e6178a968 100644 --- a/variants/nrf52840/muzi_base/variant.cpp +++ b/variants/nrf52840/muzi_base/variant.cpp @@ -63,8 +63,8 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); digitalWrite(PIN_LED1, HIGH); - pinMode(PIN_LED2, OUTPUT); - digitalWrite(PIN_LED2, HIGH); + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_BLUE, HIGH); // Initialize LoRa pins pinMode(SX126X_RESET, OUTPUT); diff --git a/variants/nrf52840/muzi_base/variant.h b/variants/nrf52840/muzi_base/variant.h index 96604c400..0cbd0e3ef 100644 --- a/variants/nrf52840/muzi_base/variant.h +++ b/variants/nrf52840/muzi_base/variant.h @@ -38,15 +38,13 @@ extern "C" { #define COMPASS_ORIENTATION meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270 #define HAS_ICM20948 // forces the i2c address to be seen as this sensor -#define HAS_RTC 1 #define RX8130CE_RTC 0x32 // LEDs #define PIN_LED1 (32 + 3) // P1.03, Green -#define PIN_LED2 (32 + 4) // P1.04, Blue +#define LED_BLUE (32 + 4) // P1.04, Blue -#define LED_BUILTIN -1 // PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 0 // State when LED is lit // Buttons @@ -176,6 +174,8 @@ extern "C" { #define EXTERNAL_FLASH_DEVICES W25Q32JVSS #define EXTERNAL_FLASH_USE_QSPI +#define SERIAL_PRINT_PORT 0 + // NFC is disabled via CONFIG_NFCT_PINS_AS_GPIOS=1 build flag // This configures P0.09 and P0.10 as regular GPIO pins instead of NFC pins diff --git a/variants/nrf52840/nano-g2-ultra/variant.h b/variants/nrf52840/nano-g2-ultra/variant.h index d8f41a68c..631af72d8 100644 --- a/variants/nrf52840/nano-g2-ultra/variant.h +++ b/variants/nrf52840/nano-g2-ultra/variant.h @@ -41,18 +41,6 @@ extern "C" { #define NUM_ANALOG_INPUTS (1) #define NUM_ANALOG_OUTPUTS (0) -// LEDs -#define PIN_LED1 (-1) -#define PIN_LED2 (-1) -#define PIN_LED3 (-1) - -#define LED_RED PIN_LED3 -#define LED_BLUE PIN_LED1 -#define LED_GREEN PIN_LED2 - -#define LED_BUILTIN LED_BLUE -#define LED_CONN PIN_GREEN - #define LED_STATE_ON 0 // State when LED is lit /* @@ -141,7 +129,6 @@ External serial flash W25Q16JV_IQ // PCF8563 RTC Module #define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC #define PCF8563_RTC 0x51 -#define HAS_RTC 1 /* * SPI Interfaces @@ -153,8 +140,6 @@ External serial flash W25Q16JV_IQ #define PIN_SPI_MOSI (0 + 11) #define PIN_SPI_SCK (0 + 12) -// #define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER diff --git a/variants/nrf52840/nrf52.ini b/variants/nrf52840/nrf52.ini index a07fefb2f..f42c29308 100644 --- a/variants/nrf52840/nrf52.ini +++ b/variants/nrf52840/nrf52.ini @@ -2,12 +2,12 @@ ; Instead of the standard nordicnrf52 platform, we use our fork which has our added variant files platform = # renovate: datasource=custom.pio depName=platformio/nordicnrf52 packageName=platformio/platform/nordicnrf52 - platformio/nordicnrf52@10.10.0 + platformio/nordicnrf52@10.11.0 extends = arduino_base platform_packages = - ; our custom Git version until they merge our PR + ; our custom Git version with C++17 support in platform.txt # TODO renovate - platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#c770c8a16a351b55b86e347a3d9d7b74ad0bbf39 + platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#cpp17-platform ; Don't renovate toolchain-gccarmnoneeabi platformio/toolchain-gccarmnoneeabi@~1.90301.0 @@ -25,6 +25,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -Os + -std=gnu++17 build_unflags = -Ofast -Og @@ -35,6 +36,8 @@ build_unflags = -g -g1 -g0 + -std=c++11 + -std=gnu++11 build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - diff --git a/variants/nrf52840/nrf52840.ini b/variants/nrf52840/nrf52840.ini index 09b2ef97d..c5590cbc3 100644 --- a/variants/nrf52840/nrf52840.ini +++ b/variants/nrf52840/nrf52840.ini @@ -4,6 +4,7 @@ extends = nrf52_base build_flags = ${nrf52_base.build_flags} -DSERIAL_BUFFER_SIZE=4096 + -DLED_BUILTIN=-1 lib_deps = ${nrf52_base.lib_deps} @@ -79,4 +80,4 @@ debug_speed = 4000 ; The following is not needed because it automatically tries do this ;debug_server_ready_pattern = -.*GDB server started on port \d+.* -;debug_port = localhost:3333 \ No newline at end of file +;debug_port = localhost:3333 diff --git a/variants/nrf52840/r1-neo/platformio.ini b/variants/nrf52840/r1-neo/platformio.ini index 85fe49cf1..0aaec2330 100644 --- a/variants/nrf52840/r1-neo/platformio.ini +++ b/variants/nrf52840/r1-neo/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/r1-neo> + + lib_deps = ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/r1-neo/variant.cpp b/variants/nrf52840/r1-neo/variant.cpp index d87b88c85..c36e88602 100644 --- a/variants/nrf52840/r1-neo/variant.cpp +++ b/variants/nrf52840/r1-neo/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail // pinMode(PIN_3V3_EN, OUTPUT); // digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/r1-neo/variant.h b/variants/nrf52840/r1-neo/variant.h index b1d96ebd0..42d44d673 100644 --- a/variants/nrf52840/r1-neo/variant.h +++ b/variants/nrf52840/r1-neo/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (32 + 4) // P1.04 Controls Green LED -#define PIN_LED2 (28) // P0.28 Controls Blue LED - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (28) // P0.28 Controls Blue LED #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -135,8 +132,6 @@ static const uint8_t SCK = PIN_SPI_SCK; #define ADC_MULTIPLIER 1.667 #define OCV_ARRAY 4120, 4020, 4000, 3940, 3870, 3820, 3750, 3630, 3550, 3450, 3100 -#define HAS_RTC 1 - #define RX8130CE_RTC 0x32 #ifdef __cplusplus diff --git a/variants/nrf52840/rak2560/platformio.ini b/variants/nrf52840/rak2560/platformio.ini index 1703a13ae..54b66f4b2 100644 --- a/variants/nrf52840/rak2560/platformio.ini +++ b/variants/nrf52840/rak2560/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 -DHAS_RAKPROT=1 ; Define if RAk OneWireSerial is used (disables GPS) build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak2560> + + + lib_deps = diff --git a/variants/nrf52840/rak2560/variant.cpp b/variants/nrf52840/rak2560/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak2560/variant.cpp +++ b/variants/nrf52840/rak2560/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/rak2560/variant.h b/variants/nrf52840/rak2560/variant.h index f922e8a61..acd1ae60e 100644 --- a/variants/nrf52840/rak2560/variant.h +++ b/variants/nrf52840/rak2560/variant.h @@ -46,13 +46,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -63,8 +60,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -249,8 +244,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define HAS_RTC 1 - #define RAK_4631 1 #define HALF_UART_PIN PIN_SERIAL1_RX diff --git a/variants/nrf52840/rak3401_1watt/platformio.ini b/variants/nrf52840/rak3401_1watt/platformio.ini index bb8fa28df..889a17ed6 100644 --- a/variants/nrf52840/rak3401_1watt/platformio.ini +++ b/variants/nrf52840/rak3401_1watt/platformio.ini @@ -22,6 +22,7 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak3401_1watt> + lib_deps = ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/rak3401_1watt/variant.cpp b/variants/nrf52840/rak3401_1watt/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak3401_1watt/variant.cpp +++ b/variants/nrf52840/rak3401_1watt/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/rak3401_1watt/variant.h b/variants/nrf52840/rak3401_1watt/variant.h index d4bb1a175..80b09cf69 100644 --- a/variants/nrf52840/rak3401_1watt/variant.h +++ b/variants/nrf52840/rak3401_1watt/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -211,8 +208,6 @@ static const uint8_t SCK = PIN_SPI_SCK; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define HAS_RTC 1 - #define RAK_4631 1 #ifdef __cplusplus diff --git a/variants/nrf52840/rak4631/platformio.ini b/variants/nrf52840/rak4631/platformio.ini index 4a96fc8d9..179d73e92 100644 --- a/variants/nrf52840/rak4631/platformio.ini +++ b/variants/nrf52840/rak4631/platformio.ini @@ -21,6 +21,7 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 build_src_filter = ${nrf52_base.build_src_filter} \ +<../variants/nrf52840/rak4631> \ + \ diff --git a/variants/nrf52840/rak4631/variant.cpp b/variants/nrf52840/rak4631/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631/variant.cpp +++ b/variants/nrf52840/rak4631/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/rak4631/variant.h b/variants/nrf52840/rak4631/variant.h index 302e531d5..6a6b32f27 100644 --- a/variants/nrf52840/rak4631/variant.h +++ b/variants/nrf52840/rak4631/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -253,9 +248,13 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define RV3028_RTC (uint8_t)0b1010010 // RAK18001 Buzzer in Slot C -// #define PIN_BUZZER 21 // IO3 is PWM2 +#define PIN_BUZZER 21 // IO3 is PWM2 // NEW: set this via protobuf instead! +// RAK4631 custom ringtone +#undef USERPREFS_RINGTONE_RTTTL +#define USERPREFS_RINGTONE_RTTTL "Rak:d=32,o=5,b=200:b7,p,b7,4p,p" + // Battery // The battery sense is hooked to pin A0 (5) #define BATTERY_PIN PIN_A0 @@ -281,8 +280,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // VDD=3.3V AIN3=6/8*VDD=2.47V VBAT=1.66*AIN3=4.1V #define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_11_16 -#define HAS_RTC 1 - #define HAS_ETHERNET 1 #define RAK_4631 1 diff --git a/variants/nrf52840/rak4631_epaper/platformio.ini b/variants/nrf52840/rak4631_epaper/platformio.ini index f0da832cb..f71fb6301 100644 --- a/variants/nrf52840/rak4631_epaper/platformio.ini +++ b/variants/nrf52840/rak4631_epaper/platformio.ini @@ -11,11 +11,12 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_epaper> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library diff --git a/variants/nrf52840/rak4631_epaper/variant.cpp b/variants/nrf52840/rak4631_epaper/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631_epaper/variant.cpp +++ b/variants/nrf52840/rak4631_epaper/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/rak4631_epaper/variant.h b/variants/nrf52840/rak4631_epaper/variant.h index c1e11bee5..82c26af7b 100644 --- a/variants/nrf52840/rak4631_epaper/variant.h +++ b/variants/nrf52840/rak4631_epaper/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -222,8 +217,6 @@ static const uint8_t SCK = PIN_SPI_SCK; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define HAS_RTC 1 - #define RAK_4631 1 #ifdef __cplusplus diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini index 112ddfc29..670b2c415 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini @@ -13,11 +13,12 @@ build_flags = ${nrf52840_base.build_flags} -D RADIOLIB_EXCLUDE_SX128X=1 -D RADIOLIB_EXCLUDE_SX127X=1 -D RADIOLIB_EXCLUDE_LR11X0=1 + -D RADIOLIB_EXCLUDE_LR2021=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_epaper_onrxtx> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp b/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp +++ b/variants/nrf52840/rak4631_epaper_onrxtx/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/variant.h b/variants/nrf52840/rak4631_epaper_onrxtx/variant.h index 1f8257e8e..2d34ab84c 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/variant.h +++ b/variants/nrf52840/rak4631_epaper_onrxtx/variant.h @@ -27,13 +27,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -195,8 +192,6 @@ static const uint8_t SCK = PIN_SPI_SCK; // #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 // #define ADC_MULTIPLIER 1.73 -// #define HAS_RTC 1 - #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini index e06a271aa..0fded96f4 100644 --- a/variants/nrf52840/rak4631_eth_gw/platformio.ini +++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini @@ -14,7 +14,6 @@ build_flags = ${nrf52840_base.build_flags} -DMESHTASTIC_EXCLUDE_WIFI=1 -DMESHTASTIC_EXCLUDE_SCREEN=1 ; -DMESHTASTIC_EXCLUDE_PKI=1 - -DMESHTASTIC_EXCLUDE_POWER_FSM=1 -DMESHTASTIC_EXCLUDE_POWERMON=1 ; -DMESHTASTIC_EXCLUDE_TZ=1 -DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 @@ -36,7 +35,7 @@ lib_deps = # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson - bblanchon/ArduinoJson@6.21.5 + bblanchon/ArduinoJson@6.21.6 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds ;upload_protocol = jlink diff --git a/variants/nrf52840/rak4631_eth_gw/variant.cpp b/variants/nrf52840/rak4631_eth_gw/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631_eth_gw/variant.cpp +++ b/variants/nrf52840/rak4631_eth_gw/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/rak4631_eth_gw/variant.h b/variants/nrf52840/rak4631_eth_gw/variant.h index c8a2f83ae..eb1d558ea 100644 --- a/variants/nrf52840/rak4631_eth_gw/variant.h +++ b/variants/nrf52840/rak4631_eth_gw/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -254,8 +249,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define HAS_RTC 1 - #define HAS_ETHERNET 1 #define RAK_4631 1 diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini b/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini index 07d763df3..f1641e7e4 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini @@ -21,11 +21,12 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_nomadstar_meteor_pro> + + lib_deps = ${nrf52840_base.lib_deps} - # TODO renovate - https://github.com/NomadStar-outdoor/IOBoard-RGB-LP5562-Library.git#9c366c8 + # renovate: datasource=git-refs depName=IOBoard-RGB-LP5562-Library packageName=NomadStar-outdoor/IOBoard-RGB-LP5562-Library gitBranch=master + https://github.com/NomadStar-outdoor/IOBoard-RGB-LP5562-Library/archive/9c366c875e1e8103ed97b5d4c09f3878345da80a.zip ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp index e84b60b3b..a035fbaf0 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h index 51baf3ada..aea497305 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -249,8 +244,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define HAS_RTC 0 - #define HAS_ETHERNET 0 #define RAK_4631 1 diff --git a/variants/nrf52840/rak_wismeshtag/platformio.ini b/variants/nrf52840/rak_wismeshtag/platformio.ini index 1e6e63e60..07fd6e73f 100644 --- a/variants/nrf52840/rak_wismeshtag/platformio.ini +++ b/variants/nrf52840/rak_wismeshtag/platformio.ini @@ -19,5 +19,6 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 -DMESHTASTIC_EXCLUDE_WIFI=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak_wismeshtag> diff --git a/variants/nrf52840/rak_wismeshtag/variant.cpp b/variants/nrf52840/rak_wismeshtag/variant.cpp index e84b60b3b..a0394b2dd 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.cpp +++ b/variants/nrf52840/rak_wismeshtag/variant.cpp @@ -19,7 +19,11 @@ */ #include "variant.h" +#include "Arduino.h" +#include "FreeRTOS.h" #include "nrf.h" +#include "power/PowerHAL.h" +#include "sleep.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -36,10 +40,43 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } + +#ifdef LOW_VDD_SYSTEMOFF_DELAY_MS +void variant_nrf52LoopHook(void) +{ + // If VDD stays unsafe for a while (brownout), force System OFF. + // Skip when VBUS present to allow recovery while USB-powered. + if (!powerHAL_isVBUSConnected()) { + // Rate-limit VDD safety checks: powerHAL_isPowerLevelSafe() calls getVDDVoltage() each time. + static constexpr uint32_t POWER_LEVEL_CHECK_INTERVAL_MS = 100; + static uint32_t last_vdd_check_ms = 0; + static bool last_power_level_safe = true; + + const uint32_t now = millis(); + if (last_vdd_check_ms == 0 || (uint32_t)(now - last_vdd_check_ms) >= POWER_LEVEL_CHECK_INTERVAL_MS) { + last_vdd_check_ms = now; + last_power_level_safe = powerHAL_isPowerLevelSafe(); + } + + // Do not use millis()==0 as a sentinel: at boot, millis() may be 0 while VDD is unsafe. + static bool low_vdd_timer_armed = false; + static uint32_t low_vdd_since_ms = 0; + + if (!last_power_level_safe) { + if (!low_vdd_timer_armed) { + low_vdd_since_ms = now; + low_vdd_timer_armed = true; + } + if ((uint32_t)(now - low_vdd_since_ms) >= (uint32_t)LOW_VDD_SYSTEMOFF_DELAY_MS) { + cpuDeepSleep(portMAX_DELAY); + } + } else { + low_vdd_timer_armed = false; + } + } +} +#endif diff --git a/variants/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index fa3e252ab..9ea215e42 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -230,7 +225,41 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define AREF_VOLTAGE 3.0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define OCV_ARRAY 4240, 4112, 4029, 3970, 3906, 3846, 3824, 3802, 3776, 3650, 3072 +#define OCV_ARRAY 4160, 4020, 3940, 3870, 3810, 3760, 3740, 3720, 3680, 3620, 2990 // updated OCV array for rak_wismeshtag + +// Wake from System OFF when battery rises again (LPCOMP). +// BAT_ADC divider: R22=1M (top), R24=1.5M (bottom) => V_BAT_ADC = VBAT * (1.5 / (1.0 + 1.5)) = 0.6 * VBAT +// RAK4630 module: AIN0 = nrf52840 AIN3 = Pin 5 (A0/BATTERY_PIN) +#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 +// LPCOMP compares the selected input to a fraction of VDD (here 5/8 of VDD at the LPCOMP input). +// With VDD ≈ 3.3 V: threshold at input ≈ (5/8) * 3.3 V ≈ 2.06 V. +// BAT_ADC divider: V_BAT_ADC = 0.6 * VBAT → equivalent VBAT ≈ 2.06 / 0.6 ≈ 3.4 V (wake when battery recovers). +// +// Note: if VDD is drooping/tracking VBAT in the low-voltage region, using a fraction >= divider ratio helps ensure the +// input is below the threshold at shutdown; the intended wake event happens when the supply recovers enough for a rising +// crossing to occur. +#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_5_8 + +// Low voltage protection: +// If VDD is below SAFE_VDD_VOLTAGE_THRESHOLD for longer than this delay (and no USB VBUS), +// the device will enter System OFF to avoid brownout loops and flash corruption. +#ifndef LOW_VDD_SYSTEMOFF_DELAY_MS +#define LOW_VDD_SYSTEMOFF_DELAY_MS 5000 +#endif + +// Prefer integer mV so platform code avoids float→int truncation quirks (e.g. 0.1 V → 99 vs 100 mV). +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_MV +#define SAFE_VDD_VOLTAGE_THRESHOLD_MV 2900 +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV 100 +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD +#define SAFE_VDD_VOLTAGE_THRESHOLD (SAFE_VDD_VOLTAGE_THRESHOLD_MV / 1000.0f) +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST (SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV / 1000.0f) +#endif #define RAK_4631 1 diff --git a/variants/nrf52840/rak_wismeshtap/variant.cpp b/variants/nrf52840/rak_wismeshtap/variant.cpp index 36572b074..73d2fac04 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.cpp +++ b/variants/nrf52840/rak_wismeshtap/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/rak_wismeshtap/variant.h b/variants/nrf52840/rak_wismeshtap/variant.h index a7b9290a5..358117cd5 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.h +++ b/variants/nrf52840/rak_wismeshtap/variant.h @@ -45,13 +45,10 @@ extern "C" { // LEDs #define PIN_LED1 (35) -#define PIN_LED2 (36) - -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 +#define LED_BLUE (36) #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 +#define LED_NOTIFICATION LED_BLUE #define LED_STATE_ON 1 // State when LED is litted @@ -62,8 +59,6 @@ extern "C" { #define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion such as the RAK14014 or RAK14015 TFT modules #define BUTTON_NEED_PULLUP #define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 /* * Analog pins @@ -271,8 +266,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (1.73F) -#define HAS_RTC 1 - #define RAK_4631 1 #define AQ_SET_PIN 10 @@ -283,7 +276,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define RAK14014 // Tell it we have a RAK14014 #define USER_SETUP_LOADED 1 -#define DISABLE_ALL_LIBRARY_WARNINGS 1 #define ST7789_DRIVER 1 #define TFT_WIDTH 240 #define TFT_HEIGHT 320 @@ -311,7 +303,6 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define USE_POWERSAVE #define SLEEP_TIME 120 -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define USE_VIRTUAL_KEYBOARD 1 /*---------------------------------------------------------------------------- * Arduino objects - C++ only diff --git a/variants/nrf52840/seeed_solar_node/variant.cpp b/variants/nrf52840/seeed_solar_node/variant.cpp index 994e97ff9..4123944d4 100644 --- a/variants/nrf52840/seeed_solar_node/variant.cpp +++ b/variants/nrf52840/seeed_solar_node/variant.cpp @@ -101,7 +101,6 @@ void initVariant() pinMode(PIN_LED2, OUTPUT); digitalWrite(PIN_LED2, LOW); pinMode(PIN_LED2, OUTPUT); - // digitalWrite(LED_PIN, LOW); pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, HIGH); diff --git a/variants/nrf52840/seeed_solar_node/variant.h b/variants/nrf52840/seeed_solar_node/variant.h index b2a1e6dff..69736a9e0 100644 --- a/variants/nrf52840/seeed_solar_node/variant.h +++ b/variants/nrf52840/seeed_solar_node/variant.h @@ -23,12 +23,8 @@ #define PIN_LED1 (12) // LED P1.15 #define PIN_LED2 (11) // -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 -// #define LED_PIN PIN_LED2 #define LED_STATE_ON 1 // State when LED is litted // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration diff --git a/variants/nrf52840/seeed_wio_tracker_L1/variant.h b/variants/nrf52840/seeed_wio_tracker_L1/variant.h index b62b65161..a1ec2508a 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1/variant.h @@ -23,13 +23,9 @@ #define PIN_LED1 (11) // LED P1.15 #define PIN_LED2 (12) // -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 -// #define LED_PIN PIN_LED2 -#define LED_STATE_ON 1 // State when LED is litted +#define LED_STATE_ON 1 // State when LED is lit // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -158,8 +154,6 @@ static const uint8_t SCL = PIN_WIRE_SCL; // joystick // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -#define CANNED_MESSAGE_MODULE_ENABLE 1 - #define CANNED_MESSAGE_ADD_CONFIRMATION 1 // trackball diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index 98aeb8700..2a2967f5e 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -9,8 +9,10 @@ #include "graphics/niche/InkHUD/InkHUD.h" // Applets +#include "graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h" #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -63,6 +65,7 @@ void setupNicheGraphics() inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Pick applets // Note: order of applets determines priority of "auto-show" feature @@ -74,6 +77,7 @@ void setupNicheGraphics() inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index 60d83b95a..26f4de565 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -7,6 +7,7 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Seeed Wio Tracker L1 E-Ink custom_meshtastic_images = wio_tracker_l1_eink.svg custom_meshtastic_tags = Seeed +custom_meshtastic_has_ink_hud = true board = seeed_wio_tracker_L1 extends = nrf52840_base @@ -34,7 +35,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_w lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip debug_tool = jlink [env:seeed_wio_tracker_L1_eink-inkhud] diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h index ae20f3c36..495c4ace8 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h @@ -23,13 +23,9 @@ #define PIN_LED1 (11) // LED P1.15 #define PIN_LED2 (12) // -#define LED_BUILTIN PIN_LED1 -#define LED_CONN PIN_LED2 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 -// #define LED_PIN PIN_LED2 -#define LED_STATE_ON 1 // State when LED is litted +#define LED_STATE_ON 1 // State when LED is lit // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -177,7 +173,6 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define TB_PRESS 29 #define TB_DIRECTION FALLING -#define CANNED_MESSAGE_MODULE_ENABLE 1 #define CANNED_MESSAGE_ADD_CONFIRMATION 1 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 68be47622..7018d054e 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -13,21 +13,23 @@ extends = nrf52840_base board = xiao_ble_sense board_level = pr build_flags = ${nrf52840_base.build_flags} - -Ivariants/nrf52840/seeed_xiao_nrf52840_kit - -Isrc/platform/nrf52/softdevice - -Isrc/platform/nrf52/softdevice/nrf52 + -I variants/nrf52840/seeed_xiao_nrf52840_kit + -I src/platform/nrf52/softdevice + -I src/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT - -DGPS_L76K + -DSEEED_XIAO_NRF_KIT_DEFAULT + -DCONFIG_NFCT_PINS_AS_GPIOS=1 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink -; Seeed Xiao BLE but with GPS undefined, and therefore i2c active +; Seeed Xiao BLE but with GPS moved to NFC pins, and therefore i2c active [env:seeed_xiao_nrf52840_kit_i2c] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -DSEEED_XIAO_NRF52840_KIT -build_unflags = -DGPS_L76K + -DSEEED_XIAO_NRF_KIT_I2C ; Define I2C variant + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h index 0844595da..4dc28557d 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h @@ -17,6 +17,37 @@ extern "C" { #endif // __cplusplus +/* +Xiao pin assignments + +| Pin | Default | I2C | BTB | BLE-L | | Pin | Default | I2C | BTB | BLE-L | +| ----- | -------- | ---- | ---- | ----- | --- | ----- | ------- | ---- | ---- | ----- | +| | | | | | | | | | | | +| D0 | G_STBY | UBTN | DIO1 | CS | | 5v | | | | | +| D1 | DIO1 | DIO1 | Busy | DIO1 | | GND | | | | | +| D2 | NRST | NRST | NRST | Busy | | 3v3 | | | | | +| D3 | Busy | Busy | CS | NRST | | D10 | MOSI | MOSI | MOSI | MOSI | +| D4 | CS | CS | RXEN | SDA | | D9 | MISO | MISO | MISO | MISO | +| D5 | RXEN | RXEN | | SCL | | D8 | SCK | SCK | SCK | SCK | +| D6 | G_TX | SDA | G_TX | | | D7 | G_RX | SCL | G_RX | RXEN | +| | | | | | | | | | | | +| | End | | | | | | | | | | +| NFC1/ | SDA | G_TX | SDA | G_TX | | NFC2/ | SCL | G_RX | SCL | G_RX | +| D30 | | | | | | D31 | | | | | +| | | | | | | | | | | | +| | Internal | | | | | | | | | | +| D16 | SCL1 | SCL1 | SCL1 | SCL1 | | | | | | | +| D17 | SDA1 | SDA1 | SDA1 | SDA1 | | | | | | | + +The default column shows the pin assignments for the Wio-SX1262 for XIAO +(standalone SKU 113010003 or nRF52840 kit SKU 102010710). +The I2C column shows an alternative pin assignment using I2C on D6/D7 in place of the GNSS. +The BTB column shows the pin assignment for the Wio-SX1262 -30-pin board-to-board connector version from the ESP32S3 kit. +The BLE-L column shows the pin assignment for the original DIY xiao_ble, and which is retained for legacy users. +Note that the in addition to the difference between the default and the I2C pinouts in placing the pins on NFC or +D6/D7, the user button is activated on D0. The button conflicts with the official GNSS module, so caution is advised. +*/ + #define PINS_COUNT (33) #define NUM_DIGITAL_PINS (33) #define NUM_ANALOG_INPUTS (8) @@ -65,15 +96,10 @@ static const uint8_t A5 = PIN_A5; #define LED_GREEN (13) #define LED_BLUE (12) -#define PIN_LED1 LED_GREEN // PIN_LED1 is used in src/platform/nrf52/architecture.h to define LED_PIN +#define PIN_LED1 LED_GREEN // PIN_LED1 is used in src/platform/nrf52/architecture.h to define LED_POWER #define PIN_LED2 LED_BLUE #define PIN_LED3 LED_RED -#define LED_BUILTIN LED_RED // LED_BUILTIN is used by framework-arduinoadafruitnrf52 to indicate flash writes - -#define LED_PWR LED_RED -#define USER_LED LED_BLUE - /* * Buttons */ @@ -96,15 +122,15 @@ static const uint8_t A5 = PIN_A5; */ #define USE_SX1262 -#ifdef XIAO_BLE_LEGACY_PINOUT +#if defined(XIAO_BLE_LEGACY_PINOUT) // Legacy xiao_ble variant pinout for third-party SX126x modules e.g. EBYTE E22 #define SX126X_CS D0 #define SX126X_DIO1 D1 #define SX126X_BUSY D2 #define SX126X_RESET D3 #define SX126X_RXEN D7 - -#elif defined(SEEED_XIAO_WIO_BTB) +#else +#if defined(SEEED_XIAO_NRF_WIO_BTB) // Wio-SX1262 for XIAO with 30-pin board-to-board connector // https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Schematic_Diagram_Wio-SX1262_for_XIAO.pdf #define SX126X_CS D3 @@ -114,13 +140,15 @@ static const uint8_t A5 = PIN_A5; #define SX126X_RXEN D4 #else // Wio-SX1262 for XIAO (standalone SKU 113010003 or nRF52840 kit SKU 102010710) +// Same for both default and I2C pinouts // https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Wio-SX1262%20for%20XIAO%20V1.0_SCH.pdf #define SX126X_CS D4 #define SX126X_DIO1 D1 #define SX126X_BUSY D3 #define SX126X_RESET D2 #define SX126X_RXEN D5 -#endif +#endif // defined(SEEED_XIAO_NRF_WIO_BTB) +#endif // defined(XIAO_BLE_LEGACY_PINOUT) // Common pinouts for all SX126x pinouts above #define SX126X_TXEN RADIOLIB_NC @@ -146,18 +174,26 @@ static const uint8_t SCK = PIN_SPI_SCK; * GPS */ // GPS L76K -#ifdef GPS_L76K + +// Default GPS L76K +#if defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) +#define GPS_L76K #define GPS_TX_PIN D6 // This is data from the MCU #define GPS_RX_PIN D7 // This is data from the GNSS module +#if defined(SEEED_XIAO_NRF_KIT_DEFAULT) +#define PIN_GPS_STANDBY D0 // this is where the conflicting pinouts come from +#endif +// I2C and BLE-Legacy put them on the NFC pins +#else +#define GPS_TX_PIN (30) +#define GPS_RX_PIN (31) +#endif + #define HAS_GPS 1 +#define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_TX GPS_TX_PIN #define PIN_SERIAL1_RX GPS_RX_PIN -#define PIN_GPS_STANDBY D0 -#else -#define PIN_SERIAL1_RX (-1) -#define PIN_SERIAL1_TX (-1) -#endif /* * Battery @@ -176,39 +212,60 @@ static const uint8_t SCK = PIN_SPI_SCK; * Wire Interfaces * Keep this section after potentially conflicting pin definitions */ -#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much -#define WIRE_INTERFACES_COUNT 1 +#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much +#define WIRE_INTERFACES_COUNT 1 // changed to 1 for now, as LSM6DS3TR has issues. #if defined(XIAO_BLE_LEGACY_PINOUT) // Used for I2C by DIY xiao_ble variant #define PIN_WIRE_SDA D4 #define PIN_WIRE_SCL D5 -#elif !defined(GPS_L76K) -// If D6 and D7 are free, I2C is probably the most versatile assignment +#else +// Put the I2C pins on the NFC pins by default +#if defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) +#define PIN_WIRE_SDA 30 +#define PIN_WIRE_SCL 31 +#else +// If not on legacy or defauly, we're wanting I2C on the back pins #define PIN_WIRE_SDA D6 #define PIN_WIRE_SCL D7 -#else -// Internal LSM6DS3TR on XIAO nRF52840 Series -#define PIN_WIRE_SDA (17) -#define PIN_WIRE_SCL (16) -#endif +#endif // defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) +#endif // defined(XIAO_BLE_LEGACY_PINOUT) -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; +// // Internal LSM6DS3TR on XIAO nRF52840 Series - put it on wire1 +// // Note: disabled for now, as there are some issues with the LSM. +// #define PIN_WIRE1_SDA (17) +// #define PIN_WIRE1_SCL (16) + +static const uint8_t SDA = PIN_WIRE_SDA; // Not sure if this is needed +static const uint8_t SCL = PIN_WIRE_SCL; // Not sure if this is needed + +// // QSPI Pins +// // --------- +// #define PIN_QSPI_SCK (24) +// #define PIN_QSPI_CS (25) +// #define PIN_QSPI_IO0 (26) +// #define PIN_QSPI_IO1 (27) +// #define PIN_QSPI_IO2 (28) +// #define PIN_QSPI_IO3 (29) + +// // On-board QSPI Flash +// // ------------------- +// #define EXTERNAL_FLASH_DEVICES P25Q16H +// #define EXTERNAL_FLASH_USE_QSPI /* * Buttons * Keep this section after potentially conflicting pin definitions * because D0 has multiple possible conflicts with various XIAO modules: - * - PIN_GPS_STANDBY on the L76K GNSS Module - * - DIO1 on the Wio-SX1262 - 30-pin board-to-board connector version - * - SX1262X CS on XIAO BLE legacy pinout */ - -#if !defined(GPS_L76K) && !defined(SEEED_XIAO_WIO_BTB) && !defined(XIAO_BLE_LEGACY_PINOUT) +#if defined(SEEED_XIAO_NRF_KIT_I2C) #define BUTTON_PIN D0 #endif +#if defined(SEEED_XIAO_NRF_WIO_BTB) +#define BUTTON_PIN D5 +#endif + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/t-echo-lite/platformio.ini b/variants/nrf52840/t-echo-lite/platformio.ini index c873dea37..1b725815e 100644 --- a/variants/nrf52840/t-echo-lite/platformio.ini +++ b/variants/nrf52840/t-echo-lite/platformio.ini @@ -30,5 +30,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo- lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip ;upload_protocol = fs diff --git a/variants/nrf52840/t-echo-lite/variant.h b/variants/nrf52840/t-echo-lite/variant.h index 0748f6d48..54c7bdfb5 100644 --- a/variants/nrf52840/t-echo-lite/variant.h +++ b/variants/nrf52840/t-echo-lite/variant.h @@ -51,9 +51,6 @@ extern "C" { #define LED_GREEN PIN_LED1 #define BLE_LED LED_BLUE -#define BLE_LED_INVERTED 1 -#define LED_BUILTIN LED_GREEN -#define LED_CONN LED_GREEN #define LED_STATE_ON 0 // State when LED is lit // Buttons @@ -171,6 +168,8 @@ static const uint8_t A0 = PIN_A0; #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) +#define SERIAL_PRINT_PORT 0 + // #define NO_EXT_GPIO 1 // PINs back side // Batt & solar connector left up corner diff --git a/variants/nrf52840/t-echo-plus/nicheGraphics.h b/variants/nrf52840/t-echo-plus/nicheGraphics.h index 483e16ea4..73067d7a7 100644 --- a/variants/nrf52840/t-echo-plus/nicheGraphics.h +++ b/variants/nrf52840/t-echo-plus/nicheGraphics.h @@ -8,6 +8,7 @@ #include "graphics/niche/Drivers/EInk/GDEY0154D67.h" #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -43,6 +44,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); diff --git a/variants/nrf52840/t-echo-plus/platformio.ini b/variants/nrf52840/t-echo-plus/platformio.ini index b77d54748..313eceadc 100644 --- a/variants/nrf52840/t-echo-plus/platformio.ini +++ b/variants/nrf52840/t-echo-plus/platformio.ini @@ -1,4 +1,15 @@ [env:t-echo-plus] +custom_meshtastic_hw_model = 33 +custom_meshtastic_hw_model_slug = T_ECHO_PLUS +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Echo Plus +custom_meshtastic_images = t-echo_plus.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_has_ink_hud = true + extends = nrf52840_base board = t-echo board_level = pr @@ -22,5 +33,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo- lib_deps = ${nrf52840_base.lib_deps} https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library + lewisxhe/PCF8563_Library@1.0.1 + # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 diff --git a/variants/nrf52840/t-echo-plus/variant.h b/variants/nrf52840/t-echo-plus/variant.h index 226f6d6fe..7ebdf48c0 100644 --- a/variants/nrf52840/t-echo-plus/variant.h +++ b/variants/nrf52840/t-echo-plus/variant.h @@ -25,9 +25,6 @@ extern "C" { #define LED_BLUE PIN_LED1 #define LED_GREEN PIN_LED2 -#define LED_BUILTIN LED_BLUE -#define LED_CONN LED_GREEN - #define LED_STATE_ON 0 // Buttons / touch @@ -135,8 +132,7 @@ static const uint8_t A0 = PIN_A0; #define HAS_DRV2605 1 -// Battery / ADC already defined above -#define HAS_RTC 1 +#define SERIAL_PRINT_PORT 0 #ifdef __cplusplus } diff --git a/variants/nrf52840/t-echo/nicheGraphics.h b/variants/nrf52840/t-echo/nicheGraphics.h index c89d816b9..c0b24dea7 100644 --- a/variants/nrf52840/t-echo/nicheGraphics.h +++ b/variants/nrf52840/t-echo/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -79,6 +80,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 @@ -123,4 +125,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index 4acd70b02..58ad029ae 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -8,6 +8,7 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Echo custom_meshtastic_images = t-echo.svg custom_meshtastic_tags = LilyGo +custom_meshtastic_has_ink_hud = true extends = nrf52840_base board = t-echo @@ -30,7 +31,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo> lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master - https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ;upload_protocol = fs diff --git a/variants/nrf52840/t-echo/variant.h b/variants/nrf52840/t-echo/variant.h index 9244fc6c3..f4644c6de 100644 --- a/variants/nrf52840/t-echo/variant.h +++ b/variants/nrf52840/t-echo/variant.h @@ -52,9 +52,6 @@ extern "C" { #define LED_BLUE PIN_LED1 #define LED_GREEN PIN_LED2 -#define LED_BUILTIN LED_BLUE -#define LED_CONN PIN_GREEN - #define LED_STATE_ON 0 // State when LED is lit /* @@ -88,6 +85,7 @@ static const uint8_t A0 = PIN_A0; /* * Serial interfaces */ +#define SERIAL_PRINT_PORT 0 /* No longer populated on PCB @@ -163,7 +161,6 @@ External serial flash WP25R1635FZUIL0 // Controls power for all peripherals (eink + GPS + LoRa + Sensor) #define PIN_POWER_EN (0 + 12) -// #define PIN_POWER_EN1 (0 + 13) #define PIN_SPI1_MISO \ (32 + 7) // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO @@ -191,7 +188,6 @@ External serial flash WP25R1635FZUIL0 // PCF8563 RTC Module #define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC #define PCF8563_RTC 0x51 -#define HAS_RTC 1 /* * SPI Interfaces diff --git a/variants/nrf52840/tracker-t1000-e/variant.cpp b/variants/nrf52840/tracker-t1000-e/variant.cpp index 8096705d0..0f7c5adaa 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.cpp +++ b/variants/nrf52840/tracker-t1000-e/variant.cpp @@ -32,16 +32,15 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); pinMode(PIN_3V3_ACC_EN, OUTPUT); digitalWrite(PIN_3V3_ACC_EN, HIGH); + pinMode(T1000X_SENSOR_EN_PIN, OUTPUT); + digitalWrite(T1000X_SENSOR_EN_PIN, HIGH); + pinMode(BUZZER_EN_PIN, OUTPUT); digitalWrite(BUZZER_EN_PIN, HIGH); diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h index 5b6719e12..de6916ac7 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.h +++ b/variants/nrf52840/tracker-t1000-e/variant.h @@ -47,8 +47,7 @@ extern "C" { #define PIN_3V3_ACC_EN (32 + 7) // P1.7, Power to Acc #define PIN_LED1 (0 + 24) // P0.24 -#define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 // Actually green #define LED_STATE_ON 1 // State when LED is lit @@ -127,7 +126,7 @@ extern "C" { #define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC #define BATTERY_IMMUTABLE #define ADC_MULTIPLIER (2.0F) -// P0.04/AIN2 is VCC_ADC, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE +// P0.04 is sensor power enable, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE #define EXT_CHRG_DETECT (32 + 3) // P1.03 #define EXT_CHRG_DETECT_VALUE LOW @@ -149,12 +148,21 @@ extern "C" { #define PIN_BUZZER (0 + 25) // P0.25, pwm output #define T1000X_SENSOR_EN -#define T1000X_VCC_PIN (0 + 4) // P0.4 -#define T1000X_NTC_PIN (0 + 31) // P0.31/AIN7 -#define T1000X_LUX_PIN (0 + 29) // P0.29/AIN5 +#define T1000X_SENSOR_EN_PIN (0 + 4) // P0.4, Power to Sensor (GPIO, not ADC) +#define T1000X_NTC_PIN (0 + 31) // P0.31/AIN7 +#define T1000X_LUX_PIN (0 + 29) // P0.29/AIN5 #define HAS_SCREEN 0 +// Enable Traffic Management Module for testing on T1000-E +// NRF52840 has 256KB RAM - 1024 entries uses ~10KB +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 1 +#endif +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1024 +#endif + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/wio-sdk-wm1110/platformio.ini b/variants/nrf52840/wio-sdk-wm1110/platformio.ini index 7c11ef6f6..9fac82289 100644 --- a/variants/nrf52840/wio-sdk-wm1110/platformio.ini +++ b/variants/nrf52840/wio-sdk-wm1110/platformio.ini @@ -9,6 +9,7 @@ extra_scripts = # Remove adafruit USB serial from the build (it is incompatible with using the ch340 serial chip on this board) build_unflags = + ${nrf52840_base.build_unflags} -Ofast -Og -ggdb3 diff --git a/variants/nrf52840/wio-sdk-wm1110/variant.h b/variants/nrf52840/wio-sdk-wm1110/variant.h index b6e5c79df..d802d20f6 100644 --- a/variants/nrf52840/wio-sdk-wm1110/variant.h +++ b/variants/nrf52840/wio-sdk-wm1110/variant.h @@ -58,8 +58,6 @@ extern "C" { #define PIN_LED1 (0 + 13) // P0.13 #define PIN_LED2 (0 + 14) // P0.14 -#define LED_BUILTIN PIN_LED1 - #define LED_GREEN PIN_LED1 #define LED_BLUE PIN_LED2 // Actually red diff --git a/variants/nrf52840/wio-t1000-s/variant.cpp b/variants/nrf52840/wio-t1000-s/variant.cpp index 85e0c44f3..54d338a19 100644 --- a/variants/nrf52840/wio-t1000-s/variant.cpp +++ b/variants/nrf52840/wio-t1000-s/variant.cpp @@ -32,16 +32,15 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); pinMode(PIN_3V3_ACC_EN, OUTPUT); digitalWrite(PIN_3V3_ACC_EN, LOW); + pinMode(T1000X_SENSOR_EN_PIN, OUTPUT); + digitalWrite(T1000X_SENSOR_EN_PIN, HIGH); + pinMode(BUZZER_EN_PIN, OUTPUT); digitalWrite(BUZZER_EN_PIN, HIGH); diff --git a/variants/nrf52840/wio-t1000-s/variant.h b/variants/nrf52840/wio-t1000-s/variant.h index 02f8a20b2..c9b98ab87 100644 --- a/variants/nrf52840/wio-t1000-s/variant.h +++ b/variants/nrf52840/wio-t1000-s/variant.h @@ -47,8 +47,7 @@ extern "C" { #define PIN_3V3_ACC_EN (32 + 7) // P1.7, Power to Acc #define PIN_LED1 (0 + 24) // P0.24 -#define LED_PIN PIN_LED1 -#define LED_BUILTIN -1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 // Actually green #define LED_STATE_ON 1 // State when LED is lit @@ -144,9 +143,9 @@ extern "C" { #define PIN_BUZZER (0 + 25) // P0.25, pwm output #define T1000X_SENSOR_EN -#define T1000X_VCC_PIN (0 + 4) // P0.4 -#define T1000X_NTC_PIN (0 + 31) // P0.31 -#define T1000X_LUX_PIN (0 + 29) // P0.29 +#define T1000X_SENSOR_EN_PIN (0 + 4) // P0.4, Power to Sensor (GPIO, not ADC) +#define T1000X_NTC_PIN (0 + 31) // P0.31 +#define T1000X_LUX_PIN (0 + 29) // P0.29 #ifdef __cplusplus } diff --git a/variants/nrf52840/wio-tracker-wm1110/variant.cpp b/variants/nrf52840/wio-tracker-wm1110/variant.cpp index 5a3587982..0d0856773 100644 --- a/variants/nrf52840/wio-tracker-wm1110/variant.cpp +++ b/variants/nrf52840/wio-tracker-wm1110/variant.cpp @@ -36,9 +36,6 @@ void initVariant() pinMode(PIN_LED1, OUTPUT); ledOff(PIN_LED1); - pinMode(PIN_LED2, OUTPUT); - ledOff(PIN_LED2); - // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/wio-tracker-wm1110/variant.h b/variants/nrf52840/wio-tracker-wm1110/variant.h index 807ca8dbb..647bd47d8 100644 --- a/variants/nrf52840/wio-tracker-wm1110/variant.h +++ b/variants/nrf52840/wio-tracker-wm1110/variant.h @@ -51,13 +51,8 @@ extern "C" { #define PIN_WIRE_SDA (0 + 5) // P0.05 #define PIN_WIRE_SCL (0 + 4) // P0.04 -#define PIN_LED1 (0 + 6) // P0.06 -#define PIN_LED2 (PINS_COUNT) // P0.14 - -#define LED_BUILTIN PIN_LED1 - +#define PIN_LED1 (0 + 6) // P0.06 #define LED_GREEN PIN_LED1 -#define LED_BLUE PIN_LED2 #define LED_STATE_ON 0 diff --git a/variants/rp2040/challenger_2040_lora/pins_arduino.h b/variants/rp2040/challenger_2040_lora/pins_arduino.h index ac472c07e..ee86e1a34 100644 --- a/variants/rp2040/challenger_2040_lora/pins_arduino.h +++ b/variants/rp2040/challenger_2040_lora/pins_arduino.h @@ -7,7 +7,7 @@ #define ADC_RESOLUTION (12u) // LEDs -#define PIN_LED (24u) +#define LED_POWER (24u) // Serial #define PIN_SERIAL1_TX (16u) @@ -45,8 +45,6 @@ #define SPI_HOWMANY (2u) #define WIRE_HOWMANY (1u) -#define LED_BUILTIN PIN_LED - static const uint8_t D0 = (16u); static const uint8_t D1 = (17u); static const uint8_t D2 = (20u); diff --git a/variants/rp2040/challenger_2040_lora/variant.h b/variants/rp2040/challenger_2040_lora/variant.h index 552f90720..f5126cfff 100644 --- a/variants/rp2040/challenger_2040_lora/variant.h +++ b/variants/rp2040/challenger_2040_lora/variant.h @@ -5,8 +5,6 @@ #define EXT_NOTIFY_OUT 0xFFFFFFFF #define BUTTON_PIN 0xFFFFFFFF -#define LED_PIN PIN_LED - #define USE_RF95 // RFM95/SX127x #undef LORA_SCK diff --git a/variants/rp2040/ec_catsniffer/variant.h b/variants/rp2040/ec_catsniffer/variant.h index 400074e59..7df69f134 100644 --- a/variants/rp2040/ec_catsniffer/variant.h +++ b/variants/rp2040/ec_catsniffer/variant.h @@ -9,7 +9,7 @@ #undef GPS_RX_PIN #undef GPS_TX_PIN -#define LED_PIN 27 +#define LED_POWER 27 #define USE_SX1262 diff --git a/variants/rp2040/feather_rp2040_rfm95/variant.h b/variants/rp2040/feather_rp2040_rfm95/variant.h index e9e178202..efaced7b4 100644 --- a/variants/rp2040/feather_rp2040_rfm95/variant.h +++ b/variants/rp2040/feather_rp2040_rfm95/variant.h @@ -20,7 +20,7 @@ #define BUTTON_PIN 7 // #define BUTTON_NEED_PULLUP -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED // #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/nibble_rp2040/variant.h b/variants/rp2040/nibble_rp2040/variant.h index 0f71b98e9..3b1dfcd7b 100644 --- a/variants/rp2040/nibble_rp2040/variant.h +++ b/variants/rp2040/nibble_rp2040/variant.h @@ -2,7 +2,7 @@ #define BUTTON_PIN -1 // Pin 17 used for antenna switching via DIO4 -#define LED_PIN 1 +#define LED_POWER 1 #define HAS_CPU_SHUTDOWN 1 diff --git a/variants/rp2040/rak11310/pins_arduino.h b/variants/rp2040/rak11310/pins_arduino.h index 0e2808b19..59290bbdb 100644 --- a/variants/rp2040/rak11310/pins_arduino.h +++ b/variants/rp2040/rak11310/pins_arduino.h @@ -23,10 +23,8 @@ static const uint8_t A2 = PIN_A2; static const uint8_t A3 = PIN_A3; // LEDs -#define PIN_LED (23u) -#define PIN_LED1 PIN_LED -#define PIN_LED2 (24u) -#define LED_BUILTIN PIN_LED +#define PIN_LED1 (23u) +#define LED_NOTIFICATION (24u) #define ADC_RESOLUTION 12 diff --git a/variants/rp2040/rak11310/variant.h b/variants/rp2040/rak11310/variant.h index 2400d56a7..4dfad060e 100644 --- a/variants/rp2040/rak11310/variant.h +++ b/variants/rp2040/rak11310/variant.h @@ -10,8 +10,7 @@ #define I2C_SDA1 2 #define I2C_SCL1 3 -#define LED_CONN PIN_LED2 -#define LED_PIN LED_BUILTIN +#define LED_POWER PIN_LED1 #define ledOff(pin) pinMode(pin, INPUT) #define BUTTON_PIN 9 diff --git a/variants/rp2040/rp2040-lora/variant.h b/variants/rp2040/rp2040-lora/variant.h index 92b067457..51a760e0b 100644 --- a/variants/rp2040/rp2040-lora/variant.h +++ b/variants/rp2040/rp2040-lora/variant.h @@ -17,7 +17,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN -1 // Pin 17 used for antenna switching via DIO4 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED // #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/rpipico-slowclock/variant.h b/variants/rp2040/rpipico-slowclock/variant.h index fb97ec0fb..40d20e17a 100644 --- a/variants/rp2040/rpipico-slowclock/variant.h +++ b/variants/rp2040/rpipico-slowclock/variant.h @@ -52,7 +52,7 @@ #define BUTTON_PIN 18 #define EXT_NOTIFY_OUT 22 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/rpipico/variant.h b/variants/rp2040/rpipico/variant.h index 7efaeaf7a..dd849c290 100644 --- a/variants/rp2040/rpipico/variant.h +++ b/variants/rp2040/rpipico/variant.h @@ -15,7 +15,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/rpipicow/variant.h b/variants/rp2040/rpipicow/variant.h index 24da8f932..2de00545e 100644 --- a/variants/rp2040/rpipicow/variant.h +++ b/variants/rp2040/rpipicow/variant.h @@ -19,7 +19,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 -#define LED_PIN LED_BUILTIN +#define LED_POWER PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/senselora_rp2040/pins_arduino.h b/variants/rp2040/senselora_rp2040/pins_arduino.h index bb0ee637e..575839cbc 100644 --- a/variants/rp2040/senselora_rp2040/pins_arduino.h +++ b/variants/rp2040/senselora_rp2040/pins_arduino.h @@ -11,9 +11,7 @@ static const uint8_t A2 = PIN_A2; static const uint8_t A3 = PIN_A3; // LEDs -#define PIN_LED (23u) -#define PIN_LED1 PIN_LED -#define LED_BUILTIN PIN_LED +#define PIN_LED1 (23u) #define ADC_RESOLUTION 12 diff --git a/variants/rp2040/senselora_rp2040/variant.h b/variants/rp2040/senselora_rp2040/variant.h index cc90284b7..f79ed66ca 100644 --- a/variants/rp2040/senselora_rp2040/variant.h +++ b/variants/rp2040/senselora_rp2040/variant.h @@ -5,7 +5,7 @@ #define BUTTON_PIN 2 #define BUTTON_NEED_PULLUP -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED1 #define ledOff(pin) pinMode(pin, INPUT) #undef BATTERY_PIN diff --git a/variants/rp2350/rpipico2/variant.h b/variants/rp2350/rpipico2/variant.h index 7efaeaf7a..dd849c290 100644 --- a/variants/rp2350/rpipico2/variant.h +++ b/variants/rp2350/rpipico2/variant.h @@ -15,7 +15,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/stm32/CDEBYTE_E77-MBL/variant.h b/variants/stm32/CDEBYTE_E77-MBL/variant.h index e3d111a33..686326137 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/variant.h +++ b/variants/stm32/CDEBYTE_E77-MBL/variant.h @@ -15,9 +15,11 @@ Do not expect a working Meshtastic device with this target. #define USE_STM32WLx -#define LED_PIN PB4 // LED1 -// #define LED_PIN PB3 // LED2 +#define LED_POWER PB4 // LED1 +// #define LED_POWER PB3 // LED2 #define LED_STATE_ON 1 +#define SERIAL_PRINT_PORT 1 + #define EBYTE_E77_MBL #endif diff --git a/variants/stm32/milesight_gs301/platformio.ini b/variants/stm32/milesight_gs301/platformio.ini new file mode 100644 index 000000000..73b9cf7ea --- /dev/null +++ b/variants/stm32/milesight_gs301/platformio.ini @@ -0,0 +1,24 @@ +; Milesight GS301 Bathroom Odor Detector +; https://www.milesight.com/iot/product/lorawan-sensor/gs301 +[env:milesight_gs301] +extends = stm32_base +board = wiscore_rak3172 ; Convenient choice as the same USART is used for programming/debug +board_level = extra +board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem +build_flags = + ${stm32_base.build_flags} + -Ivariants/stm32/milesight_gs301 + -DPRIVATE_HW + -DMESHTASTIC_EXCLUDE_GPS=1 + -DMESHTASTIC_EXCLUDE_I2C=1 # Analog ADuCM355 (unsupported) so no point building support for I2C in + -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 +build_unflags = + -DDEBUG_MUTE # We have space for debug output until sensor support is added +build_src_filter = + ${stm32_base.build_src_filter} + +<../variants/stm32/milesight_gs301> +lib_deps = + ${stm32_base.lib_deps} + +upload_port = stlink diff --git a/variants/stm32/milesight_gs301/rfswitch.h b/variants/stm32/milesight_gs301/rfswitch.h new file mode 100644 index 000000000..d9f60038a --- /dev/null +++ b/variants/stm32/milesight_gs301/rfswitch.h @@ -0,0 +1,7 @@ +// Seems to use the same RF switch pins as RAK3172… getting Tx/Rx SNR +11dB with a nearby node +// PB8, PC13 + +static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PB8, PC13, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[4] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE}; diff --git a/variants/stm32/milesight_gs301/variant.cpp b/variants/stm32/milesight_gs301/variant.cpp new file mode 100644 index 000000000..ef90d5a54 --- /dev/null +++ b/variants/stm32/milesight_gs301/variant.cpp @@ -0,0 +1,8 @@ +#include "variant.h" +#include "Arduino.h" + +void earlyInitVariant() +{ + pinMode(USER_LED, OUTPUT); + digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); +} \ No newline at end of file diff --git a/variants/stm32/milesight_gs301/variant.h b/variants/stm32/milesight_gs301/variant.h new file mode 100644 index 000000000..f68d70458 --- /dev/null +++ b/variants/stm32/milesight_gs301/variant.h @@ -0,0 +1,41 @@ +#ifndef _VARIANT_MILESIGHT_GS301_ +#define _VARIANT_MILESIGHT_GS301_ + +#define USE_STM32WLx + +// I/O +#define LED_STATE_ON 1 +#define PIN_LED1 PA0 // Green LED +#define LED_POWER PIN_LED1 +#define PIN_LED2 PA0 // Red LED +#define USER_LED PIN_LED2 +#define BUTTON_PIN PC13 +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP false +#define PIN_BUZZER PA6 + +// EC Sense DGM10 Double Gas Module +// Analog ADuCM355 (unsupported); SHTC3 is connected to ADuCM355 and not directly accessible +#define PIN_WIRE_SDA PB7 +#define PIN_WIRE_SCL PB8 +// Commented out to keep sensor powered down due to lack of support +/* +#define VEXT_ENABLE PB12 // TI TPS61291DRV VSEL - set LOW before ENable for Vout = 3.3V +#define VEXT_ON_VALUE LOW +#define SENSOR_POWER_CTRL_PIN PB2 // TI TPS61291DRV EN pin +#define SENSOR_POWER_ON HIGH +#define HAS_SENSOR 1 +*/ + +#define ENABLE_HWSERIAL1 +#define PIN_SERIAL1_RX NC +#define PIN_SERIAL1_TX PB6 + +// LoRa +#define SX126X_DIO3_TCXO_VOLTAGE 3.0 + +// Required to avoid Serial1 conflicts due to board definition here: +// https://github.com/stm32duino/Arduino_Core_STM32/blob/main/variants/STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U/variant_RAK3172_MODULE.h +#define RAK3172 + +#endif diff --git a/variants/stm32/rak3172/variant.h b/variants/stm32/rak3172/variant.h index 30d2b57b4..75e3e0c91 100644 --- a/variants/stm32/rak3172/variant.h +++ b/variants/stm32/rak3172/variant.h @@ -13,9 +13,15 @@ Do not expect a working Meshtastic device with this target. #define USE_STM32WLx -#define LED_PIN PA0 // Green LED +#define LED_POWER PA0 // Green LED #define LED_STATE_ON 1 +#define BATTERY_PIN AVBAT +// ADC_MULTIPLIER: 3.0 = internal 1:3 bridge divider (DS13105§3.18.3) +// Margin: 1.10 = AVBAT divider tolerance ±10% (Table 82) +#define ADC_MULTIPLIER (1.01f * 3) + #define RAK3172 +#define SERIAL_PRINT_PORT 1 #endif diff --git a/variants/stm32/russell/platformio.ini b/variants/stm32/russell/platformio.ini index 0dd57a2c7..73cf7f81a 100644 --- a/variants/stm32/russell/platformio.ini +++ b/variants/stm32/russell/platformio.ini @@ -13,9 +13,19 @@ build_flags = ${stm32_base.build_flags} -Ivariants/stm32/russell -DPRIVATE_HW + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 + -DMESHTASTIC_EXCLUDE_RANGETEST=1 + -DMESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 + -DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 + -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 + -DMESHTASTIC_EXCLUDE_NEIGHBORINFO=1 + -DMESHTASTIC_EXCLUDE_TRACEROUTE=1 + -DMESHTASTIC_EXCLUDE_WAYPOINT=1 lib_deps = ${stm32_base.lib_deps} # 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_BME680 packageName=adafruit/library/Adafruit BME680 Library + adafruit/Adafruit BME680 Library@2.0.6 upload_port = stlink diff --git a/variants/stm32/russell/variant.h b/variants/stm32/russell/variant.h index 796302d34..d36826c15 100644 --- a/variants/stm32/russell/variant.h +++ b/variants/stm32/russell/variant.h @@ -4,7 +4,7 @@ #define USE_STM32WLx // I/O -#define LED_PIN PA0 // Red LED +#define LED_POWER PA0 // Red LED #define LED_STATE_ON 1 #define BUTTON_PIN PH3 // Shared with BOOT0 #define BUTTON_NEED_PULLUP @@ -13,6 +13,16 @@ // #define EXT_CHRG_DETECT PA5 // #define EXT_PWR_DETECT PA4 +#define BATTERY_PIN AVBAT +// ADC_MULTIPLIER: 3.0 = internal 1:3 bridge divider (DS13105§3.18.3) +// Margin: 1.10 = AVBAT divider tolerance ±10% (Table 82) +#define ADC_MULTIPLIER (1.01f * 3) +/* +Sample OCV curve for Li-SOCl2 primary lithium cells (e.g. Saft cells have fresh OCV of 3.67V) +#define NUM_OCV_POINTS 11 +#define OCV_ARRAY 3670, 3650, 3630, 3610, 3590, 3560, 3530, 3480, 3400, 3200, 2500 +*/ + // Bosch Sensortec BME280 #define HAS_SENSOR 1 @@ -20,6 +30,11 @@ #define ENABLE_HWSERIAL1 #define PIN_SERIAL1_RX PB7 #define PIN_SERIAL1_TX PB6 + +// Debug serial (USART2) +#define ENABLE_HWSERIAL2 +#define PIN_SERIAL2_TX PA2 +#define PIN_SERIAL2_RX PA3 #define HAS_GPS 1 #define PIN_GPS_STANDBY PA15 #define GPS_RX_PIN PB7 diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index bb0a4d3ce..1efe18e3d 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -2,7 +2,7 @@ extends = arduino_base platform = # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 - platformio/ststm32@19.4.0 + platformio/ststm32@19.5.0 platform_packages = # renovate: datasource=github-tags depName=Arduino_Core_STM32 packageName=stm32duino/Arduino_Core_STM32 platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip @@ -27,7 +27,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small. -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported. - -DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized. + ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; Enable this if enabling debugg logging. It is REQUIRED for at least traceroute debug prints - without it the length returned by printf ends up uninitialized. -DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs. -fmerge-all-constants -ffunction-sections @@ -35,6 +35,8 @@ build_flags = -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 + -DRADIOLIB_EXCLUDE_LR2021=1 + -DMESHTASTIC_DYNAMIC_SBRK_HEAP -DHAL_DAC_MODULE_ONLY -DHAL_RNG_MODULE_ENABLED -Wl,--wrap=__assert_func @@ -56,3 +58,6 @@ lib_deps = lib_ignore = OneButton + +; Set a custom linker script with a higher MinStackSize value, to match NRF52. +board_build.ldscript = $PROJECT_DIR/variants/stm32/stm32wle5xx.ld \ No newline at end of file diff --git a/variants/stm32/stm32wle5xx.ld b/variants/stm32/stm32wle5xx.ld new file mode 100644 index 000000000..c13782926 --- /dev/null +++ b/variants/stm32/stm32wle5xx.ld @@ -0,0 +1,204 @@ +/* +****************************************************************************** +** +** File : LinkerScript.ld +** +** Author : STM32CubeIDE +** +** Abstract : Linker script for STM32WL55xC Device +** 256Kbytes FLASH +** 64Kbytes RAM +** +** Set heap size, stack size and stack location according +** to application requirements. +** +** Set memory bank area and size if external memory is used. +** +** Target : STMicroelectronics STM32 +** +** Distribution: The file is distributed as is without any warranty +** of any kind. +** +***************************************************************************** +** @attention +** +**

© Copyright (c) 2020 STMicroelectronics. +** All rights reserved.

+** +** This software component is licensed by ST under BSD 3-Clause license, +** the "License"; You may not use this file except in compliance with the +** License. You may obtain a copy of the License at: +** opensource.org/licenses/BSD-3-Clause +** +***************************************************************************** +*/ + +/* Entry Point */ +ENTRY(Reset_Handler) + +/* Highest address of the user mode stack */ +_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of "RAM" Ram type memory */ + +_Min_Heap_Size = 0x200 ; /* required amount of heap */ +/* Modified from original to 2KB, to match NRF52 */ +_Min_Stack_Size = 2048 ; /* required amount of stack */ + +/* Memories definition */ +MEMORY +{ + RAM (xrw) : ORIGIN = 0x20000000, LENGTH = LD_MAX_DATA_SIZE + FLASH (rx) : ORIGIN = 0x08000000 + LD_FLASH_OFFSET, LENGTH = LD_MAX_SIZE - LD_FLASH_OFFSET +} + +/* Sections */ +SECTIONS +{ + /* The startup code into "FLASH" Rom type memory */ + .isr_vector : + { + . = ALIGN(4); + KEEP(*(.isr_vector)) /* Startup code */ + . = ALIGN(4); + } >FLASH + + /* The program code and other data into "FLASH" Rom type memory */ + .text : + { + . = ALIGN(4); + *(.text) /* .text sections (code) */ + *(.text*) /* .text* sections (code) */ + *(.glue_7) /* glue arm to thumb code */ + *(.glue_7t) /* glue thumb to arm code */ + *(.eh_frame) + + KEEP (*(.init)) + KEEP (*(.fini)) + + . = ALIGN(4); + _etext = .; /* define a global symbols at end of code */ + } >FLASH + + /* Constant data into "FLASH" Rom type memory */ + .rodata : + { + . = ALIGN(4); + *(.rodata) /* .rodata sections (constants, strings, etc.) */ + *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ + . = ALIGN(4); + } >FLASH + + .ARM.extab (READONLY) : { + . = ALIGN(4); + *(.ARM.extab* .gnu.linkonce.armextab.*) + . = ALIGN(4); + } >FLASH + + .ARM (READONLY) : { + . = ALIGN(4); + __exidx_start = .; + *(.ARM.exidx*) + __exidx_end = .; + . = ALIGN(4); + } >FLASH + + .preinit_array (READONLY) : + { + . = ALIGN(4); + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP (*(.preinit_array*)) + PROVIDE_HIDDEN (__preinit_array_end = .); + . = ALIGN(4); + } >FLASH + + .init_array (READONLY) : + { + . = ALIGN(4); + PROVIDE_HIDDEN (__init_array_start = .); + KEEP (*(SORT(.init_array.*))) + KEEP (*(.init_array*)) + PROVIDE_HIDDEN (__init_array_end = .); + . = ALIGN(4); + } >FLASH + + .fini_array (READONLY) : + { + . = ALIGN(4); + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP (*(SORT(.fini_array.*))) + KEEP (*(.fini_array*)) + PROVIDE_HIDDEN (__fini_array_end = .); + . = ALIGN(4); + } >FLASH + + /* Used by the startup to initialize data */ + _sidata = LOADADDR(.data); + + /* Initialized data sections into "RAM" Ram type memory */ + .data : + { + . = ALIGN(4); + _sdata = .; /* create a global symbol at data start */ + *(.data) /* .data sections */ + *(.data*) /* .data* sections */ + *(.RamFunc) /* .RamFunc sections */ + *(.RamFunc*) /* .RamFunc* sections */ + + . = ALIGN(4); + _edata = .; /* define a global symbol at data end */ + + } >RAM AT> FLASH + + /* Uninitialized data section into "RAM" Ram type memory */ + . = ALIGN(4); + .bss : + { + /* This is used by the startup in order to initialize the .bss section */ + _sbss = .; /* define a global symbol at bss start */ + __bss_start__ = _sbss; + *(.bss) + *(.bss*) + *(COMMON) + + . = ALIGN(4); + _ebss = .; /* define a global symbol at bss end */ + __bss_end__ = _ebss; + } >RAM + + /* Define a noinit output section and mark it as NOLOAD to prevent + * putting its contents into the resulting .bin file (which is the + * default). */ + .noinit (NOLOAD) : + { + /* Ensure output is aligned */ + . = ALIGN(4); + /* Define a global _snoinit (and _enoinit below) symbol just in case + * code wants to iterate over all noinit variables for some reason */ + _snoinit = .; + /* Actually import the .noinit and .noinit* import sections */ + *(.noinit) + *(.noinit*) + . = ALIGN(4); + _enoinit = .; + } >RAM + + /* User_heap_stack section, used to check that there is enough "RAM" Ram type memory left */ + ._user_heap_stack : + { + . = ALIGN(8); + PROVIDE ( end = . ); + PROVIDE ( _end = . ); + . = . + _Min_Heap_Size; + . = . + _Min_Stack_Size; + . = ALIGN(8); + } >RAM + + /* Remove information from the compiler libraries */ + /DISCARD/ : + { + libc.a ( * ) + libm.a ( * ) + libgcc.a ( * ) + } + + .ARM.attributes 0 : { *(.ARM.attributes) } +} diff --git a/variants/stm32/wio-e5/platformio.ini b/variants/stm32/wio-e5/platformio.ini index 311cade58..c8dbb2b72 100644 --- a/variants/stm32/wio-e5/platformio.ini +++ b/variants/stm32/wio-e5/platformio.ini @@ -17,5 +17,6 @@ build_flags = -DPIN_SERIAL2_RX=PA3 -DHAS_GPS=1 -DGPS_SERIAL_PORT=Serial2 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 upload_port = stlink diff --git a/variants/stm32/wio-e5/variant.h b/variants/stm32/wio-e5/variant.h index a312b31bd..da2c623fb 100644 --- a/variants/stm32/wio-e5/variant.h +++ b/variants/stm32/wio-e5/variant.h @@ -14,14 +14,9 @@ Do not expect a working Meshtastic device with this target. #define USE_STM32WLx -#define LED_PIN PB5 +#define LED_POWER PB5 #define LED_STATE_ON 0 #define WIO_E5 -#if (defined(LED_BUILTIN) && LED_BUILTIN == PNUM_NOT_DEFINED) -#undef LED_BUILTIN -#define LED_BUILTIN (LED_PIN) -#endif - #endif diff --git a/version.properties b/version.properties index 62145da14..4ee342bb8 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 19 +build = 23