mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-04 11:28:11 +10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84355d15a |
+6
-6
@@ -4,19 +4,19 @@ cli:
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.7.6
|
||||
ref: v1.7.5
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.508
|
||||
- renovate@43.76.3
|
||||
- checkov@3.2.507
|
||||
- renovate@43.57.0
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.93.8
|
||||
- trufflehog@3.93.7
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.4
|
||||
- trivy@0.69.3
|
||||
- taplo@0.10.0
|
||||
- ruff@0.15.6
|
||||
- ruff@0.15.5
|
||||
- isort@8.0.1
|
||||
- markdownlint@0.48.0
|
||||
- oxipng@10.1.0
|
||||
@@ -26,7 +26,7 @@ lint:
|
||||
- hadolint@2.14.0
|
||||
- shfmt@3.6.0
|
||||
- shellcheck@0.11.0
|
||||
- black@26.3.1
|
||||
- black@26.1.0
|
||||
- git-diff-check
|
||||
- gitleaks@8.30.0
|
||||
- clang-format@16.0.3
|
||||
|
||||
@@ -13,4 +13,4 @@ Lora:
|
||||
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]
|
||||
TX_GAIN_LORA: [8]
|
||||
@@ -9,4 +9,4 @@ Lora:
|
||||
- 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]
|
||||
TX_GAIN_LORA: [8]
|
||||
@@ -12,4 +12,4 @@ Lora:
|
||||
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]
|
||||
TX_GAIN_LORA: [8]
|
||||
@@ -87,9 +87,6 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.21" date="2026-03-11">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.21</url>
|
||||
</release>
|
||||
<release version="2.7.20" date="2026-02-11">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url>
|
||||
</release>
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "nrf52840_s140_v7.ld"
|
||||
},
|
||||
"core": "nRF5",
|
||||
"cpu": "cortex-m4",
|
||||
"extra_flags": "-DARDUINO_TRACKER_T2000_C -DNRF52840_XXAA",
|
||||
"f_cpu": "64000000L",
|
||||
"hwids": [
|
||||
["0x239A", "0x8029"],
|
||||
["0x239A", "0x0029"],
|
||||
["0x239A", "0x002A"],
|
||||
["0x239A", "0x802A"],
|
||||
["0x2886", "0x0057"]
|
||||
],
|
||||
"usb_product": "T2000-BOOT",
|
||||
"mcu": "nrf52840",
|
||||
"variant": "Seeed_T2000-C",
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_flags": "-DS140",
|
||||
"sd_name": "s140",
|
||||
"sd_version": "7.3.0",
|
||||
"sd_fwid": "0x0123"
|
||||
},
|
||||
"bootloader": {
|
||||
"settings_addr": "0xFF000"
|
||||
}
|
||||
},
|
||||
"connectivity": ["bluetooth"],
|
||||
"debug": {
|
||||
"jlink_device": "nRF52840_xxAA",
|
||||
"svd_path": "nrf52840.svd",
|
||||
"openocd_target": "nrf52840-mdk-rs"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "Seeed T2000-C",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104,
|
||||
"speed": 115200,
|
||||
"protocol": "nrfutil",
|
||||
"protocols": [
|
||||
"jlink",
|
||||
"nrfjprog",
|
||||
"nrfutil",
|
||||
"stlink",
|
||||
"cmsis-dap",
|
||||
"blackmagic"
|
||||
],
|
||||
"use_1200bps_touch": true,
|
||||
"require_upload_port": true,
|
||||
"wait_for_upload_port": true
|
||||
},
|
||||
"url": "https://www.seeedstudio.com/",
|
||||
"vendor": "Seeed Studio"
|
||||
}
|
||||
Vendored
-6
@@ -1,9 +1,3 @@
|
||||
meshtasticd (2.7.21.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.21
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 11 Mar 2026 11:45:36 +0000
|
||||
|
||||
meshtasticd (2.7.20.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.20
|
||||
|
||||
Vendored
-4
@@ -10,10 +10,6 @@ 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
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
# BLE Mesh Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Add BLE as a mesh transport alongside LoRa, enabling Meshtastic nodes to relay packets over Bluetooth Low Energy. Follow the same handler pattern used by UDP multicast ([UdpMulticastHandler](src/mesh/udp/UdpMulticastHandler.h)) — a standalone handler class with `onSend()` hooked into `Router::send()` and received packets injected via `router->enqueueReceivedMessage()`. No changes to `RadioInterface` or Router architecture needed.
|
||||
|
||||
BLE mesh is useful for:
|
||||
|
||||
- Dense indoor deployments where LoRa range is excessive
|
||||
- Extending mesh reach through devices that lack LoRa hardware
|
||||
- Hybrid LoRa+BLE networks (gateway nodes bridge both transports)
|
||||
- Lower latency links between nearby nodes
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Pattern: Same as UDP Multicast
|
||||
|
||||
```
|
||||
Router::send()
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
iface->send(p) udpHandler->onSend(p) bleMeshHandler->onSend(p)
|
||||
(LoRa) (UDP) (BLE mesh)
|
||||
|
||||
Router::enqueueReceivedMessage()
|
||||
▲
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
LoRa RX ISR udpHandler->onReceive() bleMeshHandler->onReceive()
|
||||
```
|
||||
|
||||
The existing UDP pattern ([Router.cpp:378-382](src/mesh/Router.cpp#L378-L382)):
|
||||
|
||||
```cpp
|
||||
#if HAS_UDP_MULTICAST
|
||||
if (udpHandler && config.network.enabled_protocols & ...) {
|
||||
udpHandler->onSend(const_cast<meshtastic_MeshPacket *>(p));
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
BLE mesh hooks in identically, right next to it.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Protobuf & Configuration
|
||||
|
||||
### 1.1 Transport mechanism enum
|
||||
|
||||
**File:** protobufs repo (`mesh.proto`)
|
||||
|
||||
Add a new transport type alongside the existing ones:
|
||||
|
||||
```protobuf
|
||||
enum TransportMechanism {
|
||||
// ... existing 0-7 ...
|
||||
TRANSPORT_BLE_MESH = 8;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 BLE Mesh configuration
|
||||
|
||||
Add BLE mesh config to the device config protobuf:
|
||||
|
||||
```protobuf
|
||||
message BleMeshConfig {
|
||||
bool enabled = 1;
|
||||
|
||||
// Operating mode
|
||||
enum Mode {
|
||||
OFF = 0;
|
||||
ADVERTISE_AND_SCAN = 1; // connectionless (extended adv)
|
||||
GATT = 2; // connection-based
|
||||
HYBRID = 3; // both
|
||||
}
|
||||
Mode mode = 2;
|
||||
|
||||
// Scan/advertising interval in ms (latency vs power tradeoff)
|
||||
uint32_t interval_ms = 3; // default: 500ms
|
||||
|
||||
// TX power level (platform-mapped)
|
||||
int8_t tx_power = 4; // dBm
|
||||
|
||||
// Max BLE mesh peers to track
|
||||
uint32_t max_peers = 5; // default: 8
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: BLE Mesh Handler — Core Class
|
||||
|
||||
### 2.1 Base handler (platform-agnostic)
|
||||
|
||||
**New file:** `src/mesh/BLEMeshHandler.h`
|
||||
|
||||
Follow the `UdpMulticastHandler` pattern — a simple class, not a `RadioInterface`:
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH
|
||||
|
||||
#include "configuration.h"
|
||||
#include "mesh/Router.h"
|
||||
|
||||
class BLEMeshHandler {
|
||||
public:
|
||||
BLEMeshHandler() : isRunning(false) {}
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
|
||||
// Called from Router::send() — broadcast packet over BLE
|
||||
virtual bool onSend(const meshtastic_MeshPacket *mp) = 0;
|
||||
|
||||
protected:
|
||||
bool isRunning;
|
||||
|
||||
// Shared RX path: decode and enqueue into router
|
||||
void deliverToRouter(const uint8_t *data, size_t len) {
|
||||
meshtastic_MeshPacket mp;
|
||||
if (!pb_decode_from_bytes(data, len, &meshtastic_MeshPacket_msg, &mp))
|
||||
return;
|
||||
if (mp.which_payload_variant != meshtastic_MeshPacket_encrypted_tag)
|
||||
return;
|
||||
|
||||
mp.transport_mechanism =
|
||||
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH;
|
||||
mp.rx_snr = 0;
|
||||
mp.rx_rssi = 0;
|
||||
|
||||
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
||||
if (router)
|
||||
router->enqueueReceivedMessage(p.release());
|
||||
}
|
||||
|
||||
// Shared TX path: encode packet to bytes
|
||||
size_t encodeForBLE(const meshtastic_MeshPacket *mp, uint8_t *buf, size_t bufLen) {
|
||||
return pb_encode_to_bytes(buf, bufLen, &meshtastic_MeshPacket_msg, mp);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH
|
||||
```
|
||||
|
||||
### 2.2 Wire format
|
||||
|
||||
Same as UDP multicast — protobuf-encoded `meshtastic_MeshPacket` (already encrypted). This means:
|
||||
|
||||
- Packets are encoded with `pb_encode_to_bytes()` on TX
|
||||
- Decoded with `pb_decode_from_bytes()` on RX
|
||||
- Encryption already applied by Router before `onSend()` is called
|
||||
- Same deduplication via `PacketHistory` (keyed on `sender, id`)
|
||||
|
||||
### 2.3 Echo prevention
|
||||
|
||||
Same pattern as UDP ([UdpMulticastHandler.h:102](src/mesh/udp/UdpMulticastHandler.h#L102)):
|
||||
|
||||
```cpp
|
||||
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH) {
|
||||
LOG_ERROR("Attempt to send BLE-sourced packet over BLE");
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: ESP32 Implementation (NimBLE)
|
||||
|
||||
### Platform: ESP32, ESP32-S3, ESP32-C3, ESP32-C6
|
||||
|
||||
**New files:** `src/nimble/NimbleBLEMesh.h` / `NimbleBLEMesh.cpp`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH && defined(ARCH_ESP32)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
|
||||
class NimbleBLEMesh : public BLEMeshHandler {
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
|
||||
private:
|
||||
// --- Advertising mode ---
|
||||
void startScanning();
|
||||
void stopScanning();
|
||||
static void onScanResult(const ble_gap_disc_desc *desc, void *arg);
|
||||
void broadcastViaAdv(const uint8_t *data, size_t len);
|
||||
|
||||
// --- GATT mode ---
|
||||
void setupMeshService();
|
||||
static int onMeshCharWrite(uint16_t conn_handle,
|
||||
const ble_gatt_error *error,
|
||||
ble_gatt_attr *attr, void *arg);
|
||||
void sendViaGATT(const uint8_t *data, size_t len);
|
||||
|
||||
// --- Peer tracking ---
|
||||
struct BLEPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle; // 0 if not connected
|
||||
};
|
||||
std::vector<BLEPeer> peers;
|
||||
void updatePeer(const ble_addr_t &addr, NodeNum nodeNum, int8_t rssi);
|
||||
void pruneStale();
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### 3.1 Advertising mode (connectionless)
|
||||
|
||||
Encode mesh packets into BLE extended advertisements with Meshtastic manufacturer data.
|
||||
|
||||
**TX flow:**
|
||||
|
||||
1. `onSend()` called from `Router::send()`
|
||||
2. Encode packet via `encodeForBLE()`
|
||||
3. Pack into extended advertisement manufacturer data
|
||||
4. Broadcast for configurable number of intervals
|
||||
|
||||
**RX flow:**
|
||||
|
||||
1. NimBLE scan callback fires
|
||||
2. Check manufacturer ID for Meshtastic magic
|
||||
3. Extract protobuf bytes
|
||||
4. Call `deliverToRouter()`
|
||||
|
||||
**Advertisement structure:**
|
||||
|
||||
```
|
||||
AD Type: 0xFF (Manufacturer Specific)
|
||||
Company ID: 0x????? (Meshtastic BLE SIG ID or 0xFFFF)
|
||||
Mesh Version: 1 byte
|
||||
Protobuf payload: variable (up to ~244 bytes with extended adv)
|
||||
```
|
||||
|
||||
**Key NimBLE APIs:**
|
||||
|
||||
- `ble_gap_ext_adv_set_data()` / `ble_gap_ext_adv_start()` — extended adv TX
|
||||
- `ble_gap_disc()` or `ble_gap_ext_disc()` — scanning
|
||||
- Scan callback receives `ble_gap_disc_desc` or `ble_gap_ext_disc_desc`
|
||||
|
||||
### 3.2 GATT mode (connection-based)
|
||||
|
||||
Add a new GATT service for mesh relay, separate from the phone API service:
|
||||
|
||||
**New Mesh Relay Service:**
|
||||
|
||||
```
|
||||
Mesh Relay Service UUID: (new, distinct from phone service in BluetoothCommon.h)
|
||||
├── MeshTX (WRITE_NO_RSP) — peers write mesh packets to us
|
||||
├── MeshRX (NOTIFY) — we notify peers of new mesh packets
|
||||
└── MeshPeer (READ) — our NodeNum for discovery
|
||||
```
|
||||
|
||||
**TX:** Write to MeshTX on all connected peers (broadcast) or specific next-hop peer (directed).
|
||||
**RX:** MeshTX write callback → `deliverToRouter()`.
|
||||
|
||||
### 3.3 Coexistence with phone BLE
|
||||
|
||||
Both services registered on the same NimBLE host:
|
||||
|
||||
- Phone API service: existing UUIDs ([BluetoothCommon.h](src/BluetoothCommon.h))
|
||||
- Mesh relay service: new UUIDs
|
||||
- Separate connection handles for phone vs mesh peers
|
||||
- Use extended adv set 0 for phone, set 1 for mesh (NimBLE supports multiple adv sets)
|
||||
- Reserve 1 connection for phone, remaining for mesh peers
|
||||
|
||||
### 3.4 ESP32 variant considerations
|
||||
|
||||
| Variant | BLE | Extended Adv | Max Conn | Strategy |
|
||||
| -------- | --- | ------------ | -------- | --------------------------------------- |
|
||||
| ESP32 | 4.2 | No | 9 | GATT-only (legacy adv too small at 31B) |
|
||||
| ESP32-S3 | 5.0 | Yes | 9 | Full adv+GATT |
|
||||
| ESP32-C3 | 5.0 | Yes | 3 | Adv preferred (limited connections) |
|
||||
| ESP32-C6 | 5.0 | Yes | 9 | Full adv+GATT |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: nRF52 Implementation (SoftDevice/Bluefruit)
|
||||
|
||||
### Platform: nRF52840 (primary), nRF52833
|
||||
|
||||
**New files:** `src/platform/nrf52/NRF52BLEMesh.h` / `NRF52BLEMesh.cpp`
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#if HAS_BLE_MESH && defined(ARCH_NRF52)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
#include <bluefruit.h>
|
||||
|
||||
class NRF52BLEMesh : public BLEMeshHandler {
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
|
||||
private:
|
||||
// Advertising mode
|
||||
void startScanning();
|
||||
static void onScanCallback(ble_gap_evt_adv_report_t *report);
|
||||
void broadcastViaAdv(const uint8_t *data, size_t len);
|
||||
|
||||
// GATT mode
|
||||
BLEService meshService;
|
||||
BLECharacteristic meshTxChar;
|
||||
BLECharacteristic meshRxChar;
|
||||
void setupMeshService();
|
||||
static void onMeshWrite(uint16_t connHandle, BLECharacteristic *chr,
|
||||
uint8_t *data, uint16_t len);
|
||||
void sendViaGATT(const uint8_t *data, size_t len);
|
||||
|
||||
// Peers
|
||||
struct BLEPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_gap_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle;
|
||||
};
|
||||
std::vector<BLEPeer> peers;
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### 4.1 Advertising mode
|
||||
|
||||
nRF52840 supports BLE 5.0 extended advertisements (up to 247 bytes).
|
||||
|
||||
**Bluefruit APIs:**
|
||||
|
||||
- `Bluefruit.Scanner.setRxCallback()` — scan result callback
|
||||
- `Bluefruit.Scanner.start()` / `.stop()` — control scanning
|
||||
- `Bluefruit.Advertising` — for standard adv
|
||||
- Raw SoftDevice `sd_ble_gap_adv_set_configure()` if Bluefruit doesn't expose extended adv data
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Use Bluefruit Scanner for RX (filter by Meshtastic manufacturer ID)
|
||||
- For TX, either use Bluefruit Advertising API or drop to raw SoftDevice for extended adv payloads
|
||||
|
||||
### 4.2 GATT mode
|
||||
|
||||
Same service structure as ESP32, using Bluefruit GATT APIs:
|
||||
|
||||
```cpp
|
||||
meshService = BLEService(MESH_RELAY_SERVICE_UUID);
|
||||
meshTxChar = BLECharacteristic(MESH_TX_CHAR_UUID);
|
||||
meshTxChar.setProperties(CHR_PROPS_WRITE_WO_RESP);
|
||||
meshTxChar.setWriteCallback(onMeshWrite);
|
||||
meshTxChar.setMaxLen(512);
|
||||
|
||||
meshRxChar = BLECharacteristic(MESH_RX_CHAR_UUID);
|
||||
meshRxChar.setProperties(CHR_PROPS_NOTIFY);
|
||||
meshRxChar.setMaxLen(512);
|
||||
```
|
||||
|
||||
**Peer connections:**
|
||||
|
||||
- nRF52840 SoftDevice S140 supports up to 20 concurrent connections
|
||||
- Use `Bluefruit.Central` for outbound connections to mesh peers
|
||||
- Bluefruit supports simultaneous peripheral (phone) + central (mesh peers)
|
||||
|
||||
### 4.3 Coexistence with phone BLE
|
||||
|
||||
Existing phone service ([NRF52Bluetooth.cpp](src/platform/nrf52/NRF52Bluetooth.cpp)) uses peripheral role:
|
||||
|
||||
- Keep phone API service as-is
|
||||
- Add mesh relay service alongside it
|
||||
- Scanner shared between peer discovery and mesh RX (filter by service UUID or manufacturer data)
|
||||
- SoftDevice connection budget: 1 peripheral (phone) + N central (mesh peers)
|
||||
|
||||
### 4.4 nRF52 extras
|
||||
|
||||
| Feature | nRF52840 | nRF52833 |
|
||||
| ---------------------- | -------- | -------- |
|
||||
| Extended Adv | Yes | Yes |
|
||||
| Max Connections | 20 | 20 |
|
||||
| RAM per connection | ~1.8KB | ~1.8KB |
|
||||
| Coded PHY (Long Range) | Yes | Yes |
|
||||
|
||||
**Coded PHY:** BLE Coded PHY (125kbps) provides ~4x range vs 1Mbps PHY. Could be used for extended-range BLE mesh links as a configuration option.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Router Integration
|
||||
|
||||
### 5.1 Hook into Router::send()
|
||||
|
||||
**File:** [src/mesh/Router.cpp](src/mesh/Router.cpp) — add right next to UDP handler:
|
||||
|
||||
```cpp
|
||||
#if HAS_BLE_MESH
|
||||
if (bleMeshHandler && config.bluetooth.mesh_enabled) {
|
||||
bleMeshHandler->onSend(p);
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
### 5.2 Global handler instance
|
||||
|
||||
**File:** [src/main.h](src/main.h):
|
||||
|
||||
```cpp
|
||||
#ifdef HAS_BLE_MESH
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
extern BLEMeshHandler *bleMeshHandler;
|
||||
#endif
|
||||
```
|
||||
|
||||
**File:** [src/main.cpp](src/main.cpp):
|
||||
|
||||
```cpp
|
||||
#ifdef HAS_BLE_MESH
|
||||
BLEMeshHandler *bleMeshHandler = nullptr;
|
||||
#endif
|
||||
|
||||
// In setup():
|
||||
#ifdef HAS_BLE_MESH
|
||||
#if defined(ARCH_ESP32)
|
||||
bleMeshHandler = new NimbleBLEMesh();
|
||||
#elif defined(ARCH_NRF52)
|
||||
bleMeshHandler = new NRF52BLEMesh();
|
||||
#endif
|
||||
bleMeshHandler->start();
|
||||
#endif
|
||||
```
|
||||
|
||||
### 5.3 NODENUM_BROADCAST_NO_LORA
|
||||
|
||||
Already reserved ([MeshTypes.h:13-14](src/mesh/MeshTypes.h#L13-L14)) and already dropped by LoRa ([RadioLibInterface.cpp:170](src/mesh/RadioLibInterface.cpp#L170)). BLE mesh handler should still send packets addressed to `NODENUM_BROADCAST_NO_LORA` — this is its purpose.
|
||||
|
||||
### 5.4 Feature flag
|
||||
|
||||
**File:** `src/configuration.h` or per-variant `platformio.ini`:
|
||||
|
||||
```cpp
|
||||
#define HAS_BLE_MESH 1 // Enable BLE mesh transport
|
||||
```
|
||||
|
||||
Disabled by default. Enabled per-board in variant configs for boards with sufficient BLE capability.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Peer Discovery & Management
|
||||
|
||||
### 6.1 Discovery methods
|
||||
|
||||
1. **Passive scanning**: Listen for Meshtastic manufacturer data in BLE advertisements
|
||||
2. **Service discovery**: Scan for Mesh Relay Service UUID (GATT mode)
|
||||
3. **NodeDB integration**: Update `meshtastic_NodeInfoLite` with BLE reachability when a peer is discovered
|
||||
|
||||
### 6.2 Peer table
|
||||
|
||||
```cpp
|
||||
struct BLEMeshPeer {
|
||||
NodeNum nodeNum;
|
||||
// Platform-specific BLE address (ble_addr_t on ESP32, ble_gap_addr_t on nRF52)
|
||||
uint8_t addr[6];
|
||||
uint8_t addrType;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
uint16_t connHandle; // 0 = not connected
|
||||
uint8_t failedConnAttempts;
|
||||
};
|
||||
```
|
||||
|
||||
Max peers: configurable (default 8), bounded by platform connection limits.
|
||||
|
||||
### 6.3 Connection policy (GATT mode)
|
||||
|
||||
- Auto-connect if RSSI > threshold and slots available
|
||||
- Disconnect stale peers (no traffic for N minutes)
|
||||
- Prefer advertising for broadcasts, GATT for directed/reliable
|
||||
- Exponential backoff on failed connection attempts
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Power Management
|
||||
|
||||
### 7.1 Scan duty cycling
|
||||
|
||||
```
|
||||
Active scanning: ~15mA (nRF52), ~90mA (ESP32)
|
||||
Advertising: ~5mA peak per event
|
||||
Idle: ~0mA additional
|
||||
|
||||
Default strategy:
|
||||
- Scan window: 50ms every 500ms (10% duty cycle)
|
||||
- ~1.5mA average on nRF52, ~9mA on ESP32
|
||||
- Configurable via BleMeshConfig.interval_ms
|
||||
```
|
||||
|
||||
### 7.2 Sleep integration
|
||||
|
||||
- Light sleep: pause scanning, continue advertising at reduced rate
|
||||
- Deep sleep: disable BLE mesh entirely
|
||||
- Hook into existing power management observers
|
||||
|
||||
### 7.3 Role-based behavior
|
||||
|
||||
| Device Role | BLE Mesh Behavior |
|
||||
| ----------- | ------------------------------------------- |
|
||||
| CLIENT | Scan on-demand, advertise when sending |
|
||||
| CLIENT_MUTE | Receive only, no rebroadcast |
|
||||
| ROUTER | Continuous scan+advertise, max connections |
|
||||
| ROUTER_LATE | Delayed rebroadcast (same as LoRa behavior) |
|
||||
| SENSOR | Minimal: advertise telemetry only |
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Testing & Validation
|
||||
|
||||
### 8.1 Unit tests
|
||||
|
||||
- BLE wire format encode/decode roundtrip
|
||||
- PacketHistory deduplication across LoRa + BLE
|
||||
- Peer table management (add, prune, limits)
|
||||
- Echo prevention (don't re-send BLE-sourced packets over BLE)
|
||||
|
||||
### 8.2 Integration tests
|
||||
|
||||
- Two ESP32 nodes: BLE-only mesh (no LoRa)
|
||||
- Two nRF52 nodes: BLE-only mesh
|
||||
- Three nodes: multi-hop BLE relay
|
||||
- Hybrid: Node A (LoRa+BLE) <-> Node B (BLE only) <-> Node C (LoRa+BLE)
|
||||
- Phone API + BLE mesh coexistence
|
||||
- Power consumption profiling
|
||||
|
||||
### 8.3 Compatibility matrix
|
||||
|
||||
| Test | ESP32 | ESP32-S3 | ESP32-C3 | ESP32-C6 | nRF52840 |
|
||||
| ------------- | ----------- | -------- | -------- | -------- | -------- |
|
||||
| Adv mode | GATT only\* | Yes | Yes | Yes | Yes |
|
||||
| GATT mode | Yes | Yes | Yes | Yes | Yes |
|
||||
| Phone coexist | Yes | Yes | Yes | Yes | Yes |
|
||||
| Multi-hop | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
\*ESP32 (BLE 4.2): legacy adv limited to 31B, use GATT-only mode
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Milestone 1: Foundation
|
||||
|
||||
1. Protobuf additions (`TRANSPORT_BLE_MESH`, `BleMeshConfig`)
|
||||
2. `BLEMeshHandler` base class
|
||||
3. Router hook (alongside UDP handler)
|
||||
4. Feature flag + main.cpp wiring
|
||||
|
||||
### Milestone 2: ESP32 MVP
|
||||
|
||||
5. `NimbleBLEMesh` — advertising mode on ESP32-S3
|
||||
6. Phone BLE coexistence validation
|
||||
7. Basic two-node BLE mesh test
|
||||
|
||||
### Milestone 3: nRF52 MVP
|
||||
|
||||
8. `NRF52BLEMesh` — advertising mode
|
||||
9. Cross-platform interop (ESP32 <-> nRF52 BLE mesh)
|
||||
|
||||
### Milestone 4: GATT Mode
|
||||
|
||||
10. GATT service + connection management for ESP32
|
||||
11. GATT service + connection management for nRF52
|
||||
12. Peer discovery & table management
|
||||
|
||||
### Milestone 5: Polish
|
||||
|
||||
13. Power management integration
|
||||
14. Full test matrix
|
||||
15. ESP32 (BLE 4.2) GATT-only fallback
|
||||
16. Admin UI / config in phone apps
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| -------------------------------- | ------------------------- | ---------------------------------------------------------------------------- |
|
||||
| BLE scan power drain | Battery life | Duty cycling, role-based policy |
|
||||
| Phone BLE drops during mesh scan | UX regression | Separate adv sets, careful interleaving |
|
||||
| NimBLE/Bluefruit API differences | Platform code duplication | Thin platform subclasses, shared base logic |
|
||||
| No extended adv on ESP32 (4.2) | Can't fit packets in adv | GATT-only fallback |
|
||||
| BLE range (~30-100m) | Coverage gaps | Hybrid LoRa+BLE; Coded PHY on nRF52 |
|
||||
| Packet storms in dense BLE mesh | Saturation | Existing flood mitigation (PacketHistory, hop_limit, role-based rebroadcast) |
|
||||
|
||||
---
|
||||
|
||||
## Files to Create
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------- | --------------------------------------------- |
|
||||
| `src/mesh/BLEMeshHandler.h` | Base handler class (like UdpMulticastHandler) |
|
||||
| `src/nimble/NimbleBLEMesh.h` | ESP32 BLE mesh header |
|
||||
| `src/nimble/NimbleBLEMesh.cpp` | ESP32 NimBLE implementation |
|
||||
| `src/platform/nrf52/NRF52BLEMesh.h` | nRF52 BLE mesh header |
|
||||
| `src/platform/nrf52/NRF52BLEMesh.cpp` | nRF52 Bluefruit implementation |
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Change |
|
||||
| --------------------------------------- | -------------------------------------------------- |
|
||||
| `src/mesh/Router.cpp` | Add `bleMeshHandler->onSend()` next to UDP handler |
|
||||
| `src/main.h` | Declare `extern BLEMeshHandler *bleMeshHandler` |
|
||||
| `src/main.cpp` | Instantiate + start BLE mesh handler |
|
||||
| `src/configuration.h` | `HAS_BLE_MESH` feature flag |
|
||||
| Protobufs (`mesh.proto`) | `TRANSPORT_BLE_MESH` enum + `BleMeshConfig` |
|
||||
| `src/nimble/NimbleBluetooth.cpp` | Coexistence (adv set separation) |
|
||||
| `src/platform/nrf52/NRF52Bluetooth.cpp` | Coexistence (scanner sharing) |
|
||||
| Platform `variant.h` / `platformio.ini` | Enable per-board |
|
||||
+1
-5
@@ -97,11 +97,7 @@ lib_deps =
|
||||
${env.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL
|
||||
end2endzone/NonBlockingRTTTL@1.4.0
|
||||
build_unflags =
|
||||
-std=c++11
|
||||
-std=gnu++11
|
||||
build_flags = ${env.build_flags} -Os
|
||||
-std=gnu++17
|
||||
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
|
||||
|
||||
; Common libs for communicating over TCP/IP networks such as MQTT
|
||||
@@ -126,7 +122,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||
https://github.com/meshtastic/device-ui/archive/622b034d8791153de9d16a473723cb8625d35839.zip
|
||||
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
|
||||
+1
-1
Submodule protobufs updated: eba2d94c8d...2edc5ab7b1
@@ -345,7 +345,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
|
||||
return;
|
||||
}
|
||||
|
||||
void RedirectablePrint::hexDump(const char *logLevel, const unsigned char *buf, uint16_t len)
|
||||
void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len)
|
||||
{
|
||||
const char alphabet[17] = "0123456789abcdef";
|
||||
log(logLevel, " +------------------------------------------------+ +----------------+");
|
||||
|
||||
@@ -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, const unsigned char *buf, uint16_t len);
|
||||
void hexDump(const char *logLevel, unsigned char *buf, uint16_t len);
|
||||
|
||||
std::string mt_sprintf(const std::string fmt_str, ...);
|
||||
|
||||
|
||||
@@ -78,11 +78,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// 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
|
||||
@@ -504,7 +499,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#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
|
||||
|
||||
@@ -37,9 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
|
||||
{
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, LSM6DSOX, BMX160,
|
||||
STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270};
|
||||
return firstOfOrNONE(11, types);
|
||||
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270};
|
||||
return firstOfOrNONE(10, types);
|
||||
}
|
||||
|
||||
ScanI2C::FoundDevice ScanI2C::firstAQI() const
|
||||
|
||||
@@ -47,7 +47,6 @@ class ScanI2C
|
||||
BMA423,
|
||||
BQ24295,
|
||||
LSM6DS3,
|
||||
LSM6DSOX,
|
||||
TCA9535,
|
||||
TCA9555,
|
||||
VEML7700,
|
||||
|
||||
@@ -341,9 +341,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = DPS310;
|
||||
break;
|
||||
}
|
||||
if (type == DPS310) {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID
|
||||
switch (registerValue) {
|
||||
@@ -511,9 +509,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
if (registerValue == 0x6A) {
|
||||
type = LSM6DS3;
|
||||
logFoundDevice("LSM6DS3", (uint8_t)addr.address);
|
||||
} else if (registerValue == 0x6C) {
|
||||
type = LSM6DSOX;
|
||||
logFoundDevice("LSM6DSOX", (uint8_t)addr.address);
|
||||
} else {
|
||||
type = QMI8658;
|
||||
logFoundDevice("QMI8658", (uint8_t)addr.address);
|
||||
@@ -551,19 +546,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
|
||||
break;
|
||||
|
||||
case LSM6DS3_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1);
|
||||
if (registerValue == 0x6A) {
|
||||
type = LSM6DS3;
|
||||
logFoundDevice("LSM6DS3", (uint8_t)addr.address);
|
||||
} else if (registerValue == 0x6C) {
|
||||
type = LSM6DSOX;
|
||||
logFoundDevice("LSM6DSOX", (uint8_t)addr.address);
|
||||
} else {
|
||||
type = QMI8658;
|
||||
logFoundDevice("QMI8658", (uint8_t)addr.address);
|
||||
}
|
||||
break;
|
||||
SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
|
||||
case TCA9555_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1);
|
||||
|
||||
@@ -143,10 +143,6 @@ 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
|
||||
@@ -206,8 +202,7 @@ 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(MINI_EPAPER_S3)
|
||||
defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
|
||||
{
|
||||
// Start HSPI
|
||||
hspi = new SPIClass(HSPI);
|
||||
@@ -221,13 +216,9 @@ 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)
|
||||
@@ -268,6 +259,17 @@ bool EInkDisplay::connect()
|
||||
adafruitDisplay->setRotation(3);
|
||||
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
|
||||
}
|
||||
#elif defined(MINI_EPAPER_S3)
|
||||
spi1 = new SPIClass(HSPI);
|
||||
spi1->begin(PIN_SPI1_SCK, PIN_SPI1_MISO, PIN_SPI1_MOSI, PIN_EINK_CS);
|
||||
|
||||
// Create GxEPD2 objects
|
||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1);
|
||||
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
|
||||
|
||||
// Init GxEPD2
|
||||
adafruitDisplay->init();
|
||||
adafruitDisplay->setRotation(1);
|
||||
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
|
||||
|
||||
// Detect display model, before starting SPI
|
||||
|
||||
@@ -89,12 +89,12 @@ 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(MINI_EPAPER_S3)
|
||||
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5)
|
||||
SPIClass *hspi = NULL;
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
|
||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) || \
|
||||
defined(MINI_EPAPER_S3)
|
||||
SPIClass *spi1 = NULL;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
|
||||
#include "graphics/EmoteRenderer.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
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<uint8_t>(s[pos]) == 0xEF && static_cast<uint8_t>(s[pos + 1]) == 0xB8 &&
|
||||
static_cast<uint8_t>(s[pos + 2]) == 0x8F;
|
||||
}
|
||||
|
||||
static inline bool isSkinToneAt(const char *s, size_t pos, size_t len)
|
||||
{
|
||||
return pos + 3 < len && static_cast<uint8_t>(s[pos]) == 0xF0 && static_cast<uint8_t>(s[pos + 1]) == 0x9F &&
|
||||
static_cast<uint8_t>(s[pos + 2]) == 0x8F &&
|
||||
(static_cast<uint8_t>(s[pos + 3]) >= 0xBB && static_cast<uint8_t>(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<uint8_t>(text[ti]);
|
||||
const uint8_t lc = static_cast<uint8_t>(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<uint8_t>(text[pos])))
|
||||
return nullptr;
|
||||
|
||||
for (int i = 0; i < emoteCount; ++i) {
|
||||
const char *label = emoteSet[i].label;
|
||||
if (!label || !*label)
|
||||
continue;
|
||||
if (static_cast<uint8_t>(label[0]) != static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(line[next]));
|
||||
}
|
||||
|
||||
if (next == i)
|
||||
next += utf8CharLen(static_cast<uint8_t>(line[i]));
|
||||
|
||||
cursorX = appendTextSpanAndMeasure(display, cursorX, fontY, line + i, next - i, true, fauxBold && inBold);
|
||||
i = next;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace EmoteRenderer
|
||||
} // namespace graphics
|
||||
|
||||
#endif // HAS_SCREEN
|
||||
@@ -1,79 +0,0 @@
|
||||
#pragma once
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_SCREEN
|
||||
#include "graphics/emotes.h"
|
||||
#include <Arduino.h>
|
||||
#include <OLEDDisplay.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<char> 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
|
||||
@@ -121,10 +121,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
}
|
||||
|
||||
// === Screen Title ===
|
||||
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_CENTER);
|
||||
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
||||
if (config.display.heading_bold) {
|
||||
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
||||
}
|
||||
}
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
@@ -514,4 +515,4 @@ std::string sanitizeString(const std::string &input)
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
@@ -7,7 +7,6 @@
|
||||
#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"
|
||||
@@ -35,6 +34,44 @@ static std::vector<std::string> cachedLines;
|
||||
static std::vector<int> 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<uint8_t>(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;
|
||||
@@ -73,7 +110,102 @@ void scrollDown()
|
||||
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||
{
|
||||
graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, FONT_HEIGHT_SMALL, emotes, 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<uint8_t>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset scroll state when new messages arrive
|
||||
@@ -245,7 +377,32 @@ 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)
|
||||
{
|
||||
return graphics::EmoteRenderer::analyzeLine(display, line, 0, emotes, emoteCount).width;
|
||||
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<uint8_t>(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;
|
||||
}
|
||||
|
||||
struct MessageBlock {
|
||||
@@ -260,7 +417,13 @@ static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool i
|
||||
return lineTopY + (FONT_HEIGHT_SMALL - 1);
|
||||
}
|
||||
|
||||
const int tallest = graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes).tallestHeight;
|
||||
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 lineHeight = std::max(FONT_HEIGHT_SMALL, tallest);
|
||||
const int iconTop = lineTopY + (lineHeight - tallest) / 2;
|
||||
@@ -373,28 +536,30 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
|
||||
|
||||
// Title string depending on mode
|
||||
char titleStr[48];
|
||||
snprintf(titleStr, sizeof(titleStr), "Messages");
|
||||
static char titleBuf[32];
|
||||
const char *titleStr = "Messages";
|
||||
switch (currentMode) {
|
||||
case ThreadMode::ALL:
|
||||
snprintf(titleStr, sizeof(titleStr), "Messages");
|
||||
titleStr = "Messages";
|
||||
break;
|
||||
case ThreadMode::CHANNEL: {
|
||||
const char *cname = channels.getName(currentChannel);
|
||||
if (cname && cname[0]) {
|
||||
snprintf(titleStr, sizeof(titleStr), "#%s", cname);
|
||||
snprintf(titleBuf, sizeof(titleBuf), "#%s", cname);
|
||||
} else {
|
||||
snprintf(titleStr, sizeof(titleStr), "Ch%d", currentChannel);
|
||||
snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel);
|
||||
}
|
||||
titleStr = titleBuf;
|
||||
break;
|
||||
}
|
||||
case ThreadMode::DIRECT: {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
|
||||
if (node && node->has_user && node->user.short_name[0]) {
|
||||
snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name);
|
||||
if (node && node->has_user) {
|
||||
snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name);
|
||||
} else {
|
||||
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer);
|
||||
snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer);
|
||||
}
|
||||
titleStr = titleBuf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -501,50 +666,44 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender);
|
||||
meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
|
||||
|
||||
char senderName[64] = "";
|
||||
char senderBuf[48] = "";
|
||||
if (node && node->has_user) {
|
||||
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);
|
||||
// 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 this is *our own* message, override senderName to who the recipient was
|
||||
// If this is *our own* message, override senderBuf to who the recipient was
|
||||
bool mine = (m.sender == nodeDB->getNodeNum());
|
||||
if (mine && node_recipient && node_recipient->has_user) {
|
||||
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);
|
||||
strcpy(senderBuf, node_recipient->user.long_name);
|
||||
}
|
||||
|
||||
// Shrink Sender name if needed
|
||||
int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) -
|
||||
display->getStringWidth(chanType) - graphics::UIRenderer::measureStringWithEmotes(display, " @...");
|
||||
display->getStringWidth(chanType) - display->getStringWidth(" @...");
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
char truncatedSender[64];
|
||||
graphics::UIRenderer::truncateStringWithEmotes(display, senderName, truncatedSender, sizeof(truncatedSender), availWidth);
|
||||
|
||||
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, "...");
|
||||
}
|
||||
|
||||
// Final header line
|
||||
char headerStr[128];
|
||||
char headerStr[96];
|
||||
if (mine) {
|
||||
if (currentMode == ThreadMode::ALL) {
|
||||
if (strcmp(chanType, "(DM)") == 0) {
|
||||
snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, truncatedSender);
|
||||
snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf);
|
||||
} else {
|
||||
snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType);
|
||||
}
|
||||
@@ -552,11 +711,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
snprintf(headerStr, sizeof(headerStr), "%s", timeBuf);
|
||||
}
|
||||
} else {
|
||||
snprintf(headerStr, sizeof(headerStr), chanType[0] ? "%s @%s %s" : "%s @%s", timeBuf, truncatedSender, chanType);
|
||||
snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType);
|
||||
}
|
||||
|
||||
// Push header line
|
||||
allLines.push_back(headerStr);
|
||||
allLines.push_back(std::string(headerStr));
|
||||
isMine.push_back(mine);
|
||||
isHeader.push_back(true);
|
||||
ackForLine.push_back(m.ackStatus);
|
||||
@@ -657,8 +816,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
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;
|
||||
bool thisLineHasEmote = false;
|
||||
for (int e = 0; e < numEmotes; ++e) {
|
||||
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
|
||||
thisLineHasEmote = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (thisLineHasEmote) {
|
||||
constexpr int EMOTE_PADDING_ABOVE = 4;
|
||||
visualTop -= EMOTE_PADDING_ABOVE;
|
||||
@@ -687,7 +851,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
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());
|
||||
w = display->getStringWidth(cachedLines[i].c_str());
|
||||
if (b.mine)
|
||||
w += 12; // room for ACK/NACK/relay mark
|
||||
} else {
|
||||
@@ -743,7 +907,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
|
||||
if (isHeader[i]) {
|
||||
|
||||
int w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str());
|
||||
int w = display->getStringWidth(cachedLines[i].c_str());
|
||||
int headerX;
|
||||
if (isMine[i]) {
|
||||
// push header left to avoid overlap with scrollbar
|
||||
@@ -753,8 +917,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
} else {
|
||||
headerX = x + textIndent;
|
||||
}
|
||||
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1,
|
||||
false);
|
||||
display->drawString(headerX, lineY, cachedLines[i].c_str());
|
||||
|
||||
// Draw underline just under header text
|
||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||
@@ -842,7 +1005,11 @@ std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerS
|
||||
} else {
|
||||
word += ch;
|
||||
std::string test = line + word;
|
||||
uint16_t strWidth = graphics::UIRenderer::measureStringWithEmotes(display, test.c_str());
|
||||
#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
|
||||
if (strWidth > textWidth) {
|
||||
if (!line.empty())
|
||||
lines.push_back(line);
|
||||
@@ -871,20 +1038,31 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
|
||||
std::vector<int> rowHeights;
|
||||
rowHeights.reserve(lines.size());
|
||||
std::vector<graphics::EmoteRenderer::LineMetrics> 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;
|
||||
|
||||
const int tallestEmote = lineMetrics[idx].tallestHeight;
|
||||
const bool hasEmote = lineMetrics[idx].hasEmote;
|
||||
const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isHeaderVec[idx]) {
|
||||
// Header line spacing
|
||||
@@ -934,22 +1112,22 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
|
||||
// Banner logic
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
||||
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';
|
||||
}
|
||||
char longName[48] = "?";
|
||||
if (node && node->user.long_name) {
|
||||
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
|
||||
longName[sizeof(longName) - 1] = '\0';
|
||||
}
|
||||
int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20);
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
char truncatedLongName[64];
|
||||
graphics::UIRenderer::truncateStringWithEmotes(display, longName, truncatedLongName, sizeof(truncatedLongName),
|
||||
availWidth);
|
||||
|
||||
size_t origLen = strlen(longName);
|
||||
while (longName[0] && display->getStringWidth(longName) > availWidth) {
|
||||
longName[strlen(longName) - 1] = '\0';
|
||||
}
|
||||
if (strlen(longName) < origLen) {
|
||||
strcat(longName, "...");
|
||||
}
|
||||
const char *msgRaw = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
|
||||
|
||||
char banner[256];
|
||||
@@ -967,8 +1145,8 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
}
|
||||
|
||||
if (isAlert) {
|
||||
if (truncatedLongName[0])
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", truncatedLongName);
|
||||
if (longName && longName[0])
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
else
|
||||
strcpy(banner, "Alert Received");
|
||||
} else {
|
||||
@@ -976,11 +1154,11 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
if (isChannelMuted)
|
||||
return;
|
||||
|
||||
if (truncatedLongName[0]) {
|
||||
if (longName && longName[0]) {
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
strcpy(banner, "New Message");
|
||||
} else {
|
||||
snprintf(banner, sizeof(banner), "New Message from\n%s", truncatedLongName);
|
||||
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
||||
}
|
||||
} else
|
||||
strcpy(banner, "New Message");
|
||||
@@ -1043,4 +1221,4 @@ void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet)
|
||||
|
||||
} // namespace MessageRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
@@ -79,15 +79,13 @@ void scrollDown()
|
||||
// Utility Functions
|
||||
// =============================
|
||||
|
||||
std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth)
|
||||
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth)
|
||||
{
|
||||
(void)display;
|
||||
(void)columnWidth;
|
||||
static char nodeName[25]; // single static buffer we return
|
||||
nodeName[0] = '\0';
|
||||
|
||||
auto fallbackId = [&] {
|
||||
char id[12];
|
||||
std::snprintf(id, sizeof(id), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
|
||||
return std::string(id);
|
||||
auto writeFallbackId = [&] {
|
||||
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
|
||||
};
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
@@ -96,10 +94,42 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
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();
|
||||
// 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<int>(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';
|
||||
}
|
||||
std::strcat(nodeName, "...");
|
||||
}
|
||||
}
|
||||
|
||||
return nodeName;
|
||||
@@ -133,15 +163,6 @@ 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()
|
||||
{
|
||||
@@ -184,13 +205,10 @@ 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 = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
||||
int nameMaxWidth = columnWidth - 25;
|
||||
int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||
|
||||
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);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
char timeStr[10];
|
||||
@@ -210,7 +228,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
@@ -237,22 +255,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
|
||||
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
||||
int nameMaxWidth = 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 int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
@@ -297,13 +312,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int nameMaxWidth =
|
||||
getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28)
|
||||
: (isLeftCol ? 20 : 22)));
|
||||
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
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);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
char distStr[10] = "";
|
||||
|
||||
@@ -357,7 +368,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
@@ -403,18 +414,14 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
|
||||
// Adjust max text width depending on column and screen width
|
||||
int nameMaxWidth =
|
||||
getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28)
|
||||
: (isLeftCol ? 20 : 22)));
|
||||
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
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);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
@@ -821,4 +828,4 @@ void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields
|
||||
|
||||
} // namespace NodeListRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
@@ -57,7 +56,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
// Utility functions
|
||||
const char *getCurrentModeTitle_Nodes(int screenWidth);
|
||||
const char *getCurrentModeTitle_Location(int screenWidth);
|
||||
std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth);
|
||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth);
|
||||
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
|
||||
|
||||
// Scrolling controls
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "DisplayFormatters.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NotificationRenderer.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
@@ -300,7 +299,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
linePointers[i] = lineStarts[i];
|
||||
}
|
||||
char scratchLineBuffer[visibleTotalLines - lineCount][64];
|
||||
char scratchLineBuffer[visibleTotalLines - lineCount][40];
|
||||
|
||||
uint8_t firstOptionToShow = 0;
|
||||
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
|
||||
@@ -313,47 +312,28 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
}
|
||||
int scratchLineNum = 0;
|
||||
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
} else {
|
||||
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));
|
||||
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
|
||||
}
|
||||
if (i == curSelected) {
|
||||
selectedNodenum = node ? node->num : 0;
|
||||
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
strncpy(scratchLineBuffer[scratchLineNum], "> ", 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);
|
||||
strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36);
|
||||
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3);
|
||||
} else {
|
||||
strncpy(scratchLineBuffer[scratchLineNum], ">", 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);
|
||||
strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37);
|
||||
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2);
|
||||
}
|
||||
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0';
|
||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||
} else {
|
||||
strncpy(scratchLineBuffer[scratchLineNum], tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 1);
|
||||
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0';
|
||||
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
|
||||
scratchLineBuffer[scratchLineNum][39] = '\0';
|
||||
}
|
||||
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
|
||||
}
|
||||
@@ -521,13 +501,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
else // if the newline wasn't found, then pull string length from strlen
|
||||
lineLengths[lineCount] = strlen(lines[lineCount]);
|
||||
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
char measureBuffer[64] = {0};
|
||||
strncpy(measureBuffer, lines[lineCount], std::min<size_t>(lineLengths[lineCount], sizeof(measureBuffer) - 1));
|
||||
lineWidths[lineCount] = UIRenderer::measureStringWithEmotes(display, measureBuffer);
|
||||
} else {
|
||||
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
|
||||
}
|
||||
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];
|
||||
@@ -633,11 +607,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
|
||||
display->setColor(BLACK);
|
||||
int yOffset = 3;
|
||||
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->drawString(textX, lineY - yOffset, lineBuffer);
|
||||
display->setColor(WHITE);
|
||||
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||
} else {
|
||||
@@ -656,11 +626,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
int totalWidth = textWidth + barsWidth;
|
||||
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
|
||||
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
UIRenderer::drawStringWithEmotes(display, groupStartX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
|
||||
} else {
|
||||
display->drawString(groupStartX, lineY, lineBuffer);
|
||||
}
|
||||
display->drawString(groupStartX, lineY, lineBuffer);
|
||||
|
||||
int baseX = groupStartX + textWidth + gap;
|
||||
int baseY = lineY + effectiveLineHeight - 1;
|
||||
@@ -676,11 +642,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
UIRenderer::drawStringWithEmotes(display, textX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
|
||||
} else {
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
}
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
}
|
||||
lineY += (effectiveLineHeight);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#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"
|
||||
@@ -314,8 +313,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
#endif
|
||||
currentFavoriteNodeNum = node->num;
|
||||
// === Create the shortName and title string ===
|
||||
const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node";
|
||||
char titlestr[40];
|
||||
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
|
||||
char titlestr[32] = {0};
|
||||
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
|
||||
|
||||
// === Draw battery/time/mail header (common across screens) ===
|
||||
@@ -329,6 +328,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
|
||||
// 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) {
|
||||
@@ -338,8 +338,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
}
|
||||
|
||||
if (username) {
|
||||
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
|
||||
// Print node's long name (e.g. "Backpack Node")
|
||||
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
|
||||
}
|
||||
|
||||
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||
@@ -801,12 +802,14 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
// === Node Identity ===
|
||||
int textWidth = 0;
|
||||
int nameX = 0;
|
||||
const char *shortName = owner.short_name ? owner.short_name : "";
|
||||
char shortnameble[35];
|
||||
snprintf(shortnameble, sizeof(shortnameble), "%s",
|
||||
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
|
||||
|
||||
// === ShortName Centered ===
|
||||
textWidth = UIRenderer::measureStringWithEmotes(display, shortName);
|
||||
textWidth = display->getStringWidth(shortnameble);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||
#else
|
||||
if (powerStatus->getHasBattery()) {
|
||||
char batStr[20];
|
||||
@@ -901,36 +904,36 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
int textWidth = 0;
|
||||
int nameX = 0;
|
||||
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5;
|
||||
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';
|
||||
std::string longNameStr;
|
||||
|
||||
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
|
||||
longNameStr = sanitizeString(ourNode->user.long_name);
|
||||
}
|
||||
if (SCREEN_WIDTH - UIRenderer::measureStringWithEmotes(display, combinedName) > 10) {
|
||||
textWidth = UIRenderer::measureStringWithEmotes(display, combinedName);
|
||||
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);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
UIRenderer::drawStringWithEmotes(
|
||||
display, nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset,
|
||||
combinedName, FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawString(
|
||||
nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName);
|
||||
} else {
|
||||
// === LongName Centered ===
|
||||
textWidth = UIRenderer::measureStringWithEmotes(display, longName);
|
||||
textWidth = display->getStringWidth(longNameStr.c_str());
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], longName, FONT_HEIGHT_SMALL, 1,
|
||||
false);
|
||||
display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
|
||||
|
||||
// === ShortName Centered ===
|
||||
textWidth = UIRenderer::measureStringWithEmotes(display, shortName);
|
||||
textWidth = display->getStringWidth(shortnameble);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1,
|
||||
false);
|
||||
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||
}
|
||||
#endif
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
@@ -1042,12 +1045,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 = (idText && idText[0]);
|
||||
const bool useId = haveGlyphs(idText);
|
||||
constexpr uint8_t padding = 2;
|
||||
constexpr uint8_t dividerGap = 1;
|
||||
|
||||
// Text widths
|
||||
const uint16_t idTextWidth = useId ? UIRenderer::measureStringWithEmotes(display, idText) : 0;
|
||||
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true);
|
||||
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);
|
||||
@@ -1072,7 +1075,7 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
|
||||
|
||||
// Draw: text
|
||||
if (useId)
|
||||
UIRenderer::drawStringWithEmotes(display, idTextLeft, idTextTop, idText, FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawString(idTextLeft, idTextTop, idText);
|
||||
display->drawString(pauseTextLeft, pauseTextTop, pauseText);
|
||||
display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold
|
||||
|
||||
@@ -1105,16 +1108,11 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
|
||||
display->drawString(msgX, msgY, upperMsg);
|
||||
}
|
||||
// Draw version and short name in bottom middle
|
||||
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);
|
||||
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);
|
||||
screen->forceDisplay();
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
|
||||
@@ -1132,15 +1130,12 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
|
||||
display->drawString(x + 0, y + 0, upperMsg);
|
||||
|
||||
// Draw version and short name in upper right
|
||||
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);
|
||||
}
|
||||
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);
|
||||
screen->forceDisplay();
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
|
||||
@@ -1370,15 +1365,11 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O
|
||||
display->drawString(x + 0, y + 0, upperMsg);
|
||||
|
||||
// Draw version and shortname in upper right
|
||||
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);
|
||||
}
|
||||
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);
|
||||
screen->forceDisplay();
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
|
||||
@@ -1567,25 +1558,6 @@ 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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "NodeDB.h"
|
||||
#include "graphics/EmoteRenderer.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include <OLEDDisplay.h>
|
||||
@@ -81,28 +80,6 @@ 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
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "./GDEW0102T4.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
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<uint16_t>(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
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
|
||||
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
|
||||
@@ -1,203 +0,0 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "./UC8175.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#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
|
||||
@@ -1,62 +0,0 @@
|
||||
// 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
|
||||
@@ -349,13 +349,13 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
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)
|
||||
// Open the on-screen keyboard if the joystick is enabled
|
||||
if (settings->joystick.enabled)
|
||||
inkhud->openKeyboard();
|
||||
break;
|
||||
|
||||
case STORE_CANNEDMESSAGE_SELECTION:
|
||||
if (!settings->joystick.enabled || inkhud->twoWayRocker)
|
||||
if (!settings->joystick.enabled)
|
||||
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
|
||||
@@ -922,7 +922,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 && !inkhud->twoWayRocker)
|
||||
if (settings->joystick.enabled)
|
||||
items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT));
|
||||
items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS,
|
||||
&settings->optionalFeatures.notifications));
|
||||
@@ -1751,7 +1751,7 @@ void InkHUD::MenuApplet::populateSendPage()
|
||||
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)
|
||||
if (settings->joystick.enabled)
|
||||
items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
|
||||
|
||||
// One menu item for each canned message
|
||||
|
||||
@@ -152,11 +152,6 @@ void InkHUD::TipsApplet::onRender(bool full)
|
||||
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");
|
||||
|
||||
@@ -88,9 +88,6 @@ 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
|
||||
|
||||
@@ -133,4 +130,4 @@ class InkHUD
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -143,7 +143,7 @@ void InkHUD::WindowManager::openMenu()
|
||||
// Bring the AlignStick applet to the foreground
|
||||
void InkHUD::WindowManager::openAlignStick()
|
||||
{
|
||||
if (settings->joystick.enabled && !inkhud->twoWayRocker) {
|
||||
if (settings->joystick.enabled) {
|
||||
AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick");
|
||||
alignStick->bringToForeground();
|
||||
}
|
||||
@@ -151,9 +151,6 @@ void InkHUD::WindowManager::openAlignStick()
|
||||
|
||||
void InkHUD::WindowManager::openKeyboard()
|
||||
{
|
||||
if (!settings->joystick.enabled || inkhud->twoWayRocker)
|
||||
return;
|
||||
|
||||
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||
|
||||
if (keyboard) {
|
||||
@@ -165,9 +162,6 @@ void InkHUD::WindowManager::openKeyboard()
|
||||
|
||||
void InkHUD::WindowManager::closeKeyboard()
|
||||
{
|
||||
if (!settings->joystick.enabled || inkhud->twoWayRocker)
|
||||
return;
|
||||
|
||||
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||
|
||||
if (keyboard) {
|
||||
@@ -483,7 +477,7 @@ 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 && !inkhud->twoWayRocker) {
|
||||
if (settings->joystick.enabled) {
|
||||
addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
|
||||
addSystemApplet("Keyboard", new KeyboardApplet, new Tile);
|
||||
}
|
||||
@@ -509,7 +503,7 @@ 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 && !inkhud->twoWayRocker) {
|
||||
if (settings->joystick.enabled) {
|
||||
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
|
||||
inkhud->getSystemApplet("Keyboard")
|
||||
|
||||
@@ -156,24 +156,6 @@ 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);
|
||||
@@ -247,13 +229,6 @@ 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()
|
||||
|
||||
@@ -45,7 +45,6 @@ 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);
|
||||
@@ -55,7 +54,6 @@ 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
|
||||
|
||||
@@ -8,14 +8,6 @@ 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.
|
||||
@@ -54,4 +46,4 @@ void UpDownInterruptImpl1::handleIntUp()
|
||||
void UpDownInterruptImpl1::handleIntPressed()
|
||||
{
|
||||
upDownInterruptImpl1->intPressHandler();
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,15 @@ extern void tftSetup(void);
|
||||
UdpMulticastHandler *udpHandler = nullptr;
|
||||
#endif
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
BLEMeshHandler *bleMeshHandler = nullptr;
|
||||
#if defined(ARCH_NRF52)
|
||||
#include "platform/nrf52/NRF52BLEMesh.h"
|
||||
#elif defined(ARCH_ESP32)
|
||||
#include "platform/esp32/ESP32BLEMesh.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(TCXO_OPTIONAL)
|
||||
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down.
|
||||
#endif
|
||||
@@ -884,6 +893,17 @@ void setup()
|
||||
udpHandler->start();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
LOG_DEBUG("Start BLE mesh handler");
|
||||
#if defined(ARCH_NRF52)
|
||||
bleMeshHandler = new NRF52BLEMesh();
|
||||
#elif defined(ARCH_ESP32)
|
||||
bleMeshHandler = new ESP32BLEMesh();
|
||||
#endif
|
||||
if (bleMeshHandler)
|
||||
bleMeshHandler->start();
|
||||
#endif
|
||||
service = new MeshService();
|
||||
service->init();
|
||||
|
||||
@@ -62,6 +62,11 @@ extern AudioThread *audioThread;
|
||||
extern UdpMulticastHandler *udpHandler;
|
||||
#endif
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
extern BLEMeshHandler *bleMeshHandler;
|
||||
#endif
|
||||
|
||||
// Global Screen singleton.
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
|
||||
#include "MeshTypes.h"
|
||||
#include "Router.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
|
||||
// Meshtastic BLE mesh manufacturer data identifier
|
||||
// Using 0xFFFF (reserved for testing) until a real BLE SIG company ID is assigned
|
||||
#define BLE_MESH_COMPANY_ID 0xFFFF
|
||||
#define BLE_MESH_PROTOCOL_VERSION 1
|
||||
|
||||
// TODO: Add TRANSPORT_BLE_MESH = 8 to mesh.proto TransportMechanism enum and regenerate.
|
||||
// Until then, define it here so the code compiles.
|
||||
#ifndef meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH
|
||||
#define meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH ((meshtastic_MeshPacket_TransportMechanism)8)
|
||||
#endif
|
||||
|
||||
class BLEMeshHandler
|
||||
{
|
||||
public:
|
||||
BLEMeshHandler() : isRunning(false) {}
|
||||
virtual ~BLEMeshHandler() {}
|
||||
|
||||
virtual void start() = 0;
|
||||
virtual void stop() = 0;
|
||||
virtual void onBluetoothReady() {}
|
||||
|
||||
// Called from Router::send() to broadcast packet over BLE mesh
|
||||
virtual bool onSend(const meshtastic_MeshPacket *mp) = 0;
|
||||
|
||||
protected:
|
||||
bool isRunning;
|
||||
|
||||
// Decode received BLE mesh data and enqueue into router
|
||||
void deliverToRouter(const uint8_t *data, size_t len)
|
||||
{
|
||||
meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero;
|
||||
bool decoded = pb_decode_from_bytes(data, len, &meshtastic_MeshPacket_msg, &mp);
|
||||
if (!decoded || mp.which_payload_variant != meshtastic_MeshPacket_encrypted_tag)
|
||||
return;
|
||||
|
||||
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH;
|
||||
mp.pki_encrypted = false;
|
||||
mp.public_key.size = 0;
|
||||
memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes));
|
||||
mp.rx_snr = 0;
|
||||
mp.rx_rssi = 0;
|
||||
|
||||
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
||||
if (p && router)
|
||||
router->enqueueReceivedMessage(p.release());
|
||||
}
|
||||
|
||||
// Encode a mesh packet for BLE transmission, returns encoded length
|
||||
size_t encodeForBLE(const meshtastic_MeshPacket *mp, uint8_t *buf, size_t bufLen)
|
||||
{
|
||||
return pb_encode_to_bytes(buf, bufLen, &meshtastic_MeshPacket_msg, mp);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH
|
||||
@@ -30,11 +30,6 @@
|
||||
#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 // 1 day between identical positions
|
||||
|
||||
#ifdef USERPREFS_RINGTONE_NAG_SECS
|
||||
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
|
||||
#else
|
||||
|
||||
@@ -25,7 +25,7 @@ void LoRaFEMInterface::init(void)
|
||||
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
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
|
||||
setLnaCanControl(true);
|
||||
} else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) {
|
||||
// FEM is GC1109
|
||||
@@ -66,7 +66,7 @@ void LoRaFEMInterface::init(void)
|
||||
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
|
||||
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
|
||||
setLnaCanControl(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ typedef enum { GC1109_PA, KCT8103L_PA, OTHER_FEM_TYPES } LoRaFEMType;
|
||||
class LoRaFEMInterface
|
||||
{
|
||||
public:
|
||||
LoRaFEMInterface() : fem_type(OTHER_FEM_TYPES) {}
|
||||
LoRaFEMInterface() {}
|
||||
virtual ~LoRaFEMInterface() {}
|
||||
void init(void);
|
||||
void setSleepModeEnable(void);
|
||||
@@ -22,7 +22,7 @@ class LoRaFEMInterface
|
||||
|
||||
private:
|
||||
LoRaFEMType fem_type;
|
||||
bool lna_enabled = true;
|
||||
bool lna_enabled = false;
|
||||
bool lna_can_control = false;
|
||||
};
|
||||
extern LoRaFEMInterface loraFEMInterface;
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||
#include "modules/TraceRouteModule.h"
|
||||
#endif
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
#include "modules/TrafficManagementModule.h"
|
||||
#endif
|
||||
#include "NodeDB.h"
|
||||
|
||||
NextHopRouter::NextHopRouter() {}
|
||||
@@ -26,7 +23,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).value_or(NO_NEXT_HOP_PREFERENCE); // set the next hop
|
||||
p->next_hop = getNextHop(p->to, p->relay_node); // 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
|
||||
@@ -129,28 +126,15 @@ 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)
|
||||
{
|
||||
// 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 (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) {
|
||||
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);
|
||||
|
||||
// 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
|
||||
// Use shared logic to determine if hop_limit should be decremented
|
||||
if (shouldDecrementHopLimit(p)) {
|
||||
tosend->hop_limit--; // bump down the hop count
|
||||
} else {
|
||||
LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit");
|
||||
@@ -186,10 +170,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)
|
||||
*/
|
||||
std::optional<uint8_t> NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
|
||||
uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
|
||||
{
|
||||
if (isBroadcast(to))
|
||||
return std::nullopt;
|
||||
return NO_NEXT_HOP_PREFERENCE;
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to);
|
||||
if (node && node->next_hop) {
|
||||
@@ -200,7 +184,7 @@ std::optional<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 std::nullopt;
|
||||
return NO_NEXT_HOP_PREFERENCE;
|
||||
}
|
||||
|
||||
PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "FloodingRouter.h"
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
@@ -147,7 +146,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)
|
||||
*/
|
||||
std::optional<uint8_t> getNextHop(NodeNum to, uint8_t relay_node);
|
||||
uint8_t 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 */
|
||||
|
||||
+1
-45
@@ -569,7 +569,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
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;
|
||||
config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED;
|
||||
#else
|
||||
config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT;
|
||||
#endif
|
||||
@@ -910,17 +910,8 @@ void NodeDB::installDefaultModuleConfig()
|
||||
moduleConfig.neighbor_info.enabled = false;
|
||||
|
||||
moduleConfig.has_detection_sensor = true;
|
||||
#ifdef DETECTION_SENSOR_DEFAULT_PIN
|
||||
moduleConfig.detection_sensor.enabled = true;
|
||||
moduleConfig.detection_sensor.monitor_pin = DETECTION_SENSOR_DEFAULT_PIN;
|
||||
moduleConfig.detection_sensor.detection_trigger_type =
|
||||
meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_EITHER_EDGE_ACTIVE_HIGH;
|
||||
moduleConfig.detection_sensor.use_pullup = true;
|
||||
strncpy(moduleConfig.detection_sensor.name, "Tamper", sizeof(moduleConfig.detection_sensor.name));
|
||||
#else
|
||||
moduleConfig.detection_sensor.enabled = false;
|
||||
moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH;
|
||||
#endif
|
||||
moduleConfig.detection_sensor.minimum_broadcast_secs = 45;
|
||||
|
||||
moduleConfig.has_ambient_lighting = true;
|
||||
@@ -1659,25 +1650,6 @@ 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
|
||||
@@ -1715,22 +1687,6 @@ 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)
|
||||
|
||||
@@ -114,27 +114,6 @@ 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,
|
||||
|
||||
@@ -449,11 +449,6 @@ 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);
|
||||
}
|
||||
|
||||
+12
-12
@@ -156,7 +156,7 @@ class RadioInterface
|
||||
virtual ErrorCode send(meshtastic_MeshPacket *p) = 0;
|
||||
|
||||
/** Return TX queue status */
|
||||
[[nodiscard]] virtual meshtastic_QueueStatus getQueueStatus()
|
||||
virtual meshtastic_QueueStatus getQueueStatus()
|
||||
{
|
||||
meshtastic_QueueStatus qs;
|
||||
qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0;
|
||||
@@ -182,22 +182,22 @@ class RadioInterface
|
||||
virtual bool reconfigure();
|
||||
|
||||
/** The delay to use for retransmitting dropped packets */
|
||||
[[nodiscard]] uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p);
|
||||
uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p);
|
||||
|
||||
/** The delay to use when we want to send something */
|
||||
[[nodiscard]] uint32_t getTxDelayMsec();
|
||||
uint32_t getTxDelayMsec();
|
||||
|
||||
/** The CW to use when calculating SNR_based delays */
|
||||
[[nodiscard]] uint8_t getCWsize(float snr);
|
||||
uint8_t getCWsize(float snr);
|
||||
|
||||
/** The worst-case SNR_based packet delay */
|
||||
[[nodiscard]] uint32_t getTxDelayMsecWeightedWorst(float snr);
|
||||
uint32_t getTxDelayMsecWeightedWorst(float snr);
|
||||
|
||||
/** Returns true if we should rebroadcast early like a ROUTER */
|
||||
[[nodiscard]] bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p);
|
||||
bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p);
|
||||
|
||||
/** The delay to use when we want to flood a message. Use a weighted scale based on SNR */
|
||||
[[nodiscard]] uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p);
|
||||
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; }
|
||||
@@ -215,18 +215,18 @@ class RadioInterface
|
||||
*
|
||||
* @return num msecs for the packet
|
||||
*/
|
||||
[[nodiscard]] uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false);
|
||||
[[nodiscard]] virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0;
|
||||
uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false);
|
||||
virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0;
|
||||
|
||||
/**
|
||||
* Get the channel we saved.
|
||||
*/
|
||||
[[nodiscard]] uint32_t getChannelNum();
|
||||
uint32_t getChannelNum();
|
||||
|
||||
/**
|
||||
* Get the frequency we saved.
|
||||
*/
|
||||
[[nodiscard]] virtual float getFreq();
|
||||
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; }
|
||||
@@ -246,7 +246,7 @@ class RadioInterface
|
||||
*
|
||||
* Used as the first step of
|
||||
*/
|
||||
[[nodiscard]] size_t beginSending(meshtastic_MeshPacket *p);
|
||||
size_t beginSending(meshtastic_MeshPacket *p);
|
||||
|
||||
/**
|
||||
* Some regulatory regions limit xmit power.
|
||||
|
||||
+7
-48
@@ -11,9 +11,6 @@
|
||||
#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
|
||||
@@ -98,20 +95,6 @@ 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)
|
||||
@@ -398,6 +381,12 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
if (bleMeshHandler) {
|
||||
bleMeshHandler->onSend(p);
|
||||
}
|
||||
#endif
|
||||
|
||||
assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside)
|
||||
return iface->send(p);
|
||||
}
|
||||
@@ -802,32 +791,8 @@ 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) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
!isFromUs(p) && mqtt)
|
||||
mqtt->onSend(*p_encrypted, *p, p->channel);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -875,12 +840,6 @@ 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);
|
||||
|
||||
+3
-3
@@ -58,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
|
||||
*/
|
||||
[[nodiscard]] meshtastic_MeshPacket *allocForSending();
|
||||
meshtastic_MeshPacket *allocForSending();
|
||||
|
||||
/** Return Underlying interface's TX queue status */
|
||||
[[nodiscard]] meshtastic_QueueStatus getQueueStatus();
|
||||
meshtastic_QueueStatus getQueueStatus();
|
||||
|
||||
/**
|
||||
* @return our local nodenum */
|
||||
[[nodiscard]] NodeNum getNodeNum();
|
||||
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'
|
||||
|
||||
@@ -60,7 +60,7 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
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);
|
||||
loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -141,12 +141,12 @@ bool TransmitHistory::saveToDisk()
|
||||
file.write((uint8_t *)&header, sizeof(header));
|
||||
|
||||
uint8_t written = 0;
|
||||
for (const auto &[key, epochSeconds] : history) {
|
||||
for (auto &pair : history) {
|
||||
if (written >= MAX_ENTRIES)
|
||||
break;
|
||||
Entry entry{};
|
||||
entry.key = key;
|
||||
entry.epochSeconds = epochSeconds;
|
||||
entry.key = pair.first;
|
||||
entry.epochSeconds = pair.second;
|
||||
file.write((uint8_t *)&entry, sizeof(entry));
|
||||
written++;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#include "ServerAPI.h"
|
||||
#include "Throttle.h"
|
||||
#include "configuration.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
static constexpr uint32_t TCP_IDLE_TIMEOUT_MS = 15 * 60 * 1000UL;
|
||||
|
||||
template <typename T>
|
||||
ServerAPI<T>::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client)
|
||||
{
|
||||
@@ -31,12 +28,6 @@ template <typename T> bool ServerAPI<T>::checkIsConnected()
|
||||
template <class T> int32_t ServerAPI<T>::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");
|
||||
@@ -66,7 +57,7 @@ template <class T, class U> int32_t APIServerPort<T, U>::runOnce()
|
||||
#else
|
||||
auto client = U::available();
|
||||
#endif
|
||||
#elif defined(ARCH_RP2040) || defined(ARCH_NRF52)
|
||||
#elif defined(ARCH_RP2040)
|
||||
auto client = U::accept();
|
||||
#else
|
||||
auto client = U::available();
|
||||
|
||||
@@ -17,15 +17,6 @@ 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");
|
||||
|
||||
@@ -24,5 +24,4 @@ class ethServerPort : public APIServerPort<ethServerAPI, EthernetServer>
|
||||
};
|
||||
|
||||
void initApiServer(int port = SERVER_API_DEFAULT_PORT);
|
||||
void deInitApiServer();
|
||||
#endif
|
||||
|
||||
@@ -32,69 +32,6 @@ 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,9 +39,6 @@ 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -229,12 +229,6 @@ typedef struct _meshtastic_SCD30_config {
|
||||
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;
|
||||
@@ -245,9 +239,6 @@ typedef struct _meshtastic_SensorConfig {
|
||||
/* 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;
|
||||
@@ -436,7 +427,6 @@ 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}
|
||||
@@ -445,11 +435,10 @@ 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, false, meshtastic_SCD30_config_init_default, false, meshtastic_SHTXX_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}
|
||||
#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}}}
|
||||
@@ -457,11 +446,10 @@ 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, false, meshtastic_SCD30_config_init_zero, false, meshtastic_SHTXX_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}
|
||||
#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
|
||||
@@ -498,11 +486,9 @@ extern "C" {
|
||||
#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
|
||||
@@ -693,14 +679,12 @@ 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, scd30_config, 3) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, shtxx_config, 4)
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3)
|
||||
#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) \
|
||||
@@ -729,11 +713,6 @@ 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;
|
||||
@@ -745,7 +724,6 @@ 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
|
||||
@@ -759,7 +737,6 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_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
|
||||
@@ -772,8 +749,7 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
|
||||
#define meshtastic_SCD30_config_size 27
|
||||
#define meshtastic_SCD4X_config_size 29
|
||||
#define meshtastic_SEN5X_config_size 7
|
||||
#define meshtastic_SHTXX_config_size 6
|
||||
#define meshtastic_SensorConfig_size 77
|
||||
#define meshtastic_SensorConfig_size 69
|
||||
#define meshtastic_SharedContact_size 127
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -304,8 +304,6 @@ typedef enum _meshtastic_HardwareModel {
|
||||
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,
|
||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
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.
|
||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||
|
||||
@@ -150,10 +150,6 @@ typedef enum _meshtastic_PortNum {
|
||||
arbitrary telemetry over meshtastic that is not covered by telemetry.proto
|
||||
ENCODING: CayenneLLP */
|
||||
meshtastic_PortNum_CAYENNE_APP = 77,
|
||||
/* 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)) */
|
||||
|
||||
@@ -26,7 +26,7 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
meshtastic_TelemetrySensorType_INA219 = 5,
|
||||
/* High accuracy temperature and pressure */
|
||||
meshtastic_TelemetrySensorType_BMP280 = 6,
|
||||
/* TODO - REMOVE High accuracy temperature and humidity */
|
||||
/* 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,
|
||||
/* TODO - REMOVE High accuracy temperature and humidity */
|
||||
/* 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,
|
||||
/* TODO - REMOVE Sensirion High accuracy temperature and humidity */
|
||||
/* 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,
|
||||
@@ -106,14 +106,12 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
meshtastic_TelemetrySensorType_BH1750 = 45,
|
||||
/* HDC1080 Temperature and Humidity Sensor */
|
||||
meshtastic_TelemetrySensorType_HDC1080 = 46,
|
||||
/* TODO - REMOVE STH21 Temperature and R. Humidity sensor */
|
||||
/* 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
|
||||
meshtastic_TelemetrySensorType_SCD30 = 49
|
||||
} meshtastic_TelemetrySensorType;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -491,8 +489,8 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SHTXX
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SHTXX+1))
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SCD30
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SCD30+1))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -69,22 +69,6 @@ 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);
|
||||
@@ -106,4 +90,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)
|
||||
#define is_in_repeated(name, n) is_in_helper(n, name, name##_count)
|
||||
@@ -73,11 +73,6 @@ class UdpMulticastHandler final
|
||||
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;
|
||||
@@ -118,4 +113,4 @@ class UdpMulticastHandler final
|
||||
AsyncUDP udp;
|
||||
bool isRunning;
|
||||
};
|
||||
#endif // HAS_UDP_MULTICAST
|
||||
#endif // HAS_UDP_MULTICAST
|
||||
@@ -800,7 +800,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
||||
#if HAS_LORA_FEM
|
||||
// Apply FEM LNA mode from config (only meaningful on hardware that supports it)
|
||||
if (loraFEMInterface.isLnaCanControl()) {
|
||||
loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED);
|
||||
loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED);
|
||||
} else if (config.lora.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");
|
||||
@@ -1008,11 +1008,6 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
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, shouldReboot);
|
||||
return true;
|
||||
@@ -1198,11 +1193,6 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
|
||||
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:
|
||||
LOG_INFO("Get module config: 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;
|
||||
}
|
||||
|
||||
// NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior.
|
||||
|
||||
+240
-140
@@ -13,12 +13,10 @@
|
||||
#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"
|
||||
@@ -47,6 +45,71 @@ 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<std::pair<bool, String>> tokenizeMessageWithEmotes(const char *msg)
|
||||
{
|
||||
std::vector<std::pair<bool, String>> 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;
|
||||
@@ -201,20 +264,19 @@ int CannedMessageModule::splitConfiguredMessages()
|
||||
}
|
||||
void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer)
|
||||
{
|
||||
(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 : "?");
|
||||
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));
|
||||
}
|
||||
} else {
|
||||
snprintf(header, sizeof(header), "To: @%s", getNodeName(this->dest));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -308,92 +370,6 @@ 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<uint8_t>(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<size_t>(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.
|
||||
@@ -515,16 +491,18 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event)
|
||||
if (event->kbchar != 0x09)
|
||||
return false;
|
||||
|
||||
const cannedMessageModuleRunState targetState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
|
||||
? CANNED_MESSAGE_RUN_STATE_FREETEXT
|
||||
: CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
|
||||
updateState((runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT
|
||||
: CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION);
|
||||
|
||||
destIndex = 0;
|
||||
scrollIndex = 0;
|
||||
if (targetState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
|
||||
// RESTORE THIS!
|
||||
if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
|
||||
updateDestinationSelectionList();
|
||||
|
||||
updateState(targetState, true);
|
||||
updateState((runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT
|
||||
: CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION,
|
||||
true);
|
||||
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
@@ -1708,51 +1686,55 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
|
||||
|
||||
int xOffset = 0;
|
||||
int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset;
|
||||
std::string entryText;
|
||||
char entryText[64] = "";
|
||||
|
||||
// Draw Channels First
|
||||
if (itemIndex < numActiveChannels) {
|
||||
uint8_t channelIndex = this->activeChannelIndices[itemIndex];
|
||||
const char *channelName = channels.getName(channelIndex);
|
||||
entryText = std::string("#") + (channelName ? channelName : "?");
|
||||
snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex));
|
||||
}
|
||||
// Then Draw Nodes
|
||||
else {
|
||||
int nodeIndex = itemIndex - numActiveChannels;
|
||||
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
|
||||
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
|
||||
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;
|
||||
}
|
||||
if (node && node->user.long_name) {
|
||||
strncpy(entryText, node->user.long_name, sizeof(entryText) - 1);
|
||||
entryText[sizeof(entryText) - 1] = '\0';
|
||||
}
|
||||
|
||||
int availWidth = display->getWidth() -
|
||||
((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) -
|
||||
((node && node->is_favorite) ? 10 : 0);
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
char truncatedEntry[96];
|
||||
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
|
||||
availWidth);
|
||||
entryText = truncatedEntry;
|
||||
|
||||
size_t origLen = strlen(entryText);
|
||||
while (entryText[0] && display->getStringWidth(entryText) > availWidth) {
|
||||
entryText[strlen(entryText) - 1] = '\0';
|
||||
}
|
||||
if (strlen(entryText) < origLen) {
|
||||
strcat(entryText, "...");
|
||||
}
|
||||
|
||||
// Prepend "* " if this is a favorite
|
||||
if (node && node->is_favorite) {
|
||||
entryText = "* " + entryText;
|
||||
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);
|
||||
}
|
||||
}
|
||||
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
|
||||
availWidth);
|
||||
entryText = truncatedEntry;
|
||||
}
|
||||
}
|
||||
|
||||
if (entryText.empty() || entryText == "Unknown")
|
||||
entryText = "?";
|
||||
if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0)
|
||||
strcpy(entryText, "?");
|
||||
|
||||
// Highlight background (if selected)
|
||||
if (itemIndex == destIndex) {
|
||||
@@ -1762,7 +1744,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
|
||||
}
|
||||
|
||||
// Draw entry text
|
||||
graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText.c_str(), FONT_HEIGHT_SMALL, 1, false);
|
||||
display->drawString(xOffset + 2, yOffset, entryText);
|
||||
display->setColor(WHITE);
|
||||
|
||||
// Draw key icon (after highlight)
|
||||
@@ -1803,10 +1785,15 @@ 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 = 4;
|
||||
const int labelGap = 6;
|
||||
const int bitmapGapX = 4;
|
||||
|
||||
const int maxEmoteHeight = graphics::EmoteRenderer::maxEmoteHeight();
|
||||
// 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 rowHeight = maxEmoteHeight + 2;
|
||||
|
||||
// Place header at top, then compute start of emote list
|
||||
@@ -1853,16 +1840,14 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
// 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 bitmap (left), 1px margin from highlight bar top
|
||||
int emoteY = rowY + 1;
|
||||
display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap);
|
||||
|
||||
// Emote label (right of bitmap)
|
||||
display->setFont(FONT_MEDIUM);
|
||||
int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2);
|
||||
display->drawString(labelStartX + labelGap, labelY, emote.label);
|
||||
display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label);
|
||||
|
||||
if (emoteIdx == emotePickerIndex)
|
||||
display->setColor(WHITE);
|
||||
@@ -2022,7 +2007,91 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
{
|
||||
int inputY = 0 + y + FONT_HEIGHT_SMALL;
|
||||
String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor);
|
||||
drawWrappedEmoteText(display, x, inputY, msgWithCursor.c_str(), display->getWidth() - x, FONT_HEIGHT_SMALL);
|
||||
|
||||
// Tokenize input into (isEmote, token) pairs
|
||||
const char *msg = msgWithCursor.c_str();
|
||||
std::vector<std::pair<bool, String>> tokens = tokenizeMessageWithEmotes(msg);
|
||||
|
||||
// Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed)
|
||||
std::vector<std::vector<std::pair<bool, String>>> lines;
|
||||
std::vector<std::pair<bool, String>> 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<int>(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 (const 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;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
@@ -2037,6 +2106,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
const int baseRowSpacing = FONT_HEIGHT_SMALL - 4;
|
||||
|
||||
int topMsg;
|
||||
std::vector<int> rowHeights;
|
||||
int _visibleRows;
|
||||
|
||||
// Draw header (To: ...)
|
||||
@@ -2052,15 +2122,36 @@ 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 = getRowHeightForEmoteText(msg, baseRowSpacing);
|
||||
int rowHeight = rowHeights[vis];
|
||||
bool _highlight = (msgIdx == currentMessageIndex);
|
||||
|
||||
// Multi-emote tokenization
|
||||
std::vector<std::pair<bool, String>> tokens = tokenizeMessageWithEmotes(msg);
|
||||
|
||||
// Vertically center based on rowHeight
|
||||
int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
|
||||
@@ -2077,8 +2168,17 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
int nextX = x + (_highlight ? 2 : 0);
|
||||
#endif
|
||||
|
||||
if (msg && *msg)
|
||||
drawCenteredEmoteText(display, nextX, lineY, rowHeight, msg);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
#ifndef USE_EINK
|
||||
if (_highlight)
|
||||
display->setColor(WHITE);
|
||||
|
||||
@@ -64,11 +64,6 @@ int32_t DetectionSensorModule::runOnce()
|
||||
if (moduleConfig.detection_sensor.enabled == false)
|
||||
return disable();
|
||||
|
||||
#ifdef DETECTION_SENSOR_DEFAULT_PIN
|
||||
if (moduleConfig.detection_sensor.monitor_pin == 0)
|
||||
moduleConfig.detection_sensor.monitor_pin = DETECTION_SENSOR_DEFAULT_PIN;
|
||||
#endif
|
||||
|
||||
if (firstTime) {
|
||||
|
||||
#ifdef DETECTION_SENSOR_EN
|
||||
|
||||
@@ -38,9 +38,6 @@
|
||||
#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"
|
||||
@@ -123,14 +120,6 @@ void setupModules()
|
||||
#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
|
||||
adminModule = new AdminModule();
|
||||
#endif
|
||||
|
||||
@@ -467,11 +467,7 @@ int32_t PositionModule::runOnce()
|
||||
if (smartPosition.hasTraveledOverThreshold &&
|
||||
Throttle::execute(
|
||||
&lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); },
|
||||
[]() {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Skip send smart broadcast due to time throttling");
|
||||
#endif
|
||||
})) {
|
||||
[]() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) {
|
||||
|
||||
LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, "
|
||||
"minTimeInterval=%ims)",
|
||||
@@ -563,11 +559,7 @@ void PositionModule::handleNewPosition()
|
||||
if (smartPosition.hasTraveledOverThreshold &&
|
||||
Throttle::execute(
|
||||
&lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); },
|
||||
[]() {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Skip send smart broadcast due to time throttling");
|
||||
#endif
|
||||
})) {
|
||||
[]() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) {
|
||||
LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, "
|
||||
"minTimeInterval=%ims)",
|
||||
localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend,
|
||||
|
||||
@@ -23,7 +23,7 @@ bool SEN5XSensor::getVersion()
|
||||
}
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t versionBuffer[12]{};
|
||||
uint8_t versionBuffer[12];
|
||||
size_t charNumber = readBuffer(&versionBuffer[0], 3);
|
||||
if (charNumber == 0) {
|
||||
LOG_ERROR("SEN5X: Error getting data ready flag value");
|
||||
@@ -638,7 +638,7 @@ bool SEN5XSensor::readValues()
|
||||
LOG_DEBUG("SEN5X: Reading PM Values");
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t dataBuffer[16]{};
|
||||
uint8_t dataBuffer[16];
|
||||
size_t receivedNumber = readBuffer(&dataBuffer[0], 24);
|
||||
if (receivedNumber == 0) {
|
||||
LOG_ERROR("SEN5X: Error getting values");
|
||||
@@ -691,7 +691,7 @@ bool SEN5XSensor::readPNValues(bool cumulative)
|
||||
LOG_DEBUG("SEN5X: Reading PN Values");
|
||||
delay(20); // From Sensirion Datasheet
|
||||
|
||||
uint8_t dataBuffer[20]{};
|
||||
uint8_t dataBuffer[20];
|
||||
size_t receivedNumber = readBuffer(&dataBuffer[0], 30);
|
||||
if (receivedNumber == 0) {
|
||||
LOG_ERROR("SEN5X: Error getting PN values");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,434 +0,0 @@
|
||||
#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<uint16_t>((1u << kNodeInfoTagBits) - 1u);
|
||||
static constexpr uint16_t kNodeInfoIndexSlotsRaw =
|
||||
static_cast<uint16_t>((kNodeInfoIndexMetadataBudgetBytes * 8u) / kNodeInfoTagBits);
|
||||
static constexpr uint16_t kNodeInfoIndexSlots =
|
||||
static_cast<uint16_t>(kNodeInfoIndexSlotsRaw - (kNodeInfoIndexSlotsRaw % kNodeInfoBucketSize));
|
||||
static constexpr uint16_t kNodeInfoTargetEntries =
|
||||
static_cast<uint16_t>((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<uint16_t>(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<uint8_t>(ticks);
|
||||
}
|
||||
uint32_t fromRelativeTime(uint8_t ticks, uint16_t resolutionSecs) const
|
||||
{
|
||||
return cacheEpochMs + (static_cast<uint32_t>(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<uint8_t>(1 + log2Floor(static_cast<uint16_t>(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<uint8_t>(1 + log2Floor(static_cast<uint16_t>(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<uint16_t>(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
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "ICM20948Sensor.h"
|
||||
#include "LIS3DHSensor.h"
|
||||
#include "LSM6DS3Sensor.h"
|
||||
#include "LSM6DSOSensor.h"
|
||||
#include "MPU6050Sensor.h"
|
||||
#include "MotionSensor.h"
|
||||
#ifdef HAS_QMA6100P
|
||||
@@ -104,9 +103,6 @@ class AccelerometerThread : public concurrency::OSThread
|
||||
case ScanI2C::DeviceType::LSM6DS3:
|
||||
sensor = new LSM6DS3Sensor(device);
|
||||
break;
|
||||
case ScanI2C::DeviceType::LSM6DSOX:
|
||||
sensor = new LSM6DSOSensor(device);
|
||||
break;
|
||||
#ifdef HAS_STK8XXX
|
||||
case ScanI2C::DeviceType::STK8BAXX:
|
||||
sensor = new STK8XXXSensor(device);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#include "LSM6DSOSensor.h"
|
||||
#include "NodeDB.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_LSM6DSOX.h>)
|
||||
|
||||
LSM6DSOSensor::LSM6DSOSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
|
||||
|
||||
bool LSM6DSOSensor::init()
|
||||
{
|
||||
if (sensor.begin_I2C(deviceAddress())) {
|
||||
|
||||
sensor.setAccelRange(LSM6DS_ACCEL_RANGE_2_G);
|
||||
sensor.enableWakeup(config.display.wake_on_tap_or_motion, 1, LSM6DSO_WAKE_THRESH);
|
||||
|
||||
LOG_DEBUG("LSM6DSO init ok");
|
||||
return true;
|
||||
}
|
||||
LOG_DEBUG("LSM6DSO init failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t LSM6DSOSensor::runOnce()
|
||||
{
|
||||
if (sensor.shake()) {
|
||||
wakeScreen();
|
||||
return 500;
|
||||
}
|
||||
return MOTION_SENSOR_CHECK_INTERVAL_MS;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
#ifndef _LSM6DSO_SENSOR_H_
|
||||
#define _LSM6DSO_SENSOR_H_
|
||||
|
||||
#include "MotionSensor.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_LSM6DSOX.h>)
|
||||
|
||||
#ifndef LSM6DSO_WAKE_THRESH
|
||||
#define LSM6DSO_WAKE_THRESH 20
|
||||
#endif
|
||||
|
||||
#include <Adafruit_LSM6DSOX.h>
|
||||
|
||||
class LSM6DSOSensor : public MotionSensor
|
||||
{
|
||||
private:
|
||||
Adafruit_LSM6DSOX sensor;
|
||||
|
||||
public:
|
||||
explicit LSM6DSOSensor(ScanI2C::FoundDevice foundDevice);
|
||||
virtual bool init() override;
|
||||
virtual int32_t runOnce() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+2
-5
@@ -300,9 +300,7 @@ struct PubSubConfig {
|
||||
if (config.tls_enabled) {
|
||||
serverPort = 8883;
|
||||
}
|
||||
auto [parsedServerAddr, parsedServerPort] = parseHostAndPort(serverAddr.c_str(), serverPort);
|
||||
serverAddr = std::move(parsedServerAddr);
|
||||
serverPort = parsedServerPort;
|
||||
std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort);
|
||||
}
|
||||
|
||||
// Defaults
|
||||
@@ -443,8 +441,7 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE)
|
||||
moduleConfig.mqtt.map_report_settings.publish_interval_secs, default_map_publish_interval_secs);
|
||||
}
|
||||
|
||||
auto [host, parsedPort] = parseHostAndPort(moduleConfig.mqtt.address);
|
||||
(void)parsedPort;
|
||||
String host = parseHostAndPort(moduleConfig.mqtt.address).first;
|
||||
isConfiguredForDefaultServer = isDefaultServer(host);
|
||||
IPAddress ip;
|
||||
isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip);
|
||||
|
||||
@@ -10,11 +10,17 @@
|
||||
#include "mesh/PhoneAPI.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include "sleep.h"
|
||||
#include <NimBLEAdvertising.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include <Preferences.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
#ifdef ARCH_ESP32
|
||||
#include <nvs.h>
|
||||
#endif
|
||||
|
||||
#if defined(NIMBLE_TWO) || (defined(CONFIG_BT_NIMBLE_EXT_ADV) && CONFIG_BT_NIMBLE_EXT_ADV)
|
||||
#include "NimBLEAdvertising.h"
|
||||
#include "NimBLEExtAdvertising.h"
|
||||
#include "PowerStatus.h"
|
||||
@@ -731,7 +737,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
// Restart Advertising
|
||||
ble->startAdvertising();
|
||||
#else
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
auto *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
if (!pAdvertising->start(0)) {
|
||||
if (pAdvertising->isAdvertising()) {
|
||||
LOG_DEBUG("BLE advertising already running");
|
||||
@@ -746,13 +752,48 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
static NimbleBluetoothToRadioCallback *toRadioCallbacks;
|
||||
static NimbleBluetoothFromRadioCallback *fromRadioCallbacks;
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
static void clearCorruptBondStoreOnce()
|
||||
{
|
||||
Preferences prefs;
|
||||
if (!prefs.begin("meshtastic", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool alreadyCleared = prefs.getBool("nimbleBondClr", false);
|
||||
if (alreadyCleared) {
|
||||
prefs.end();
|
||||
return;
|
||||
}
|
||||
|
||||
nvs_handle_t nimbleHandle;
|
||||
esp_err_t err = nvs_open("nimble_bond", NVS_READWRITE, &nimbleHandle);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_erase_all(nimbleHandle);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(nimbleHandle);
|
||||
}
|
||||
nvs_close(nimbleHandle);
|
||||
|
||||
if (err == ESP_OK) {
|
||||
LOG_WARN("Cleared NimBLE bond database from NVS (one-time recovery)");
|
||||
} else {
|
||||
LOG_WARN("Failed clearing NimBLE bond database, err=%d", err);
|
||||
}
|
||||
}
|
||||
|
||||
prefs.putBool("nimbleBondClr", true);
|
||||
prefs.end();
|
||||
}
|
||||
#endif
|
||||
|
||||
void NimbleBluetooth::shutdown()
|
||||
{
|
||||
// No measurable power saving for ESP32 during light-sleep(?)
|
||||
#ifndef ARCH_ESP32
|
||||
// Shutdown bluetooth for minimum power draw
|
||||
LOG_INFO("Disable bluetooth");
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
auto *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->reset();
|
||||
pAdvertising->stop();
|
||||
#endif
|
||||
@@ -825,6 +866,11 @@ void NimbleBluetooth::setup()
|
||||
|
||||
LOG_INFO("Init the NimBLE bluetooth module");
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Recovery for units that carry corrupted bond blobs that crash NimBLE during populate_db_from_nvs.
|
||||
clearCorruptBondStoreOnce();
|
||||
#endif
|
||||
|
||||
NimBLEDevice::init(getDeviceName());
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
|
||||
@@ -867,6 +913,12 @@ void NimbleBluetooth::setup()
|
||||
bleServer->setCallbacks(serverCallbacks, true);
|
||||
setupService();
|
||||
startAdvertising();
|
||||
|
||||
#if HAS_BLE_MESH
|
||||
if (bleMeshHandler) {
|
||||
bleMeshHandler->onBluetoothReady();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void NimbleBluetooth::setupService()
|
||||
@@ -922,8 +974,12 @@ void NimbleBluetooth::setupService()
|
||||
|
||||
void NimbleBluetooth::startAdvertising()
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
#if defined(NIMBLE_TWO) || (defined(CONFIG_BT_NIMBLE_EXT_ADV) && CONFIG_BT_NIMBLE_EXT_ADV)
|
||||
// NimBLE-Arduino 1.4.x can leave ext-adv callbacks uninitialized.
|
||||
// Explicitly install a default callback sink before starting any instance.
|
||||
static NimBLEExtAdvertisingCallbacks extAdvCallbacks;
|
||||
NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->setCallbacks(&extAdvCallbacks, false);
|
||||
NimBLEExtAdvertisement legacyAdvertising;
|
||||
|
||||
legacyAdvertising.setLegacyAdvertising(true);
|
||||
@@ -950,7 +1006,7 @@ void NimbleBluetooth::startAdvertising()
|
||||
LOG_ERROR("BLE failed to start legacyAdvertising");
|
||||
}
|
||||
#else
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
auto *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->reset();
|
||||
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
|
||||
pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_BLE_MESH && defined(ARCH_ESP32)
|
||||
|
||||
#include "ESP32BLEMesh.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Router.h"
|
||||
|
||||
#if defined(CONFIG_NIMBLE_CPP_IDF)
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_hs_adv.h"
|
||||
#else
|
||||
#include "nimble/nimble/host/include/host/ble_gap.h"
|
||||
#include "nimble/nimble/host/include/host/ble_hs_adv.h"
|
||||
#endif
|
||||
|
||||
void ESP32BLEMesh::start()
|
||||
{
|
||||
if (isRunning) {
|
||||
LOG_DEBUG("BLE mesh already running");
|
||||
return;
|
||||
}
|
||||
|
||||
memset(peers, 0, sizeof(peers));
|
||||
peerCount = 0;
|
||||
bluetoothReady = false;
|
||||
isRunning = true;
|
||||
LOG_INFO("BLE mesh started (waiting for Bluetooth ready)");
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::onBluetoothReady()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
if (bluetoothReady)
|
||||
return;
|
||||
|
||||
bluetoothReady = true;
|
||||
startScanning();
|
||||
LOG_DEBUG("BLE mesh Bluetooth ready");
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::stop()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
stopScanning();
|
||||
isRunning = false;
|
||||
LOG_INFO("BLE mesh stopped");
|
||||
}
|
||||
|
||||
bool ESP32BLEMesh::onSend(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
if (!isRunning || !mp)
|
||||
return false;
|
||||
|
||||
if (!bluetoothReady) {
|
||||
LOG_DEBUG("BLE mesh TX deferred, Bluetooth not ready");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't echo packets that arrived via BLE mesh
|
||||
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH) {
|
||||
LOG_DEBUG("Drop BLE mesh echo");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t buffer[meshtastic_MeshPacket_size];
|
||||
size_t encodedLen = encodeForBLE(mp, buffer, sizeof(buffer));
|
||||
if (encodedLen == 0) {
|
||||
LOG_WARN("BLE mesh encode failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build manufacturer data: company ID (2 bytes) + version (1 byte) + protobuf payload
|
||||
size_t mfgDataLen = 2 + 1 + encodedLen;
|
||||
|
||||
// Extended advertising can carry up to 255 bytes total.
|
||||
// Reserve 5 bytes for the flags and manufacturer-data AD headers.
|
||||
if (mfgDataLen > 250) {
|
||||
LOG_WARN("BLE mesh packet too large: %u bytes", mfgDataLen);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t mfgData[250];
|
||||
mfgData[0] = (uint8_t)(BLE_MESH_COMPANY_ID & 0xFF);
|
||||
mfgData[1] = (uint8_t)((BLE_MESH_COMPANY_ID >> 8) & 0xFF);
|
||||
mfgData[2] = BLE_MESH_PROTOCOL_VERSION;
|
||||
memcpy(&mfgData[3], buffer, encodedLen);
|
||||
|
||||
// Stop scanning while we advertise
|
||||
stopScanning();
|
||||
|
||||
// Build raw advertisement data
|
||||
uint8_t advBuf[251];
|
||||
size_t advLen = 0;
|
||||
|
||||
// Flags AD structure (3 bytes)
|
||||
advBuf[advLen++] = 2; // length
|
||||
advBuf[advLen++] = BLE_HS_ADV_TYPE_FLAGS;
|
||||
advBuf[advLen++] = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
// Manufacturer data AD structure
|
||||
advBuf[advLen++] = (uint8_t)(mfgDataLen + 1); // length (type byte + data)
|
||||
advBuf[advLen++] = BLE_HS_ADV_TYPE_MFG_DATA;
|
||||
memcpy(&advBuf[advLen], mfgData, mfgDataLen);
|
||||
advLen += mfgDataLen;
|
||||
|
||||
const bool priorityRetries = shouldUsePriorityRetries(mp);
|
||||
const uint8_t extraRetries = priorityRetries ? BLE_MESH_PRIORITY_EXTRA_RETRIES : 0;
|
||||
bool sentAny = false;
|
||||
|
||||
for (uint8_t attempt = 0; attempt <= extraRetries; ++attempt) {
|
||||
if (attempt > 0) {
|
||||
const uint16_t retryDelayMs =
|
||||
random(BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS, BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS + 1);
|
||||
delay(retryDelayMs);
|
||||
}
|
||||
|
||||
if (sendAdvertisementBurst(advBuf, advLen, mp->id, encodedLen)) {
|
||||
sentAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Resume scanning
|
||||
startScanning();
|
||||
|
||||
return sentAny;
|
||||
}
|
||||
|
||||
bool ESP32BLEMesh::sendAdvertisementBurst(const uint8_t *advBuf, size_t advLen, PacketId packetId, size_t encodedLen)
|
||||
{
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
// Use extended advertising if available (ESP32-S3, C3, C6)
|
||||
struct ble_gap_ext_adv_params extAdvParams = {};
|
||||
extAdvParams.connectable = 0;
|
||||
extAdvParams.scannable = 0;
|
||||
extAdvParams.directed = 0;
|
||||
extAdvParams.high_duty_directed = 0;
|
||||
extAdvParams.legacy_pdu = 0;
|
||||
extAdvParams.anonymous = 0;
|
||||
extAdvParams.include_tx_power = 0;
|
||||
extAdvParams.scan_req_notif = 0;
|
||||
extAdvParams.itvl_min = BLE_MESH_ADV_INTERVAL;
|
||||
extAdvParams.itvl_max = BLE_MESH_ADV_INTERVAL;
|
||||
extAdvParams.channel_map = 0; // all channels
|
||||
extAdvParams.own_addr_type = BLE_OWN_ADDR_PUBLIC;
|
||||
extAdvParams.primary_phy = BLE_HCI_LE_PHY_1M;
|
||||
extAdvParams.secondary_phy = BLE_HCI_LE_PHY_1M;
|
||||
extAdvParams.tx_power = 127; // host picks max
|
||||
|
||||
// Use instance 1 to avoid conflicting with phone advertising on instance 0
|
||||
const uint8_t meshAdvInstance = 1;
|
||||
int8_t selectedTxPower = 0;
|
||||
|
||||
int rc = ble_gap_ext_adv_configure(meshAdvInstance, &extAdvParams, &selectedTxPower, onGapEvent, this);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh ext adv configure failed: %d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct os_mbuf *advData = os_msys_get_pkthdr(advLen, 0);
|
||||
if (!advData) {
|
||||
LOG_WARN("BLE mesh: failed to allocate mbuf");
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return false;
|
||||
}
|
||||
rc = os_mbuf_append(advData, advBuf, advLen);
|
||||
if (rc != 0) {
|
||||
os_mbuf_free_chain(advData);
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return false;
|
||||
}
|
||||
|
||||
rc = ble_gap_ext_adv_set_data(meshAdvInstance, advData);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh ext adv set data failed: %d", rc);
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Advertise for BLE_MESH_ADV_COUNT * 100ms, then stop.
|
||||
rc = ble_gap_ext_adv_start(meshAdvInstance, BLE_MESH_ADV_COUNT, 0);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh ext adv start failed: %d", rc);
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("BLE mesh adv sent (id=%u, len=%u)", packetId, encodedLen);
|
||||
delay(BLE_MESH_ADV_BURST_MS);
|
||||
ble_gap_ext_adv_stop(meshAdvInstance);
|
||||
ble_gap_ext_adv_remove(meshAdvInstance);
|
||||
return true;
|
||||
#else
|
||||
// Legacy advertising fallback (ESP32 classic - BLE 4.2, limited to 31 bytes)
|
||||
if (advLen > 31) {
|
||||
LOG_WARN("BLE mesh packet too large for legacy adv: %u bytes", advLen);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ble_gap_adv_params legacyAdvParams = {};
|
||||
legacyAdvParams.conn_mode = BLE_GAP_CONN_MODE_NON;
|
||||
legacyAdvParams.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
legacyAdvParams.itvl_min = BLE_MESH_ADV_INTERVAL;
|
||||
legacyAdvParams.itvl_max = BLE_MESH_ADV_INTERVAL;
|
||||
legacyAdvParams.channel_map = 0; // all channels
|
||||
|
||||
int rc = ble_gap_adv_set_data(advBuf, advLen);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh legacy adv set data failed: %d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32_t durationMs = BLE_MESH_ADV_BURST_MS;
|
||||
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, durationMs / 10, &legacyAdvParams, onGapEvent, this);
|
||||
if (rc != 0) {
|
||||
LOG_WARN("BLE mesh legacy adv start failed: %d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("BLE mesh adv sent (id=%u, len=%u)", packetId, encodedLen);
|
||||
delay(durationMs);
|
||||
ble_gap_adv_stop();
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ESP32BLEMesh::shouldUsePriorityRetries(const meshtastic_MeshPacket *mp) const
|
||||
{
|
||||
if (!mp)
|
||||
return false;
|
||||
|
||||
// Prioritize retries for ack-sensitive traffic and direct messages only.
|
||||
const bool isDirect = (mp->to != 0) && !isBroadcast(mp->to);
|
||||
return mp->want_ack || isDirect;
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::startScanning()
|
||||
{
|
||||
if (!bluetoothReady)
|
||||
return;
|
||||
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
struct ble_gap_ext_disc_params uncodedParams = {};
|
||||
uncodedParams.itvl = BLE_MESH_SCAN_INTERVAL;
|
||||
uncodedParams.window = BLE_MESH_SCAN_WINDOW;
|
||||
uncodedParams.passive = 1;
|
||||
|
||||
int rc = ble_gap_ext_disc(BLE_OWN_ADDR_PUBLIC,
|
||||
0, // duration 0 => forever
|
||||
0, // period
|
||||
0, // no duplicate filtering
|
||||
BLE_HCI_SCAN_FILT_NO_WL,
|
||||
0, // not limited
|
||||
&uncodedParams, NULL, onGapEvent, this);
|
||||
#else
|
||||
struct ble_gap_disc_params scanParams = {};
|
||||
scanParams.passive = 1; // Passive scan to save power
|
||||
scanParams.itvl = BLE_MESH_SCAN_INTERVAL;
|
||||
scanParams.window = BLE_MESH_SCAN_WINDOW;
|
||||
scanParams.filter_duplicates = 0; // We want to see repeated mesh advertisements
|
||||
scanParams.limited = 0;
|
||||
scanParams.filter_policy = BLE_HCI_SCAN_FILT_NO_WL;
|
||||
|
||||
int rc = ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, &scanParams, onGapEvent, this);
|
||||
#endif
|
||||
if (rc == 0) {
|
||||
LOG_DEBUG("BLE mesh scanning started");
|
||||
} else if (rc == BLE_HS_EALREADY) {
|
||||
LOG_DEBUG("BLE mesh scanning already active");
|
||||
} else {
|
||||
LOG_WARN("BLE mesh scan start failed: %d", rc);
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::stopScanning()
|
||||
{
|
||||
if (!bluetoothReady)
|
||||
return;
|
||||
|
||||
ble_gap_disc_cancel();
|
||||
}
|
||||
|
||||
int ESP32BLEMesh::onGapEvent(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
auto *self = static_cast<ESP32BLEMesh *>(arg);
|
||||
if (!self)
|
||||
return 0;
|
||||
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_DISC:
|
||||
self->handleAdvertisement(&event->disc);
|
||||
break;
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
case BLE_GAP_EVENT_EXT_DISC:
|
||||
self->handleExtendedAdvertisement(&event->ext_disc);
|
||||
break;
|
||||
#endif
|
||||
case BLE_GAP_EVENT_DISC_COMPLETE:
|
||||
// Scanning timed out or was stopped - restart if still running
|
||||
if (self->isRunning) {
|
||||
self->startScanning();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::handleAdvertisement(const struct ble_gap_disc_desc *desc)
|
||||
{
|
||||
if (!isRunning || !desc)
|
||||
return;
|
||||
|
||||
handleAdvertisementData(desc->addr, desc->rssi, desc->data, desc->length_data);
|
||||
}
|
||||
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
void ESP32BLEMesh::handleExtendedAdvertisement(const struct ble_gap_ext_disc_desc *desc)
|
||||
{
|
||||
if (!isRunning || !desc)
|
||||
return;
|
||||
|
||||
if (desc->data_status != BLE_GAP_EXT_ADV_DATA_STATUS_COMPLETE || !desc->data)
|
||||
return;
|
||||
|
||||
handleAdvertisementData(desc->addr, desc->rssi, desc->data, desc->length_data);
|
||||
}
|
||||
#endif
|
||||
|
||||
void ESP32BLEMesh::handleAdvertisementData(const ble_addr_t &addr, int8_t rssi, const uint8_t *data, uint8_t len)
|
||||
{
|
||||
if (!isRunning || !data)
|
||||
return;
|
||||
|
||||
// Parse advertisement data to find manufacturer-specific data
|
||||
uint16_t offset = 0;
|
||||
|
||||
while (offset < len) {
|
||||
uint8_t adLen = data[offset];
|
||||
if (adLen == 0 || offset + adLen >= len)
|
||||
break;
|
||||
|
||||
uint8_t adType = data[offset + 1];
|
||||
if (adType == BLE_HS_ADV_TYPE_MFG_DATA && adLen >= 4) {
|
||||
// Check company ID
|
||||
uint16_t companyId = data[offset + 2] | (data[offset + 3] << 8);
|
||||
if (companyId == BLE_MESH_COMPANY_ID) {
|
||||
// Check protocol version
|
||||
uint8_t version = data[offset + 4];
|
||||
if (version == BLE_MESH_PROTOCOL_VERSION) {
|
||||
// Extract protobuf payload (skip company ID + version = 3 bytes)
|
||||
const uint8_t *payload = &data[offset + 5];
|
||||
size_t payloadLen = adLen - 4; // adLen includes type byte, subtract type + company(2) + version(1)
|
||||
|
||||
LOG_DEBUG("BLE mesh RX: rssi=%d, len=%u", rssi, payloadLen);
|
||||
updatePeer(addr, rssi);
|
||||
deliverToRouter(payload, payloadLen);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += adLen + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::updatePeer(const ble_addr_t &addr, int8_t rssi)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
|
||||
// Check if peer already known
|
||||
for (uint8_t i = 0; i < peerCount; i++) {
|
||||
if (memcmp(&peers[i].addr, &addr, sizeof(ble_addr_t)) == 0) {
|
||||
peers[i].rssi = rssi;
|
||||
peers[i].lastSeenMs = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new peer
|
||||
if (peerCount < BLE_MESH_MAX_PEERS) {
|
||||
peers[peerCount].addr = addr;
|
||||
peers[peerCount].rssi = rssi;
|
||||
peers[peerCount].lastSeenMs = now;
|
||||
peers[peerCount].nodeNum = 0;
|
||||
peerCount++;
|
||||
LOG_DEBUG("BLE mesh new peer (%u total)", peerCount);
|
||||
} else {
|
||||
// Replace oldest peer
|
||||
pruneStale();
|
||||
if (peerCount < BLE_MESH_MAX_PEERS) {
|
||||
peers[peerCount].addr = addr;
|
||||
peers[peerCount].rssi = rssi;
|
||||
peers[peerCount].lastSeenMs = now;
|
||||
peers[peerCount].nodeNum = 0;
|
||||
peerCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLEMesh::pruneStale()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint8_t writeIdx = 0;
|
||||
|
||||
for (uint8_t i = 0; i < peerCount; i++) {
|
||||
if (now - peers[i].lastSeenMs < BLE_MESH_PEER_TIMEOUT_MS) {
|
||||
if (writeIdx != i)
|
||||
peers[writeIdx] = peers[i];
|
||||
writeIdx++;
|
||||
}
|
||||
}
|
||||
peerCount = writeIdx;
|
||||
}
|
||||
|
||||
#endif // HAS_BLE_MESH && ARCH_ESP32
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#if HAS_BLE_MESH && defined(ARCH_ESP32)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
|
||||
#if defined(CONFIG_NIMBLE_CPP_IDF)
|
||||
#include "host/ble_gap.h"
|
||||
#else
|
||||
#include "nimble/nimble/host/include/host/ble_gap.h"
|
||||
#endif
|
||||
|
||||
// Max number of BLE mesh peers we can track
|
||||
#ifndef BLE_MESH_MAX_PEERS
|
||||
#define BLE_MESH_MAX_PEERS 8
|
||||
#endif
|
||||
|
||||
// Scan interval and window in units of 0.625ms
|
||||
// Default: continuous scan (100% duty) to maximize packet capture reliability.
|
||||
// For lower power builds, these can be overridden in build flags.
|
||||
#ifndef BLE_MESH_SCAN_INTERVAL
|
||||
#define BLE_MESH_SCAN_INTERVAL 160 // 100ms
|
||||
#endif
|
||||
#ifndef BLE_MESH_SCAN_WINDOW
|
||||
#define BLE_MESH_SCAN_WINDOW 160 // 100ms
|
||||
#endif
|
||||
|
||||
// Advertising interval for mesh data in units of 0.625ms
|
||||
#ifndef BLE_MESH_ADV_INTERVAL
|
||||
#define BLE_MESH_ADV_INTERVAL 160 // 100ms
|
||||
#endif
|
||||
|
||||
// How many times to repeat a mesh advertisement
|
||||
#ifndef BLE_MESH_ADV_COUNT
|
||||
#define BLE_MESH_ADV_COUNT 3
|
||||
#endif
|
||||
|
||||
#ifndef BLE_MESH_ADV_BURST_MS
|
||||
#define BLE_MESH_ADV_BURST_MS (BLE_MESH_ADV_COUNT * 100)
|
||||
#endif
|
||||
|
||||
// Extra retry bursts for higher-priority traffic only.
|
||||
// Base burst always runs once for every packet.
|
||||
#ifndef BLE_MESH_PRIORITY_EXTRA_RETRIES
|
||||
#define BLE_MESH_PRIORITY_EXTRA_RETRIES 2
|
||||
#endif
|
||||
|
||||
// Randomized delay range between extra retry bursts in milliseconds.
|
||||
#ifndef BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS
|
||||
#define BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS 50
|
||||
#endif
|
||||
#ifndef BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS
|
||||
#define BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS 200
|
||||
#endif
|
||||
|
||||
// How long before a peer is considered stale (ms)
|
||||
#ifndef BLE_MESH_PEER_TIMEOUT_MS
|
||||
#define BLE_MESH_PEER_TIMEOUT_MS 300000 // 5 minutes
|
||||
#endif
|
||||
|
||||
class ESP32BLEMesh : public BLEMeshHandler
|
||||
{
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
void onBluetoothReady() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
|
||||
private:
|
||||
// Scanning
|
||||
void startScanning();
|
||||
void stopScanning();
|
||||
static int onGapEvent(struct ble_gap_event *event, void *arg);
|
||||
void handleAdvertisement(const struct ble_gap_disc_desc *desc);
|
||||
#if MYNEWT_VAL(BLE_EXT_ADV)
|
||||
void handleExtendedAdvertisement(const struct ble_gap_ext_disc_desc *desc);
|
||||
#endif
|
||||
void handleAdvertisementData(const ble_addr_t &addr, int8_t rssi, const uint8_t *data, uint8_t len);
|
||||
bool sendAdvertisementBurst(const uint8_t *advBuf, size_t advLen, PacketId packetId, size_t encodedLen);
|
||||
bool shouldUsePriorityRetries(const meshtastic_MeshPacket *mp) const;
|
||||
|
||||
// Peer tracking
|
||||
struct BLEMeshPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
};
|
||||
BLEMeshPeer peers[BLE_MESH_MAX_PEERS];
|
||||
uint8_t peerCount = 0;
|
||||
bool bluetoothReady = false;
|
||||
void updatePeer(const ble_addr_t &addr, int8_t rssi);
|
||||
void pruneStale();
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH && ARCH_ESP32
|
||||
@@ -0,0 +1,340 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_BLE_MESH && defined(ARCH_NRF52)
|
||||
|
||||
#include "NRF52BLEMesh.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Router.h"
|
||||
#include <bluefruit.h>
|
||||
|
||||
NRF52BLEMesh *NRF52BLEMesh::instance = nullptr;
|
||||
|
||||
static uint8_t bleMeshScanBuffer[BLE_GAP_SCAN_BUFFER_EXTENDED_MAX_SUPPORTED];
|
||||
static ble_data_t bleMeshScanReportData = {.p_data = bleMeshScanBuffer, .len = sizeof(bleMeshScanBuffer)};
|
||||
static ble_gap_scan_params_t bleMeshScanParams = {
|
||||
.extended = 1,
|
||||
.report_incomplete_evts = 0,
|
||||
.active = 0,
|
||||
.filter_policy = BLE_GAP_SCAN_FP_ACCEPT_ALL,
|
||||
.scan_phys = BLE_GAP_PHY_1MBPS,
|
||||
.interval = BLE_MESH_SCAN_INTERVAL,
|
||||
.window = BLE_MESH_SCAN_WINDOW,
|
||||
.timeout = 0,
|
||||
.channel_mask = {0, 0, 0, 0, 0},
|
||||
};
|
||||
|
||||
void NRF52BLEMesh::start()
|
||||
{
|
||||
if (isRunning) {
|
||||
LOG_DEBUG("BLE mesh already running");
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
memset(peers, 0, sizeof(peers));
|
||||
peerCount = 0;
|
||||
|
||||
isRunning = true;
|
||||
LOG_INFO("BLE mesh started");
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::onBluetoothReady()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
LOG_DEBUG("BLE mesh Bluetooth ready");
|
||||
Bluefruit.setEventCallback(onBleEvent);
|
||||
startScanning();
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::stop()
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
stopScanning();
|
||||
isRunning = false;
|
||||
LOG_INFO("BLE mesh stopped");
|
||||
}
|
||||
|
||||
bool NRF52BLEMesh::onSend(const meshtastic_MeshPacket *mp)
|
||||
{
|
||||
if (!isRunning || !mp)
|
||||
return false;
|
||||
|
||||
// Don't echo packets that arrived via BLE mesh
|
||||
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_MESH) {
|
||||
LOG_DEBUG("Drop BLE mesh echo");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t buffer[meshtastic_MeshPacket_size];
|
||||
size_t encodedLen = encodeForBLE(mp, buffer, sizeof(buffer));
|
||||
if (encodedLen == 0) {
|
||||
LOG_WARN("BLE mesh encode failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build manufacturer data: company ID (2 bytes) + version (1 byte) + protobuf payload
|
||||
size_t mfgDataLen = 2 + 1 + encodedLen;
|
||||
|
||||
// Extended advertising can carry up to 255 bytes total.
|
||||
// Reserve 5 bytes for the flags and manufacturer-data AD headers.
|
||||
if (mfgDataLen > 250) {
|
||||
LOG_WARN("BLE mesh packet too large: %u bytes", mfgDataLen);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t mfgData[250];
|
||||
mfgData[0] = (uint8_t)(BLE_MESH_COMPANY_ID & 0xFF);
|
||||
mfgData[1] = (uint8_t)((BLE_MESH_COMPANY_ID >> 8) & 0xFF);
|
||||
mfgData[2] = BLE_MESH_PROTOCOL_VERSION;
|
||||
memcpy(&mfgData[3], buffer, encodedLen);
|
||||
|
||||
// Stop scanning during mesh advertisement burst
|
||||
stopScanning();
|
||||
|
||||
static uint8_t adv_buf[BLE_GAP_ADV_SET_DATA_SIZE_EXTENDED_MAX_SUPPORTED];
|
||||
|
||||
size_t adv_len = 0;
|
||||
adv_buf[adv_len++] = 2;
|
||||
adv_buf[adv_len++] = BLE_GAP_AD_TYPE_FLAGS;
|
||||
adv_buf[adv_len++] = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE;
|
||||
adv_buf[adv_len++] = (uint8_t)(mfgDataLen + 1);
|
||||
adv_buf[adv_len++] = BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA;
|
||||
memcpy(&adv_buf[adv_len], mfgData, mfgDataLen);
|
||||
adv_len += mfgDataLen;
|
||||
|
||||
Bluefruit.Advertising.stop();
|
||||
delay(BLE_MESH_ADV_SWITCH_SETTLE_MS);
|
||||
|
||||
const bool priorityRetries = shouldUsePriorityRetries(mp);
|
||||
const uint8_t extraRetries = priorityRetries ? BLE_MESH_PRIORITY_EXTRA_RETRIES : 0;
|
||||
bool sentAny = false;
|
||||
|
||||
for (uint8_t attempt = 0; attempt <= extraRetries; ++attempt) {
|
||||
if (attempt > 0) {
|
||||
const uint16_t retryDelayMs =
|
||||
random(BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS, BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS + 1);
|
||||
delay(retryDelayMs);
|
||||
}
|
||||
|
||||
if (sendAdvertisementBurst(adv_buf, adv_len, mp->id, encodedLen)) {
|
||||
sentAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore phone advertising through the normal Bluefruit setup path.
|
||||
if (config.bluetooth.enabled && nrf52Bluetooth) {
|
||||
nrf52Bluetooth->resumeAdvertising();
|
||||
}
|
||||
|
||||
// Resume scanning
|
||||
startScanning();
|
||||
|
||||
return sentAny;
|
||||
}
|
||||
|
||||
bool NRF52BLEMesh::sendAdvertisementBurst(const uint8_t *advBuf, size_t advLen, PacketId packetId, size_t encodedLen)
|
||||
{
|
||||
static ble_gap_adv_data_t gapAdvData;
|
||||
static ble_gap_adv_params_t advParams;
|
||||
|
||||
gapAdvData.adv_data.p_data = const_cast<uint8_t *>(advBuf);
|
||||
gapAdvData.adv_data.len = advLen;
|
||||
gapAdvData.scan_rsp_data.p_data = NULL;
|
||||
gapAdvData.scan_rsp_data.len = 0;
|
||||
|
||||
memset(&advParams, 0, sizeof(advParams));
|
||||
advParams.properties.type = BLE_GAP_ADV_TYPE_EXTENDED_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED;
|
||||
advParams.p_peer_addr = NULL;
|
||||
advParams.filter_policy = BLE_GAP_ADV_FP_ANY;
|
||||
advParams.interval = BLE_MESH_ADV_INTERVAL;
|
||||
advParams.duration = 0;
|
||||
advParams.primary_phy = BLE_GAP_PHY_1MBPS;
|
||||
advParams.secondary_phy = BLE_GAP_PHY_1MBPS;
|
||||
advParams.max_adv_evts = BLE_MESH_ADV_COUNT;
|
||||
|
||||
uint8_t meshAdvHandle = 0;
|
||||
uint32_t stopErr = sd_ble_gap_adv_stop(meshAdvHandle);
|
||||
if (stopErr != NRF_SUCCESS && stopErr != NRF_ERROR_INVALID_STATE) {
|
||||
LOG_WARN("BLE mesh adv stop failed: 0x%x", stopErr);
|
||||
}
|
||||
delay(BLE_MESH_ADV_SWITCH_SETTLE_MS);
|
||||
|
||||
uint32_t err = sd_ble_gap_adv_set_configure(&meshAdvHandle, &gapAdvData, &advParams);
|
||||
if (err != NRF_SUCCESS) {
|
||||
LOG_WARN("BLE mesh adv configure failed: 0x%x", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
err = sd_ble_gap_adv_start(meshAdvHandle, BLE_CONN_CFG_TAG_DEFAULT);
|
||||
if (err != NRF_SUCCESS) {
|
||||
LOG_WARN("BLE mesh adv start failed: 0x%x", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("BLE mesh adv sent (id=%u, len=%u)", packetId, encodedLen);
|
||||
delay(BLE_MESH_ADV_COUNT * 10 + BLE_MESH_ADV_BURST_GUARD_MS);
|
||||
|
||||
stopErr = sd_ble_gap_adv_stop(meshAdvHandle);
|
||||
if (stopErr != NRF_SUCCESS && stopErr != NRF_ERROR_INVALID_STATE) {
|
||||
LOG_WARN("BLE mesh adv stop after send failed: 0x%x", stopErr);
|
||||
}
|
||||
delay(BLE_MESH_ADV_SWITCH_SETTLE_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NRF52BLEMesh::shouldUsePriorityRetries(const meshtastic_MeshPacket *mp) const
|
||||
{
|
||||
if (!mp)
|
||||
return false;
|
||||
|
||||
// Prioritize retries for ack-sensitive traffic and direct messages only.
|
||||
const bool isDirect = (mp->to != 0) && !isBroadcast(mp->to);
|
||||
return mp->want_ack || isDirect;
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::startScanning()
|
||||
{
|
||||
bleMeshScanReportData.len = sizeof(bleMeshScanBuffer);
|
||||
bleMeshScanParams.interval = BLE_MESH_SCAN_INTERVAL;
|
||||
bleMeshScanParams.window = BLE_MESH_SCAN_WINDOW;
|
||||
|
||||
uint32_t err = sd_ble_gap_scan_start(&bleMeshScanParams, &bleMeshScanReportData);
|
||||
if (err == NRF_SUCCESS) {
|
||||
LOG_DEBUG("BLE mesh scanning started");
|
||||
} else if (err == NRF_ERROR_INVALID_STATE) {
|
||||
LOG_DEBUG("BLE mesh scanning already active");
|
||||
} else {
|
||||
LOG_WARN("BLE mesh scan start failed: 0x%x", err);
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::stopScanning()
|
||||
{
|
||||
uint32_t err = sd_ble_gap_scan_stop();
|
||||
if (err != NRF_SUCCESS && err != NRF_ERROR_INVALID_STATE) {
|
||||
LOG_WARN("BLE mesh scan stop failed: 0x%x", err);
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::onBleEvent(ble_evt_t *event)
|
||||
{
|
||||
if (!instance || !instance->isRunning || !event)
|
||||
return;
|
||||
|
||||
switch (event->header.evt_id) {
|
||||
case BLE_GAP_EVT_ADV_REPORT: {
|
||||
ble_gap_evt_adv_report_t *report = &event->evt.gap_evt.params.adv_report;
|
||||
|
||||
if (report->type.status == BLE_GAP_ADV_DATA_STATUS_COMPLETE) {
|
||||
instance->handleScanResult(report);
|
||||
}
|
||||
|
||||
bleMeshScanReportData.len = sizeof(bleMeshScanBuffer);
|
||||
uint32_t err = sd_ble_gap_scan_start(NULL, &bleMeshScanReportData);
|
||||
if (err != NRF_SUCCESS && err != NRF_ERROR_INVALID_STATE) {
|
||||
LOG_WARN("BLE mesh scan resume failed: 0x%x", err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BLE_GAP_EVT_TIMEOUT:
|
||||
if (event->evt.gap_evt.params.timeout.src == BLE_GAP_TIMEOUT_SRC_SCAN) {
|
||||
instance->startScanning();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::handleScanResult(ble_gap_evt_adv_report_t *report)
|
||||
{
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
// Parse advertisement data to find manufacturer-specific data
|
||||
uint8_t *data = report->data.p_data;
|
||||
uint16_t len = report->data.len;
|
||||
uint16_t offset = 0;
|
||||
|
||||
while (offset < len) {
|
||||
uint8_t adLen = data[offset];
|
||||
if (adLen == 0 || offset + adLen >= len)
|
||||
break;
|
||||
|
||||
uint8_t adType = data[offset + 1];
|
||||
if (adType == BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA && adLen >= 4) {
|
||||
// Check company ID
|
||||
uint16_t companyId = data[offset + 2] | (data[offset + 3] << 8);
|
||||
if (companyId == BLE_MESH_COMPANY_ID) {
|
||||
// Check protocol version
|
||||
uint8_t version = data[offset + 4];
|
||||
if (version == BLE_MESH_PROTOCOL_VERSION) {
|
||||
// Extract protobuf payload (skip company ID + version = 3 bytes)
|
||||
const uint8_t *payload = &data[offset + 5];
|
||||
size_t payloadLen = adLen - 4; // adLen includes type byte, subtract type + company(2) + version(1)
|
||||
|
||||
LOG_DEBUG("BLE mesh RX: rssi=%d, len=%u", report->rssi, payloadLen);
|
||||
updatePeer(report->peer_addr, report->rssi);
|
||||
deliverToRouter(payload, payloadLen);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += adLen + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::updatePeer(const ble_gap_addr_t &addr, int8_t rssi)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
|
||||
// Check if peer already known
|
||||
for (uint8_t i = 0; i < peerCount; i++) {
|
||||
if (memcmp(&peers[i].addr, &addr, sizeof(ble_gap_addr_t)) == 0) {
|
||||
peers[i].rssi = rssi;
|
||||
peers[i].lastSeenMs = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new peer
|
||||
if (peerCount < BLE_MESH_MAX_PEERS) {
|
||||
peers[peerCount].addr = addr;
|
||||
peers[peerCount].rssi = rssi;
|
||||
peers[peerCount].lastSeenMs = now;
|
||||
peers[peerCount].nodeNum = 0; // Unknown until we decode a packet from them
|
||||
peerCount++;
|
||||
LOG_DEBUG("BLE mesh new peer (%u total)", peerCount);
|
||||
} else {
|
||||
// Replace oldest peer
|
||||
pruneStale();
|
||||
if (peerCount < BLE_MESH_MAX_PEERS) {
|
||||
peers[peerCount].addr = addr;
|
||||
peers[peerCount].rssi = rssi;
|
||||
peers[peerCount].lastSeenMs = now;
|
||||
peers[peerCount].nodeNum = 0;
|
||||
peerCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NRF52BLEMesh::pruneStale()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint8_t writeIdx = 0;
|
||||
|
||||
for (uint8_t i = 0; i < peerCount; i++) {
|
||||
if (now - peers[i].lastSeenMs < BLE_MESH_PEER_TIMEOUT_MS) {
|
||||
if (writeIdx != i)
|
||||
peers[writeIdx] = peers[i];
|
||||
writeIdx++;
|
||||
}
|
||||
}
|
||||
peerCount = writeIdx;
|
||||
}
|
||||
|
||||
#endif // HAS_BLE_MESH && ARCH_NRF52
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#if HAS_BLE_MESH && defined(ARCH_NRF52)
|
||||
|
||||
#include "mesh/BLEMeshHandler.h"
|
||||
#include <bluefruit.h>
|
||||
|
||||
// Max number of BLE mesh peers we can track
|
||||
#ifndef BLE_MESH_MAX_PEERS
|
||||
#define BLE_MESH_MAX_PEERS 8
|
||||
#endif
|
||||
|
||||
// Scan interval and window in units of 0.625ms
|
||||
// Default: scan 50ms every 500ms (10% duty cycle)
|
||||
#ifndef BLE_MESH_SCAN_INTERVAL
|
||||
#define BLE_MESH_SCAN_INTERVAL 800 // 500ms
|
||||
#endif
|
||||
#ifndef BLE_MESH_SCAN_WINDOW
|
||||
#define BLE_MESH_SCAN_WINDOW 80 // 50ms
|
||||
#endif
|
||||
|
||||
// Advertising interval for mesh data in units of 0.625ms
|
||||
#ifndef BLE_MESH_ADV_INTERVAL
|
||||
#define BLE_MESH_ADV_INTERVAL 160 // 100ms
|
||||
#endif
|
||||
|
||||
// How many times to repeat a mesh advertisement
|
||||
#ifndef BLE_MESH_ADV_COUNT
|
||||
#define BLE_MESH_ADV_COUNT 3
|
||||
#endif
|
||||
|
||||
// Small guard intervals help the SoftDevice finish the stop/start transition
|
||||
// when we temporarily borrow the single advertising handle.
|
||||
#ifndef BLE_MESH_ADV_SWITCH_SETTLE_MS
|
||||
#define BLE_MESH_ADV_SWITCH_SETTLE_MS 2
|
||||
#endif
|
||||
|
||||
#ifndef BLE_MESH_ADV_BURST_GUARD_MS
|
||||
#define BLE_MESH_ADV_BURST_GUARD_MS 5
|
||||
#endif
|
||||
|
||||
// Extra retry bursts for higher-priority traffic only.
|
||||
// Base burst always runs once for every packet.
|
||||
#ifndef BLE_MESH_PRIORITY_EXTRA_RETRIES
|
||||
#define BLE_MESH_PRIORITY_EXTRA_RETRIES 2
|
||||
#endif
|
||||
|
||||
// Randomized delay range between extra retry bursts in milliseconds.
|
||||
#ifndef BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS
|
||||
#define BLE_MESH_PRIORITY_RETRY_JITTER_MIN_MS 50
|
||||
#endif
|
||||
#ifndef BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS
|
||||
#define BLE_MESH_PRIORITY_RETRY_JITTER_MAX_MS 200
|
||||
#endif
|
||||
|
||||
// How long before a peer is considered stale (ms)
|
||||
#ifndef BLE_MESH_PEER_TIMEOUT_MS
|
||||
#define BLE_MESH_PEER_TIMEOUT_MS 300000 // 5 minutes
|
||||
#endif
|
||||
|
||||
class NRF52BLEMesh : public BLEMeshHandler
|
||||
{
|
||||
public:
|
||||
void start() override;
|
||||
void stop() override;
|
||||
bool onSend(const meshtastic_MeshPacket *mp) override;
|
||||
void onBluetoothReady() override;
|
||||
|
||||
private:
|
||||
// Scanning
|
||||
void startScanning();
|
||||
void stopScanning();
|
||||
static void onBleEvent(ble_evt_t *event);
|
||||
void handleScanResult(ble_gap_evt_adv_report_t *report);
|
||||
bool sendAdvertisementBurst(const uint8_t *advBuf, size_t advLen, PacketId packetId, size_t encodedLen);
|
||||
bool shouldUsePriorityRetries(const meshtastic_MeshPacket *mp) const;
|
||||
|
||||
// Peer tracking
|
||||
struct BLEMeshPeer {
|
||||
NodeNum nodeNum;
|
||||
ble_gap_addr_t addr;
|
||||
int8_t rssi;
|
||||
uint32_t lastSeenMs;
|
||||
};
|
||||
BLEMeshPeer peers[BLE_MESH_MAX_PEERS];
|
||||
uint8_t peerCount = 0;
|
||||
void updatePeer(const ble_gap_addr_t &addr, int8_t rssi);
|
||||
void pruneStale();
|
||||
|
||||
// Singleton for static callbacks
|
||||
static NRF52BLEMesh *instance;
|
||||
};
|
||||
|
||||
#endif // HAS_BLE_MESH && ARCH_NRF52
|
||||
@@ -233,6 +233,7 @@ void NRF52Bluetooth::shutdown()
|
||||
Bluefruit.Security.setPairPasskeyCallback(NRF52Bluetooth::onUnwantedPairing); // Actively refuse (during factory reset)
|
||||
disconnect();
|
||||
Bluefruit.Advertising.stop();
|
||||
meshReadyNotified = false;
|
||||
}
|
||||
void NRF52Bluetooth::startDisabled()
|
||||
{
|
||||
@@ -241,6 +242,7 @@ void NRF52Bluetooth::startDisabled()
|
||||
// Shutdown bluetooth for minimum power draw
|
||||
Bluefruit.Advertising.stop();
|
||||
Bluefruit.setTxPower(-40); // Minimum power
|
||||
meshReadyNotified = false;
|
||||
LOG_INFO("Disable NRF52 Bluetooth. (Workaround: tx power min, advertise stopped)");
|
||||
}
|
||||
bool NRF52Bluetooth::isConnected()
|
||||
@@ -263,7 +265,8 @@ void NRF52Bluetooth::setup()
|
||||
LOG_INFO("Init the Bluefruit nRF52 module");
|
||||
Bluefruit.autoConnLed(false);
|
||||
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
|
||||
Bluefruit.begin();
|
||||
Bluefruit.configCentralBandwidth(BANDWIDTH_MAX);
|
||||
Bluefruit.begin(1, 1);
|
||||
// Clear existing data.
|
||||
Bluefruit.Advertising.stop();
|
||||
Bluefruit.Advertising.clearData();
|
||||
@@ -338,14 +341,22 @@ void NRF52Bluetooth::setup()
|
||||
// Setup the advertising packet(s)
|
||||
LOG_INFO("Set up the advertising payload(s)");
|
||||
startAdv();
|
||||
if (bleMeshHandler && !meshReadyNotified) {
|
||||
bleMeshHandler->onBluetoothReady();
|
||||
meshReadyNotified = true;
|
||||
}
|
||||
LOG_INFO("Advertise");
|
||||
}
|
||||
void NRF52Bluetooth::resumeAdvertising()
|
||||
{
|
||||
Bluefruit.Advertising.restartOnDisconnect(true);
|
||||
Bluefruit.Advertising.setInterval(32, 668); // in unit of 0.625 ms
|
||||
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
|
||||
Bluefruit.Advertising.start(0);
|
||||
Bluefruit.Advertising.stop();
|
||||
Bluefruit.Advertising.clearData();
|
||||
Bluefruit.ScanResponse.clearData();
|
||||
startAdv();
|
||||
if (bleMeshHandler && !meshReadyNotified) {
|
||||
bleMeshHandler->onBluetoothReady();
|
||||
meshReadyNotified = true;
|
||||
}
|
||||
}
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
|
||||
@@ -22,4 +22,6 @@ class NRF52Bluetooth : BluetoothApi
|
||||
|
||||
static bool onUnwantedPairing(uint16_t conn_handle, uint8_t const passkey[6], bool match_request);
|
||||
static void disconnect();
|
||||
|
||||
bool meshReadyNotified = false;
|
||||
};
|
||||
+4
-10
@@ -429,13 +429,8 @@ 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
|
||||
#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);
|
||||
#ifdef INPUTDRIVER_ENCODER_BTN
|
||||
gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL);
|
||||
#endif
|
||||
#if defined(WAKE_ON_TOUCH)
|
||||
gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL);
|
||||
@@ -476,9 +471,8 @@ 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
|
||||
#ifdef INPUTDRIVER_WAKE_BTN_PIN
|
||||
gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN);
|
||||
#undef INPUTDRIVER_WAKE_BTN_PIN
|
||||
#if defined(INPUTDRIVER_ENCODER_BTN)
|
||||
gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_ENCODER_BTN);
|
||||
#endif
|
||||
#if defined(WAKE_ON_TOUCH)
|
||||
gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,20 +27,15 @@ 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
|
||||
# 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_unflags = -fno-lto
|
||||
build_flags =
|
||||
${arduino_base.build_flags}
|
||||
-flto
|
||||
-Wall
|
||||
-Wextra
|
||||
-Isrc/platform/esp32
|
||||
-include mbedtls/error.h
|
||||
-std=gnu++17
|
||||
-std=c++11
|
||||
-DHAS_BLE_MESH=1
|
||||
-DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG
|
||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
||||
-DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL
|
||||
|
||||
@@ -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.8
|
||||
zinggjm/GxEPD2@1.6.7
|
||||
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
|
||||
lewisxhe/SensorLib@0.3.4
|
||||
lib_ignore =
|
||||
|
||||
@@ -16,8 +16,4 @@ 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
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
extends = esp32_common
|
||||
custom_esp32_kind = esp32c3
|
||||
|
||||
build_flags =
|
||||
${esp32_common.build_flags}
|
||||
-DCONFIG_BT_NIMBLE_EXT_ADV=1
|
||||
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2
|
||||
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_DATA_LEN=251
|
||||
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_c3_exception_decoder
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ build_flags =
|
||||
-Wall
|
||||
-Wextra
|
||||
-Isrc/platform/esp32
|
||||
-std=c++11
|
||||
-DESP_OPENSSL_SUPPRESS_LEGACY_WARNING
|
||||
-DSERIAL_BUFFER_SIZE=4096
|
||||
-DLIBPAX_ARDUINO
|
||||
|
||||
@@ -11,7 +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.8
|
||||
zinggjm/GxEPD2@1.6.7
|
||||
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
|
||||
adafruit/Adafruit NeoPixel@1.15.4
|
||||
build_unflags =
|
||||
|
||||
@@ -12,8 +12,8 @@ build_flags =
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
lib_deps = ${esp32s3_base.lib_deps}
|
||||
# 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=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
|
||||
earlephilhower/ESP8266Audio@1.9.9
|
||||
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
|
||||
earlephilhower/ESP8266SAM@1.1.0
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ build_flags = ${esp32s3_base.build_flags} -Os
|
||||
|
||||
lib_deps = ${esp32s3_base.lib_deps}
|
||||
${device-ui_base.lib_deps}
|
||||
# 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=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
|
||||
earlephilhower/ESP8266Audio@1.9.9
|
||||
# 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
|
||||
|
||||
@@ -23,6 +23,6 @@ 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.8
|
||||
zinggjm/GxEPD2@1.6.7
|
||||
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
|
||||
adafruit/Adafruit NeoPixel@1.15.4
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
extends = esp32_common
|
||||
custom_esp32_kind = esp32s3
|
||||
|
||||
build_flags =
|
||||
${esp32_common.build_flags}
|
||||
-DCONFIG_BT_NIMBLE_EXT_ADV=1
|
||||
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2
|
||||
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_DATA_LEN=251
|
||||
-include mbedtls/error.h
|
||||
|
||||
monitor_speed = 115200
|
||||
|
||||
lib_deps =
|
||||
|
||||
@@ -8,7 +8,6 @@ build_flags =
|
||||
-D HELTEC_V4
|
||||
-D HAS_LORA_FEM=1
|
||||
-I variants/esp32s3/heltec_v4
|
||||
-ULED_BUILTIN
|
||||
|
||||
|
||||
[env:heltec-v4]
|
||||
|
||||
@@ -29,14 +29,6 @@
|
||||
#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.2 uses a GC1109 FEM chip with integrated PA and LNA
|
||||
// RF path: SX1262 -> Pi attenuator -> GC1109 PA -> Antenna
|
||||
@@ -97,4 +89,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
|
||||
#define GPS_THREAD_INTERVAL 50
|
||||
@@ -17,8 +17,8 @@ lib_deps =
|
||||
https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.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=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio
|
||||
earlephilhower/ESP8266Audio@1.9.9
|
||||
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
|
||||
earlephilhower/ESP8266SAM@1.1.0
|
||||
# renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user