Browse Source

Merge remote-tracking branch 'upstream/dev' into meshcore_seeed_sensecap_indicator

pull/2568/head
Christos Themelis 1 month ago
parent
commit
581dc526cb
  1. 4
      .github/workflows/pr-build-check.yml
  2. 31
      .github/workflows/run-unit-tests.yml
  3. 8
      README.md
  4. 198
      arch/nrf52/extra_scripts/patch_bluefruit.py
  5. 7
      boards/t-echo.json
  6. 10
      docs/cli_commands.md
  7. 144
      docs/companion_protocol.md
  8. 34
      docs/faq.md
  9. 1
      docs/number_allocations.md
  10. 45
      examples/companion_radio/MyMesh.cpp
  11. 3
      examples/companion_radio/MyMesh.h
  12. 2
      examples/companion_radio/main.cpp
  13. 31
      examples/companion_radio/ui-new/UITask.cpp
  14. 1
      examples/companion_radio/ui-orig/UITask.cpp
  15. 24
      examples/kiss_modem/KissModem.cpp
  16. 2
      examples/kiss_modem/KissModem.h
  17. 11
      examples/kiss_modem/main.cpp
  18. 10
      examples/simple_repeater/MyMesh.cpp
  19. 12
      examples/simple_repeater/UITask.cpp
  20. 2
      examples/simple_repeater/main.cpp
  21. 10
      examples/simple_room_server/MyMesh.cpp
  22. 12
      examples/simple_room_server/UITask.cpp
  23. 2
      examples/simple_room_server/main.cpp
  24. 6
      examples/simple_secure_chat/main.cpp
  25. 10
      examples/simple_sensor/SensorMesh.cpp
  26. 4
      examples/simple_sensor/SensorMesh.h
  27. 12
      examples/simple_sensor/UITask.cpp
  28. 2
      examples/simple_sensor/main.cpp
  29. 29
      platformio.ini
  30. 2
      src/helpers/AutoDiscoverRTCClock.cpp
  31. 10
      src/helpers/CommonCLI.cpp
  32. 4
      src/helpers/esp32/ESPNOWRadio.cpp
  33. 7
      src/helpers/esp32/ESPNOWRadio.h
  34. 10
      src/helpers/radiolib/CustomLLCC68Wrapper.h
  35. 2
      src/helpers/radiolib/CustomLR1110.h
  36. 13
      src/helpers/radiolib/CustomLR1110Wrapper.h
  37. 10
      src/helpers/radiolib/CustomSTM32WLxWrapper.h
  38. 10
      src/helpers/radiolib/CustomSX1262Wrapper.h
  39. 10
      src/helpers/radiolib/CustomSX1268Wrapper.h
  40. 10
      src/helpers/radiolib/CustomSX1276Wrapper.h
  41. 10
      src/helpers/radiolib/RadioLibWrappers.cpp
  42. 10
      src/helpers/radiolib/RadioLibWrappers.h
  43. 733
      src/helpers/sensors/EnvironmentSensorManager.cpp
  44. 34
      src/helpers/sensors/EnvironmentSensorManager.h
  45. 2
      src/helpers/sensors/RAK12035_SoilMoisture.cpp
  46. 8
      src/helpers/ui/ST7789LCDDisplay.cpp
  47. 1
      src/helpers/ui/buzzer.cpp
  48. 13
      test/mocks/AES.h
  49. 14
      test/mocks/SHA256.h
  50. 10
      test/mocks/Stream.h
  51. 57
      test/test_utils/test_tohex.cpp
  52. 5
      variants/ebyte_eora_s3/platformio.ini
  53. 15
      variants/ebyte_eora_s3/target.cpp
  54. 3
      variants/ebyte_eora_s3/target.h
  55. 4
      variants/gat562_30s_mesh_kit/platformio.ini
  56. 15
      variants/gat562_30s_mesh_kit/target.cpp
  57. 3
      variants/gat562_30s_mesh_kit/target.h
  58. 5
      variants/gat562_mesh_evb_pro/platformio.ini
  59. 15
      variants/gat562_mesh_evb_pro/target.cpp
  60. 3
      variants/gat562_mesh_evb_pro/target.h
  61. 4
      variants/gat562_mesh_tracker_pro/platformio.ini
  62. 15
      variants/gat562_mesh_tracker_pro/target.cpp
  63. 3
      variants/gat562_mesh_tracker_pro/target.h
  64. 4
      variants/gat562_mesh_watch13/platformio.ini
  65. 15
      variants/gat562_mesh_watch13/target.cpp
  66. 3
      variants/gat562_mesh_watch13/target.h
  67. 5
      variants/generic-e22/platformio.ini
  68. 15
      variants/generic-e22/target.cpp
  69. 3
      variants/generic-e22/target.h
  70. 12
      variants/generic_espnow/target.cpp
  71. 3
      variants/generic_espnow/target.h
  72. 5
      variants/heltec_ct62/platformio.ini
  73. 15
      variants/heltec_ct62/target.cpp
  74. 3
      variants/heltec_ct62/target.h
  75. 5
      variants/heltec_e213/platformio.ini
  76. 15
      variants/heltec_e213/target.cpp
  77. 3
      variants/heltec_e213/target.h
  78. 5
      variants/heltec_e290/platformio.ini
  79. 15
      variants/heltec_e290/target.cpp
  80. 3
      variants/heltec_e290/target.h
  81. 7
      variants/heltec_mesh_solar/platformio.ini
  82. 15
      variants/heltec_mesh_solar/target.cpp
  83. 3
      variants/heltec_mesh_solar/target.h
  84. 2
      variants/heltec_t096/LoRaFEMControl.cpp
  85. 9
      variants/heltec_t096/platformio.ini
  86. 15
      variants/heltec_t096/target.cpp
  87. 3
      variants/heltec_t096/target.h
  88. 12
      variants/heltec_t114/platformio.ini
  89. 15
      variants/heltec_t114/target.cpp
  90. 3
      variants/heltec_t114/target.h
  91. 5
      variants/heltec_t190/platformio.ini
  92. 15
      variants/heltec_t190/target.cpp
  93. 3
      variants/heltec_t190/target.h
  94. 5
      variants/heltec_tracker/platformio.ini
  95. 15
      variants/heltec_tracker/target.cpp
  96. 3
      variants/heltec_tracker/target.h
  97. 2
      variants/heltec_tracker_v2/LoRaFEMControl.cpp
  98. 7
      variants/heltec_tracker_v2/platformio.ini
  99. 15
      variants/heltec_tracker_v2/target.cpp
  100. 3
      variants/heltec_tracker_v2/target.h

4
.github/workflows/pr-build-check.yml

@ -39,6 +39,10 @@ jobs:
- wio-e5-mini_repeater
# ESP32-C6
- LilyGo_Tlora_C6_repeater_
# LR1110 (nRF52)
- wio_wm1110_repeater
# SX1276 (ESP32)
- Tbeam_SX1276_repeater
steps:
- name: Clone Repo

31
.github/workflows/run-unit-tests.yml

@ -0,0 +1,31 @@
name: Run Unit Tests
on:
push:
branches:
- main
- master
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v4
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Run Unit Tests
run: pio test -e native -vv
- name: Upload Test Results
# Upload test results even if the test step failed.
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: .pio/build/native/

8
README.md

@ -98,6 +98,14 @@ Here are some general principals you should try to adhere to:
Help us prioritize! Please react with thumbs-up to issues/PRs you care about most. We look at reaction counts when planning work.
### Running unit tests
To run unit tests, run the following command:
```bash
pio test --environment native --verbose
```
## Road-Map / To-Do
There are a number of fairly major features in the pipeline, with no particular time-frames attached yet. In very rough chronological order:

198
arch/nrf52/extra_scripts/patch_bluefruit.py

@ -1,198 +0,0 @@
"""
Bluefruit BLE Patch Script
Patches Bluefruit library to fix semaphore leak bug that causes device lockup
when BLE central disconnects unexpectedly (e.g., going out of range, supervision timeout).
Patches applied:
1. BLEConnection.h: Add _hvn_qsize member to track semaphore queue size
2. BLEConnection.cpp: Store hvn_qsize and restore semaphore on disconnect
Bug description:
- When a BLE central disconnects unexpectedly (reason=8 supervision timeout),
the BLE_GATTS_EVT_HVN_TX_COMPLETE event may never fire
- This leaves the _hvn_sem counting semaphore in a decremented state
- Since BLEConnection objects are reused (destructor never called), the
semaphore count is never restored
- Eventually all semaphore counts are exhausted and notify() blocks/fails
"""
from pathlib import Path
Import("env") # pylint: disable=undefined-variable
def _patch_ble_connection_header(source: Path) -> bool:
"""
Add _hvn_qsize member variable to BLEConnection class.
This is needed to restore the semaphore to its correct count on disconnect.
Returns True if patch was applied or already applied, False on error.
"""
try:
content = source.read_text()
# Check if already patched
if "_hvn_qsize" in content:
return True # Already patched
# Find the location to insert - after _phy declaration
original_pattern = ''' uint8_t _phy;
uint8_t _role;'''
patched_pattern = ''' uint8_t _phy;
uint8_t _hvn_qsize;
uint8_t _role;'''
if original_pattern not in content:
print("Bluefruit patch: WARNING - BLEConnection.h pattern not found")
return False
content = content.replace(original_pattern, patched_pattern)
source.write_text(content)
# Verify
if "_hvn_qsize" not in source.read_text():
return False
return True
except Exception as e:
print(f"Bluefruit patch: ERROR patching BLEConnection.h: {e}")
return False
def _patch_ble_connection_source(source: Path) -> bool:
"""
Patch BLEConnection.cpp to:
1. Store hvn_qsize in constructor
2. Restore _hvn_sem semaphore to full count on disconnect
Returns True if patch was applied or already applied, False on error.
"""
try:
content = source.read_text()
# Check if already patched (look for the restore loop)
if "uxSemaphoreGetCount(_hvn_sem)" in content:
return True # Already patched
# Patch 1: Store queue size in constructor
constructor_original = ''' _hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);'''
constructor_patched = ''' _hvn_qsize = hvn_qsize;
_hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);'''
if constructor_original not in content:
print("Bluefruit patch: WARNING - BLEConnection.cpp constructor pattern not found")
return False
content = content.replace(constructor_original, constructor_patched)
# Patch 2: Restore semaphore on disconnect
disconnect_original = ''' case BLE_GAP_EVT_DISCONNECTED:
// mark as disconnected
_connected = false;
break;'''
disconnect_patched = ''' case BLE_GAP_EVT_DISCONNECTED:
// Restore notification semaphore to full count
// This fixes lockup when disconnect occurs with notifications in flight
while (uxSemaphoreGetCount(_hvn_sem) < _hvn_qsize) {
xSemaphoreGive(_hvn_sem);
}
// Release indication semaphore if waiting
if (_hvc_sem) {
_hvc_received = false;
xSemaphoreGive(_hvc_sem);
}
// mark as disconnected
_connected = false;
break;'''
if disconnect_original not in content:
print("Bluefruit patch: WARNING - BLEConnection.cpp disconnect pattern not found")
return False
content = content.replace(disconnect_original, disconnect_patched)
source.write_text(content)
# Verify
verify_content = source.read_text()
if "uxSemaphoreGetCount(_hvn_sem)" not in verify_content:
return False
if "_hvn_qsize = hvn_qsize" not in verify_content:
return False
return True
except Exception as e:
print(f"Bluefruit patch: ERROR patching BLEConnection.cpp: {e}")
return False
def _apply_bluefruit_patches(target, source, env): # pylint: disable=unused-argument
framework_path = env.get("PLATFORMFW_DIR")
if not framework_path:
framework_path = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52")
if not framework_path:
print("Bluefruit patch: ERROR - framework directory not found")
env.Exit(1)
return
framework_dir = Path(framework_path)
bluefruit_lib = framework_dir / "libraries" / "Bluefruit52Lib" / "src"
patch_failed = False
# Patch BLEConnection.h
conn_header = bluefruit_lib / "BLEConnection.h"
if conn_header.exists():
before = conn_header.read_text()
success = _patch_ble_connection_header(conn_header)
after = conn_header.read_text()
if success:
if before != after:
print("Bluefruit patch: OK - Applied BLEConnection.h fix (added _hvn_qsize member)")
else:
print("Bluefruit patch: OK - BLEConnection.h already patched")
else:
print("Bluefruit patch: FAILED - BLEConnection.h")
patch_failed = True
else:
print(f"Bluefruit patch: ERROR - BLEConnection.h not found at {conn_header}")
patch_failed = True
# Patch BLEConnection.cpp
conn_source = bluefruit_lib / "BLEConnection.cpp"
if conn_source.exists():
before = conn_source.read_text()
success = _patch_ble_connection_source(conn_source)
after = conn_source.read_text()
if success:
if before != after:
print("Bluefruit patch: OK - Applied BLEConnection.cpp fix (restore semaphore on disconnect)")
else:
print("Bluefruit patch: OK - BLEConnection.cpp already patched")
else:
print("Bluefruit patch: FAILED - BLEConnection.cpp")
patch_failed = True
else:
print(f"Bluefruit patch: ERROR - BLEConnection.cpp not found at {conn_source}")
patch_failed = True
if patch_failed:
print("Bluefruit patch: CRITICAL - Patch failed! Build aborted.")
env.Exit(1)
# Register the patch to run before build
bluefruit_action = env.VerboseAction(_apply_bluefruit_patches, "Applying Bluefruit BLE patches...")
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", bluefruit_action)
# Also run immediately to patch before any compilation
_apply_bluefruit_patches(None, None, env)

7
boards/t-echo.json

@ -53,11 +53,14 @@
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink",
"cmsis-dap",
"blackmagic"
]
],
"use_1200bps_touch": true,
"wait_for_upload_port": true
},
"url": "https://os.mbed.com/platforms/Nordic-nRF52840-DK/",
"vendor": "Nordic"
}
}

10
docs/cli_commands.md

@ -456,7 +456,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14
**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.
**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes.
---
@ -490,6 +490,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Default:** `0.5`
**Note:** When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. `0` disables the window entirely.
---
#### View or change the retransmit delay factor for direct traffic
@ -502,6 +504,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Default:** `0.2`
**Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them.
---
#### [Experimental] View or change the processing delay for received traffic
@ -514,6 +518,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Default:** `0.0`
**Note:** When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions.
---
#### View or change the duty cycle limit
@ -927,7 +933,7 @@ region save
---
#### View or change thevalue of a sensor
#### View or change the value of a sensor
**Usage:**
- `sensor get <key>`
- `sensor set <key> <value>`

144
docs/companion_protocol.md

@ -283,32 +283,110 @@ Bytes 7+: Message Text (UTF-8, variable length)
### 6. Send Channel Data Datagram
**Purpose**: Send binary datagram data to a channel.
**Purpose**: Send a binary datagram to a channel. Unlike channel text messages, datagrams carry no built-in sender identity and no timestamp — applications needing either must encode them inside the binary payload.
**Command Format**:
```
Byte 0: 0x3E
Bytes 1-2: Data Type (`data_type`, 16-bit little-endian)
Byte 3: Channel Index (0-7)
Bytes 4+: Binary payload bytes (variable length)
Byte 0: 0x3E
Byte 1: Channel Index (0-7)
Byte 2: Path Length (0xFF = flood, otherwise actual path length)
Bytes 3 .. 2+path_len: Path (omitted when path_len == 0xFF)
Next 2 bytes (little-endian): Data Type (`data_type`, uint16)
Remaining bytes: Binary payload (variable length)
```
**Example** (flood, `DATA_TYPE_DEV`, payload `A1 B2 C3`, channel 1):
```
3E 01 FF FF FF A1 B2 C3
```
**Data Type / Transport Mapping**:
- `0x0000` is invalid for this command.
- `0x0000` (`DATA_TYPE_RESERVED`) is invalid and rejected with `PACKET_ERROR`.
- `0xFFFF` (`DATA_TYPE_DEV`) is the developer namespace for experimenting and developing apps.
- Other non-zero values can be used as assigned application/community namespaces.
**Note**: Applications that need a timestamp should encode it inside the binary payload.
- Values `0x0001`–`0xFFFE` are available for registered application/community namespaces. See the [Registered data_type values](#registered-data_type-values) table below.
**Limits**:
- Maximum payload length is `163` bytes.
- Larger payloads are rejected with `PACKET_ERROR`.
- Maximum payload length is `MAX_CHANNEL_DATA_LENGTH = MAX_FRAME_SIZE - 9 = 163` bytes.
- Larger payloads are rejected with `PACKET_ERROR` (`ERR_CODE_ILLEGAL_ARG`).
**Response**: `PACKET_OK` (0x00) on success, or `PACKET_ERROR` (0x01) with one of:
- `ERR_CODE_NOT_FOUND` (2) — unknown `channel_idx`
- `ERR_CODE_ILLEGAL_ARG` (6) — invalid `path_len`, reserved `data_type` (`0x0000`), or payload larger than `MAX_CHANNEL_DATA_LENGTH`
- `ERR_CODE_TABLE_FULL` (3) — outbound send queue is full; retry later
**Inbound datagrams** are delivered to the host via `RESP_CODE_CHANNEL_DATA_RECV` (0x1B); see [Receive Channel Data Datagram](#receive-channel-data-datagram).
#### Registered `data_type` values
`data_type` is an **application identifier**, not a payload-format identifier. Each registered value identifies an application that owns its own internal payload schemas. The firmware does not inspect payload contents — `data_type` is transported opaquely.
| Value | Constant | Purpose |
|-----------------|----------------------|--------------------------------------------------------------------------|
| 0x0000 | `DATA_TYPE_RESERVED` | Reserved; invalid on send |
| 0x0001 – 0x00FF | — | Reserved for internal use |
| 0x0100 – 0xFEFF | — | Registered application namespaces (see [number_allocations.md](number_allocations.md)) |
| 0xFF00 – 0xFFFE | — | Testing/development; no registration required |
| 0xFFFF | `DATA_TYPE_DEV` | Developer/experimental namespace |
**Response**: `PACKET_OK` (0x00) on success
To register a new application, submit a PR adding a row to the table in [docs/number_allocations.md](number_allocations.md). Internal sub-formats within an allocated application ID are owned by that application and are not tracked in MeshCore firmware or this document.
---
### 6. Get Message
### Receive Channel Data Datagram
Inbound group datagrams (radio-level `PAYLOAD_TYPE_GRP_DATA`, 0x06) are forwarded to the host as `RESP_CODE_CHANNEL_DATA_RECV` notifications.
**Frame Format** (`RESP_CODE_CHANNEL_DATA_RECV`, 0x1B):
```
Byte 0: 0x1B (packet type)
Byte 1: SNR (signed int8, scaled ×4 — divide by 4.0 to recover dB)
Bytes 2-3: Reserved (clients MUST ignore)
Byte 4: Channel Index (0-7)
Byte 5: Path Length (actual path length when flooded, otherwise 0xFF for direct)
Bytes 6-7: Data Type (uint16 little-endian)
Byte 8: Data Length
Bytes 9 .. 8+data_len: Payload
```
**Path bytes are not forwarded**: Only `path_len` is reported in the receive frame — the path itself is not copied to the host. There are no path bytes between byte 5 and the data_type field at bytes 6–7, regardless of `path_len`.
**Path Length semantics differ between send and receive**:
| Direction | `path_len = 0xFF` | `path_len ≠ 0xFF` |
|-----------|---------------------------------|-------------------------------------------------------------------------------------|
| Send | Flood the network | Direct route; the encoded path follows (low 6 bits = hash count, top 2 bits + 1 = hash size; on-wire byte count = `hash_count × hash_size`) |
| Receive | Packet arrived via direct route | Packet was flooded; this is the encoded `pkt->path_len` field as observed (no path bytes follow) |
In other words, the meaning of `0xFF` is inverted between the two directions, and on receive the field carries metadata only — never a routable path. `path_len` is an encoded byte (see `Packet::isValidPathLen` / `Packet::writePath` in `src/Packet.cpp`), not a raw byte count.
**Note**: The device may also emit `PACKET_MESSAGES_WAITING` (0x83) to notify the host that datagrams are queued; poll with `CMD_SYNC_NEXT_MESSAGE` (0x0A) to retrieve them.
**Parsing Pseudocode**:
```python
def parse_channel_data_recv(data):
if len(data) < 9:
return None
snr_byte = data[1]
snr = (snr_byte if snr_byte < 128 else snr_byte - 256) / 4.0
channel_idx = data[4]
path_len = data[5]
data_type = int.from_bytes(data[6:8], 'little')
data_len = data[8]
if 9 + data_len > len(data):
return None
payload = data[9:9 + data_len]
return {
'snr': snr,
'channel_idx': channel_idx,
'path_len': path_len,
'data_type': data_type,
'payload': bytes(payload),
}
```
---
### 7. Get Message
**Purpose**: Request the next queued message from the device.
@ -325,13 +403,14 @@ Byte 0: 0x0A
**Response**:
- `PACKET_CHANNEL_MSG_RECV` (0x08) or `PACKET_CHANNEL_MSG_RECV_V3` (0x11) for channel messages
- `PACKET_CONTACT_MSG_RECV` (0x07) or `PACKET_CONTACT_MSG_RECV_V3` (0x10) for contact messages
- `PACKET_CHANNEL_DATA_RECV` (0x1B) for channel data datagrams
- `PACKET_NO_MORE_MSGS` (0x0A) if no messages available
**Note**: Poll this command periodically to retrieve queued messages. The device may also send `PACKET_MESSAGES_WAITING` (0x83) as a notification when messages are available.
---
### 7. Get Battery and Storage
### 8. Get Battery and Storage
**Purpose**: Query device battery voltage and storage usage.
@ -527,6 +606,15 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
## Response Parsing
### Terminology
This document uses a spec-level naming convention (`PACKET_*`) for bytes the firmware sends back to the host. In the firmware source these same values are split across two `#define` families by purpose:
- `RESP_CODE_*` — direct replies to a command (e.g. `RESP_CODE_CHANNEL_DATA_RECV` = `PACKET_CHANNEL_DATA_RECV` = 0x1B).
- `PUSH_CODE_*` — asynchronous notifications not tied to a specific command (e.g. `PUSH_CODE_MSG_WAITING` = `PACKET_MESSAGES_WAITING` = 0x83).
Byte values are authoritative; names are aliases. When reading firmware source, `RESP_CODE_X` / `PUSH_CODE_X` correspond to this doc's `PACKET_X` of the same numeric value.
### Packet Types
| Value | Name | Description |
@ -547,6 +635,7 @@ Use the `SEND_CHANNEL_MESSAGE` command (see [Commands](#commands)).
| 0x10 | PACKET_CONTACT_MSG_RECV_V3 | Contact message (V3 with SNR) |
| 0x11 | PACKET_CHANNEL_MSG_RECV_V3 | Channel message (V3 with SNR) |
| 0x12 | PACKET_CHANNEL_INFO | Channel information |
| 0x1B | PACKET_CHANNEL_DATA_RECV | Channel data datagram |
| 0x80 | PACKET_ADVERTISEMENT | Advertisement packet |
| 0x82 | PACKET_ACK | Acknowledgment |
| 0x83 | PACKET_MESSAGES_WAITING | Messages waiting notification |
@ -718,22 +807,18 @@ Bytes 1-6: ACK Code (6 bytes, hex)
### Error Codes
**PACKET_ERROR** (0x01) may include an error code in byte 1:
`PACKET_ERROR` (0x01) carries a single-byte error code in byte 1. Values match the `ERR_CODE_*` constants defined in `examples/companion_radio/MyMesh.cpp`:
| Error Code | Description |
|------------|-------------|
| 0x00 | Generic error (no specific code) |
| 0x01 | Invalid command |
| 0x02 | Invalid parameter |
| 0x03 | Channel not found |
| 0x04 | Channel already exists |
| 0x05 | Channel index out of range |
| 0x06 | Secret mismatch |
| 0x07 | Message too long |
| 0x08 | Device busy |
| 0x09 | Not enough storage |
| Code | Constant (firmware) | Description |
|------|----------------------------|------------------------------------------------------------------------------|
| 1 | `ERR_CODE_UNSUPPORTED_CMD` | Unknown or unsupported command byte / sub-command |
| 2 | `ERR_CODE_NOT_FOUND` | Target not found (channel, contact, message, etc.) |
| 3 | `ERR_CODE_TABLE_FULL` | Internal queue or table is full — retry later |
| 4 | `ERR_CODE_BAD_STATE` | Operation not valid in current device state (e.g. iterator already running) |
| 5 | `ERR_CODE_FILE_IO_ERROR` | Filesystem or storage I/O failure |
| 6 | `ERR_CODE_ILLEGAL_ARG` | Invalid argument (bad length, out-of-range value, reserved field, etc.) |
**Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response.
**Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response, and treat unknown codes as generic errors.
### Frame Handling
@ -765,7 +850,8 @@ BLE implementations enqueue and deliver one protocol frame per BLE write/notific
- `GET_CHANNEL``PACKET_CHANNEL_INFO`
- `SET_CHANNEL``PACKET_OK` or `PACKET_ERROR`
- `SEND_CHANNEL_MESSAGE``PACKET_MSG_SENT`
- `GET_MESSAGE``PACKET_CHANNEL_MSG_RECV`, `PACKET_CONTACT_MSG_RECV`, or `PACKET_NO_MORE_MSGS`
- `GET_MESSAGE``PACKET_CHANNEL_MSG_RECV`, `PACKET_CONTACT_MSG_RECV`, `PACKET_CHANNEL_DATA_RECV`, or `PACKET_NO_MORE_MSGS`
- `SEND_CHANNEL_DATA``PACKET_OK` or `PACKET_ERROR`
- `GET_BATTERY``PACKET_BATTERY`
4. **Timeout Handling**:

34
docs/faq.md

@ -23,8 +23,8 @@ A list of frequently-asked questions and answers for MeshCore
- [3.3. Q: What is the password to administer a repeater or a room server?](#33-q-what-is-the-password-to-administer-a-repeater-or-a-room-server)
- [3.4. Q: What is the password to join a room server?](#34-q-what-is-the-password-to-join-a-room-server)
- [3.5. Q: Can I retrieve a repeater's private key or set a repeater's private key?](#35-q-can-i-retrieve-a-repeaters-private-key-or-set-a-repeaters-private-key)
- [3.6. Q: The first byte of my repeater's public key collides with an exisitng repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?](#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-exisitng-repeater-on-the-mesh--how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing)
- [3.7. Q: My repeater maybe suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?](#37-q-my-repeater-maybe-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios--what-can-i-do)
- [3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?](#36-q-the-first-byte-of-my-repeaters-public-key-collides-with-an-existing-repeater-on-the-mesh--how-do-i-get-a-new-private-key-with-a-matching-public-key-that-has-its-first-byte-of-my-choosing)
- [3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?](#37-q-my-repeater-may-be-suffering-from-deafness-due-to-high-power-interference-near-my-meshs-frequency-it-is-not-hearing-other-in-range-meshcore-radios--what-can-i-do)
- [3.8. Q: How do I make my repeater an observer on the mesh?](#38-q-how-do-i-make-my-repeater-an-observer-on-the-mesh)
- [3.9. Q: What is multi-byte support? What do 1-byte, 2-byte, 3-byte adverts and messages mean?](#39-q-what-is-multi-byte-support--what-do-1-byte-2-byte-3-byte-adverts-and-messages-mean)
- [3.9.1. Q: **What path hash sizes will my repeater forward?**](#391-q-what-path-hash-sizes-will-my-repeater-forward)
@ -39,7 +39,7 @@ A list of frequently-asked questions and answers for MeshCore
- [4.3. Q: Why is my T-Deck Plus not getting any satellite lock?](#43-q-why-is-my-t-deck-plus-not-getting-any-satellite-lock)
- [4.4. Q: Why is my OG (non-Plus) T-Deck not getting any satellite lock?](#44-q-why-is-my-og-non-plus-t-deck-not-getting-any-satellite-lock)
- [4.5. Q: What size of SD card does the T-Deck support?](#45-q-what-size-of-sd-card-does-the-t-deck-support)
- [4.6. Q: what is the public key for the default public channel?](#46-q-what-is-the-public-key-for-the-default-public-channel)
- [4.6. Q: What is the public key for the default public channel?](#46-q-what-is-the-public-key-for-the-default-public-channel)
- [4.7. Q: How do I get maps on T-Deck?](#47-q-how-do-i-get-maps-on-t-deck)
- [4.8. Q: Where do the map tiles go?](#48-q-where-do-the-map-tiles-go)
- [4.9. Q: How to unlock deeper map zoom and server management features on T-Deck?](#49-q-how-to-unlock-deeper-map-zoom-and-server-management-features-on-t-deck)
@ -52,9 +52,9 @@ A list of frequently-asked questions and answers for MeshCore
- [5.1. Q: What are BW, SF, and CR?](#51-q-what-are-bw-sf-and-cr)
- [5.2. Q: Do MeshCore clients repeat?](#52-q-do-meshcore-clients-repeat)
- [5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone?](#53-q-what-happens-when-a-node-learns-a-route-via-a-mobile-repeater-and-that-repeater-is-gone)
- [5.4. Q: How does a node discovery a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?](#54-q-how-does-a-node-discovery-a-path-to-its-destination-and-then-use-it-to-send-messages-in-the-future-instead-of-flooding-every-message-it-sends-like-meshtastic)
- [5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?](#54-q-how-does-a-node-discover-a-path-to-its-destination-and-then-use-it-to-send-messages-in-the-future-instead-of-flooding-every-message-it-sends-like-meshtastic)
- [5.5. Q: Do public channels always flood? Do private channels always flood?](#55-q-do-public-channels-always-flood-do-private-channels-always-flood)
- [5.6. Q: what is the public key for the default public channel?](#56-q-what-is-the-public-key-for-the-default-public-channel)
- [5.6. Q: What is the public key for the default public channel?](#56-q-what-is-the-public-key-for-the-default-public-channel)
- [5.7. Q: Is MeshCore open source?](#57-q-is-meshcore-open-source)
- [5.8. Q: How can I support MeshCore?](#58-q-how-can-i-support-meshcore)
- [5.9. Q: How do I build MeshCore firmware from source?](#59-q-how-do-i-build-meshcore-firmware-from-source)
@ -62,7 +62,7 @@ A list of frequently-asked questions and answers for MeshCore
- [5.11. Q: Does MeshCore support ATAK](#511-q-does-meshcore-support-atak)
- [5.12. Q: How do I add a node to the MeshCore Map](#512-q-how-do-i-add-a-node-to-the-meshcore-map)
- [5.13. Q: Can I use a Raspberry Pi to update a MeshCore radio?](#513-q-can-i-use-a-raspberry-pi-to-update-a-meshcore-radio)
- [5.14. Q: Are there are projects built around MeshCore?](#514-q-are-there-are-projects-built-around-meshcore)
- [5.14. Q: Are there projects built around MeshCore?](#514-q-are-there-projects-built-around-meshcore)
- [5.15. Q: Are there client applications for Windows or Mac?](#515-q-are-there-client-applications-for-windows-or-mac)
- [5.16. Q: Are there any resources that compare MeshCore to other LoRa systems?](#516-q-are-there-any-resources-that-compare-meshcore-to-other-lora-systems)
- [6. Troubleshooting](#6-troubleshooting)
@ -187,7 +187,7 @@ The T-Deck firmware is free to download and most features are available without
### 2.3. Q: What frequencies are supported by MeshCore?
**A:** It supports the 868MHz range in the UK/EU and the 915MHz range in New Zealand, Australia, and the USA. Countries and regions in these two frequency ranges are also supported.
Use the smartphone client or the repeater setup feature on there web flasher to set your radios' RF settings by choosing the preset for your regions.
Use the smartphone client or the repeater setup feature on the web flasher to set your radios' RF settings by choosing the preset for your regions.
Recently, as of October 2025, many regions have moved to the "narrow" setting, aka using BW62.5 and a lower SF number (instead of the original SF11). For example, USA/Canada (Recommended) preset is 910.525MHz, SF7, BW62.5, CR5.
@ -271,7 +271,7 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo
Reboot the repeater after `set prv.key <hex>` command for the new private key to take effect.
### 3.6. Q: The first byte of my repeater's public key collides with an exisitng repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?
### 3.6. Q: The first byte of my repeater's public key collides with an existing repeater on the mesh. How do I get a new private key with a matching public key that has its first byte of my choosing?
**A:** You can generate a new private key and specific the first byte of its public key here: https://gessaman.com/mc-keygen/
@ -280,7 +280,7 @@ Having multiple repeaters with the same first byte ID does not negatively affect
Best practice is when you set up a new repeater, choose a public key that is not in use. If it is not possible to find a unique first byte for your repeater's public key, choose one that is unique within about 10 miles (16 km) to minimize collision with nearby repeaters.
### 3.7. Q: My repeater maybe suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?
### 3.7. Q: My repeater may be suffering from deafness due to high power interference near my mesh's frequency, it is not hearing other in-range MeshCore radios. What can I do?
**A:** This may be due to the SX1262 radio's auto gain control feature. You can use this command to periodically reset its AGC.
@ -379,7 +379,7 @@ GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-De
### 4.5. Q: What size of SD card does the T-Deck support?
**A:** Users have had no issues using 16GB or 32GB SD cards. Format the SD card to **FAT32**.
### 4.6. Q: what is the public key for the default public channel?
### 4.6. Q: What is the public key for the default public channel?
**A:**
T-Deck uses the same key the smartphone apps use but in base64
`izOH6cXN6mrJ5e26oRXNcg==`
@ -495,7 +495,7 @@ In MeshCore, only repeaters and room server with `set repeat on` repeat.
In the case if users are moving around frequently, and the paths are breaking, they just see the phone client retries and revert to flood to attempt to re-establish a path.
### 5.4. Q: How does a node discovery a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?
### 5.4. Q: How does a node discover a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic?
Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization.
@ -508,7 +508,7 @@ Routes are stored in sender's contact list. When you send a message the first t
[Source](https://discord.com/channels/1343693475589263471/1343693475589263474/1350023009527664672)
### 5.6. Q: what is the public key for the default public channel?
### 5.6. Q: What is the public key for the default public channel?
**A:** The smartphone app key is in hex:
` 8b3387e9c5cdea6ac9e5edbaa115cd72`
@ -650,11 +650,11 @@ From here, reference repeater and room server command line commands on MeshCore
- https://github.com/meshcore-dev/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
### 5.14. Q: Are there are projects built around MeshCore?
### 5.14. Q: Are there projects built around MeshCore?
**A:** Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Asistant.
**A:** Yes, there are many. MeshCore's protocol is open source using the MIT license. The MIT license and the open source protocol makes it very easy for the MeshCore community to build new firmware for radios, applications on mobile devices, map tools, and analysis tools, and integration with other projects like Home Assistant.
As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. [samuk](https://github.com/samuk) maintains a very exhausive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.
As new MeshCore community projects become available on a weekly basis, we have stopped tracking them here in this FAQ. [samuk](https://github.com/samuk) maintains a very exhaustive list of MeshCore community project at https://github.com/samuk/awesome-meshcore/blob/main/README.md. samuk accepts PRs and merges them regularly.
### 5.15. Q: Are there client applications for Windows or Mac?
@ -708,7 +708,7 @@ You can get the epoch time on <https://www.epochconverter.com/> and use it to se
- For RAK, click the reset button **TWICE**
- For T1000-e, quickly disconnect and reconnect the magnetic side of the cable from the device **TWICE**
- For Heltec T114, click the reset button **TWICE** (the bottom button)
- For Xiao nRF52, click the reset button once. If that doesn't work, quickly double click the reset button twice. If that doesn't work, disconnection the board from your PC and reconnect again ([seeed studio wiki](https://wiki.seeedstudio.com/XIAO_BLE/#access-the-swd-pins-for-debugging-and-reflashing-bootloader))
- For Xiao nRF52, click the reset button once. If that doesn't work, quickly double click the reset button twice. If that doesn't work, disconnect the board from your PC and reconnect again ([seeed studio wiki](https://wiki.seeedstudio.com/XIAO_BLE/#access-the-swd-pins-for-debugging-and-reflashing-bootloader))
5. A new folder will appear on your computer's desktop
6. Download the `flash_erase*.uf2` file for your device on https://flasher.meshcore.io
- RAK WisBlock and Heltec T114: `Flash_erase-nRF32_softdevice_v6.uf2`
@ -818,7 +818,7 @@ Edit WIFI_SSID and WIFI_PWD in `./variants/heltec_v3/platformio.ini` and then fl
### 7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with a EByte E22-900M30S or a E22-900M33S module, what should their transmit power be set to?
**A:**
For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command `set tx`. You can get their current value using command line comand `get tx`
For companion radios, you can set these radios' transmit power in the smartphone app. For repeater and room server radios, you can set their transmit power using the command line command `set tx`. You can get their current value using command line command `get tx`
⚠️ **WARNING: Set these values at your own risk. Incorrect power settings can permanently damage your radio hardware.**

1
docs/number_allocations.md

@ -15,6 +15,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist
| Data-Type range | App name | Contact |
|-----------------|-----------------------------|------------------------------------------------------|
| 0000 - 00FF | -reserved for internal use- | |
| 0100 | MeshCore Open | [email protected] — https://github.com/zjs81/meshcore-open |
| FF00 - FFFF | -reserved for testing/dev- | |
(add rows, inside the range 0100 - FEFF for custom apps)

45
examples/companion_radio/MyMesh.cpp

@ -495,19 +495,27 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
if (send_unscoped) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
} else {
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
sendFloodScoped(*scope, pkt, delay_millis);
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
sendFloodScoped(*scope, pkt, delay_millis);
}
}
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: have per-channel send_scope
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
if (send_unscoped) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped
} else {
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
sendFloodScoped(*scope, pkt, delay_millis);
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
sendFloodScoped(*scope, pkt, delay_millis);
}
}
void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
@ -856,6 +864,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
dirty_contacts_expiry = 0;
memset(advert_paths, 0, sizeof(advert_paths));
memset(send_scope.key, 0, sizeof(send_scope.key));
send_unscoped = false;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
@ -951,8 +960,8 @@ void MyMesh::begin(bool has_display) {
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
_store->loadChannels(this);
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
@ -977,9 +986,13 @@ struct FreqRange {
};
static FreqRange repeat_freq_ranges[] = {
#ifdef ALLOWED_REPEAT_FREQ_RANGE
ALLOWED_REPEAT_FREQ_RANGE
#else
{ 433000, 433000 },
{ 869000, 869000 },
{ 918000, 918000 }
#endif
};
bool MyMesh::isValidClientRepeatFreq(uint32_t f) const {
@ -1378,7 +1391,7 @@ void MyMesh::handleCmdFrame(size_t len) {
_prefs.client_repeat = repeat;
savePrefs();
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf,
(uint32_t)cr);
@ -1395,7 +1408,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
_prefs.tx_power_dbm = power;
savePrefs();
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setTxPower(_prefs.tx_power_dbm);
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) {
@ -1889,10 +1902,14 @@ void MyMesh::handleCmdFrame(size_t len) {
}
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 0) {
if (len >= 2 + 16) {
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set curr scope TransportKey
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set scope override TransportKey
} else {
memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null
memset(send_scope.key, 0, sizeof(send_scope.key)); // reset scope override
}
send_unscoped = false;
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 1) { // ver 12+
send_unscoped = true;
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_DEFAULT_FLOOD_SCOPE && len >= 1) {
if (len >= 1+31+16) {

3
examples/companion_radio/MyMesh.h

@ -5,7 +5,7 @@
#include "AbstractUITask.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 11
#define FIRMWARE_VER_CODE 12
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
@ -218,6 +218,7 @@ private:
uint32_t _active_ble_pin;
bool _iter_started;
bool _cli_rescue;
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
char cli_command[80];
uint8_t app_target_ver;
uint8_t *sign_data;

2
examples/companion_radio/main.cpp

@ -125,7 +125,7 @@ void setup() {
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();

31
examples/companion_radio/ui-new/UITask.cpp

@ -57,13 +57,21 @@ public:
int logoWidth = 128;
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1);
uint16_t websiteWidth = display.getTextWidth(website);
display.setCursor((display.width() - websiteWidth) / 2, 22);
display.print(website);
// version info
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(2);
display.drawTextCentered(display.width()/2, 22, _version_info);
display.setTextSize(1);
display.drawTextCentered(display.width()/2, 35, _version_info);
display.setTextSize(1);
display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE);
display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE);
return 1000;
}
@ -146,7 +154,7 @@ class HomeScreen : public UIScreen {
bool sensors_scroll = false;
int sensors_scroll_offset = 0;
int next_sensors_refresh = 0;
void refresh_sensors() {
if (millis() > next_sensors_refresh) {
sensors_lpp.reset();
@ -170,7 +178,7 @@ class HomeScreen : public UIScreen {
public:
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_shutdown_init(false), sensors_lpp(200) { }
void poll() override {
@ -213,7 +221,7 @@ public:
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 54, tmp);
display.drawTextCentered(display.width() / 2, 54, tmp);
#endif
if (_task->hasConnection()) {
display.setColor(DisplayDriver::GREEN);
@ -241,10 +249,10 @@ public:
} else {
sprintf(tmp, "%dh", secs / (60*60));
}
int timestamp_width = display.getTextWidth(tmp);
int max_name_width = display.width() - timestamp_width - 1;
char filtered_recent_name[sizeof(a->name)];
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name));
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name);
@ -310,7 +318,7 @@ public:
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
display.drawTextLeftAlign(0, y, "pos");
sprintf(buf, "%.4f %.4f",
sprintf(buf, "%.4f %.4f",
nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
@ -567,6 +575,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#ifdef PIN_BUZZER
buzzer.begin();
buzzer.quiet(_node_prefs->buzzer_quiet);
buzzer.startup();
#endif
#ifdef PIN_VIBRATION
@ -741,7 +750,7 @@ void UITask::loop() {
#endif
#if defined(PIN_USER_BTN_ANA)
if (abs(millis() - _analogue_pin_read_millis) > 10) {
ev = analog_btn.check();
int ev = analog_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_NEXT);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
@ -878,7 +887,7 @@ bool UITask::getGPSState() {
return !strcmp(_sensors->getSettingValue(i), "1");
}
}
}
}
return false;
}

1
examples/companion_radio/ui-orig/UITask.cpp

@ -57,6 +57,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#ifdef PIN_BUZZER
buzzer.begin();
buzzer.quiet(_node_prefs->buzzer_quiet);
buzzer.startup();
#endif
// Initialize digital button if available

24
examples/kiss_modem/KissModem.cpp

@ -129,6 +129,8 @@ void KissModem::processFrame() {
memcpy(_pending_tx, data, data_len);
_pending_tx_len = data_len;
_has_pending_tx = true;
} else if (_has_pending_tx) {
writeHardwareError(HW_ERR_TX_BUSY);
}
break;
@ -257,6 +259,7 @@ void KissModem::processTx() {
_tx_timer = millis();
_tx_state = TX_DELAY;
} else {
_tx_timer = millis();
_tx_state = TX_WAIT_CLEAR;
}
}
@ -273,19 +276,30 @@ void KissModem::processTx() {
_tx_timer = millis();
_tx_state = TX_SLOT_WAIT;
}
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(KISS_MAX_PACKET_SIZE) * KISS_TX_TIMEOUT_FACTOR) {
_tx_timer = millis();
_tx_state = TX_DELAY;
}
break;
case TX_SLOT_WAIT:
if (millis() - _tx_timer >= (uint32_t)_slottime * 10) {
_tx_timer = millis();
_tx_state = TX_WAIT_CLEAR;
}
break;
case TX_DELAY:
if (millis() - _tx_timer >= (uint32_t)_txdelay * 10) {
_radio.startSendRaw(_pending_tx, _pending_tx_len);
_tx_state = TX_SENDING;
if (_radio.startSendRaw(_pending_tx, _pending_tx_len)) {
_tx_timer = millis();
_tx_state = TX_SENDING;
} else {
uint8_t result = 0x00;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
}
break;
@ -296,6 +310,12 @@ void KissModem::processTx() {
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) {
_radio.onSendFinished();
uint8_t result = 0x00;
writeHardwareFrame(HW_RESP_TX_DONE, &result, 1);
_has_pending_tx = false;
_tx_state = TX_IDLE;
}
break;
}

2
examples/kiss_modem/KissModem.h

@ -26,6 +26,7 @@
#define KISS_DEFAULT_TXDELAY 50
#define KISS_DEFAULT_PERSISTENCE 63
#define KISS_DEFAULT_SLOTTIME 10
#define KISS_TX_TIMEOUT_FACTOR 3/2 // 1.5x estimated airtime
#define HW_CMD_GET_IDENTITY 0x01
#define HW_CMD_GET_RANDOM 0x02
@ -71,6 +72,7 @@
#define HW_ERR_MAC_FAILED 0x04
#define HW_ERR_UNKNOWN_CMD 0x05
#define HW_ERR_ENCRYPT_FAILED 0x06
#define HW_ERR_TX_BUSY 0x07
#define KISS_FIRMWARE_VERSION 1

11
examples/kiss_modem/main.cpp

@ -10,7 +10,10 @@
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
#else
#include <InternalFileSystem.h>
#endif
#if defined(KISS_UART_RX) && defined(KISS_UART_TX)
#include <HardwareSerial.h>
#endif
@ -29,7 +32,7 @@ void halt() {
}
void loadOrCreateIdentity() {
#if defined(NRF52_PLATFORM)
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
@ -53,11 +56,11 @@ void loadOrCreateIdentity() {
}
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) {
radio_set_params(freq, bw, sf, cr);
radio_driver.setParams(freq, bw, sf, cr);
}
void onSetTxPower(uint8_t power) {
radio_set_tx_power(power);
radio_driver.setTxPower(power);
}
float onGetCurrentRssi() {
@ -79,7 +82,7 @@ void setup() {
radio_driver.begin();
rng.begin(radio_get_rng_seed());
rng.begin(radio_driver.getRngSeed());
loadOrCreateIdentity();
sensors.begin();

10
examples/simple_repeater/MyMesh.cpp

@ -953,8 +953,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
#endif
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
@ -1050,7 +1050,7 @@ void MyMesh::dumpLogFile() {
}
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
radio_driver.setTxPower(power_dbm);
}
#if defined(USE_SX1262) || defined(USE_SX1268)
@ -1279,13 +1279,13 @@ void MyMesh::loop() {
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}

12
examples/simple_repeater/UITask.cpp

@ -52,17 +52,25 @@ void UITask::renderCurrScreen() {
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->setCursor((_display->width() - versionWidth) / 2, 35);
_display->print(_version_info);
// node type
const char* node_type = "< Repeater >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->setCursor((_display->width() - typeWidth) / 2, 48);
_display->print(node_type);
} else { // home screen
// node name

2
examples/simple_repeater/main.cpp

@ -57,7 +57,7 @@ void setup() {
halt();
}
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)

10
examples/simple_room_server/MyMesh.cpp

@ -691,8 +691,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
}
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
@ -796,7 +796,7 @@ void MyMesh::dumpLogFile() {
}
void MyMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
radio_driver.setTxPower(power_dbm);
}
void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
@ -999,13 +999,13 @@ void MyMesh::loop() {
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}

12
examples/simple_room_server/UITask.cpp

@ -52,17 +52,25 @@ void UITask::renderCurrScreen() {
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->setCursor((_display->width() - versionWidth) / 2, 35);
_display->print(_version_info);
// node type
const char* node_type = "< Room Server >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->setCursor((_display->width() - typeWidth) / 2, 48);
_display->print(node_type);
} else { // home screen
// node name

2
examples/simple_room_server/main.cpp

@ -35,7 +35,7 @@ void setup() {
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM)

6
examples/simple_secure_chat/main.cpp

@ -562,7 +562,7 @@ void setup() {
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
#if defined(NRF52_PLATFORM)
InternalFS.begin();
@ -577,8 +577,8 @@ void setup() {
#error "need to define filesystem"
#endif
radio_set_params(the_mesh.getFreqPref(), LORA_BW, LORA_SF, LORA_CR);
radio_set_tx_power(the_mesh.getTxPowerPref());
radio_driver.setParams(the_mesh.getFreqPref(), LORA_BW, LORA_SF, LORA_CR);
radio_driver.setTxPower(the_mesh.getTxPowerPref());
the_mesh.showWelcome();

10
examples/simple_sensor/SensorMesh.cpp

@ -764,8 +764,8 @@ void SensorMesh::begin(FILESYSTEM* fs) {
}
}
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
updateAdvertTimer();
updateFloodAdvertTimer();
@ -842,7 +842,7 @@ void SensorMesh::updateFloodAdvertTimer() {
}
void SensorMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
radio_driver.setTxPower(power_dbm);
}
void SensorMesh::formatStatsReply(char *reply) {
@ -908,13 +908,13 @@ void SensorMesh::loop() {
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
MESH_DEBUG_PRINTLN("Temp radio params");
}
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
MESH_DEBUG_PRINTLN("Radio params restored");
}

4
examples/simple_sensor/SensorMesh.h

@ -34,11 +34,11 @@
#define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "20 Mar 2026"
#define FIRMWARE_BUILD_DATE "19 Apr 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.14.1"
#define FIRMWARE_VERSION "v1.15.0"
#endif
#define FIRMWARE_ROLE "sensor"

12
examples/simple_sensor/UITask.cpp

@ -52,17 +52,25 @@ void UITask::renderCurrScreen() {
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
// meshcore website
const char* website = "https://meshcore.io";
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t websiteWidth = _display->getTextWidth(website);
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
_display->print(website);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->setCursor((_display->width() - versionWidth) / 2, 35);
_display->print(_version_info);
// node type
const char* node_type = "< Sensor >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->setCursor((_display->width() - typeWidth) / 2, 48);
_display->print(node_type);
} else { // home screen
// node name

2
examples/simple_sensor/main.cpp

@ -68,7 +68,7 @@ void setup() {
if (!radio_init()) { halt(); }
fast_rng.begin(radio_get_rng_seed());
fast_rng.begin(radio_driver.getRngSeed());
FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)

29
platformio.ini

@ -10,8 +10,8 @@
[platformio]
extra_configs =
variants/*/platformio.ini
platformio.local.ini
variants/*/platformio.ini
platformio.local.ini
[arduino_base]
framework = arduino
@ -81,17 +81,18 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/
extends = arduino_base
platform = nordicnrf52
platform_packages =
framework-arduinoadafruitnrf52 @ 1.10700.0
extra_scripts =
create-uf2.py
arch/nrf52/extra_scripts/patch_bluefruit.py
; use internal fork that includes patch to ble stack to prevent firmware lockup during rapid connect/disconnect
; https://github.com/meshcore-dev/MeshCore/pull/1177
; https://github.com/meshcore-dev/MeshCore/pull/1295
framework-arduinoadafruitnrf52 @ https://github.com/meshcore-dev/Adafruit_nRF52_Arduino#d541301
extra_scripts = create-uf2.py
build_flags = ${arduino_base.build_flags}
-D NRF52_PLATFORM
-D LFS_NO_ASSERT=1
-D EXTRAFS=1
lib_deps =
${arduino_base.lib_deps}
https://github.com/oltaco/CustomLFS @ 0.2.1
https://github.com/oltaco/CustomLFS#0.2.1
; ----------------- RP2040 ---------------------
[rp2040_base]
@ -151,3 +152,17 @@ lib_deps =
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit BME680 Library @ ^2.0.4
adafruit/Adafruit BMP085 Library @ ^1.2.4
; ----------------- TESTING ---------------------
[env:native]
platform = native
build_flags = -std=c++17
-I src
-I test/mocks
test_build_src = yes
build_src_filter =
-<*>
+<../src/Utils.cpp>
lib_deps =
google/googletest @ 1.17.0

2
src/helpers/AutoDiscoverRTCClock.cpp

@ -27,9 +27,11 @@ bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) {
}
void AutoDiscoverRTCClock::begin(TwoWire& wire) {
#if !defined(DISABLE_DS3231_PROBE)
if (i2c_probe(wire, DS3231_ADDRESS)) {
ds3231_success = rtc_3231.begin(&wire);
}
#endif
if (i2c_probe(wire, RV3028_ADDRESS)) {
rtc_rv3028.initI2C(wire);

10
src/helpers/CommonCLI.cpp

@ -2,6 +2,7 @@
#include "CommonCLI.h"
#include "TxtDataHelpers.h"
#include "AdvertDataHelpers.h"
#include "TxtDataHelpers.h"
#include <RTClib.h>
#ifndef BRIDGE_MAX_BAUD
@ -285,7 +286,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
// change admin password
StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
savePrefs();
sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!!
sprintf(reply, "password now: ");
StrHelper::strncpy(&reply[14], _prefs->password, 160-15); // echo back just to let admin know for sure!!
} else if (memcmp(command, "clear stats", 11) == 0) {
_callbacks->clearStats();
strcpy(reply, "(OK - stats reset)");
@ -726,7 +728,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error: unsupported by this board");
};
} else {
sprintf(reply, "unknown config: %s", config);
strcpy(reply, "unknown config: ");
StrHelper::strncpy(&reply[16], config, 160-17);
}
}
@ -784,10 +787,11 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(config, "direct.txdelay", 14) == 0) {
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor));
} else if (memcmp(config, "owner.info", 10) == 0) {
auto start = reply;
*reply++ = '>';
*reply++ = ' ';
const char* sp = _prefs->owner_info;
while (*sp) {
while (*sp && reply - start < 159) {
*reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|'
sp++;
}

4
src/helpers/esp32/ESPNOWRadio.cpp

@ -120,6 +120,10 @@ void ESPNOWRadio::init() {
}
}
uint32_t ESPNOWRadio::getRngSeed() {
return millis() + intID(); // TODO: where to get some entropy?
}
void ESPNOWRadio::setTxPower(uint8_t dbm) {
esp_wifi_set_max_tx_power(dbm * 4);
}

7
src/helpers/esp32/ESPNOWRadio.h

@ -9,6 +9,13 @@ protected:
public:
ESPNOWRadio() { n_recv = n_sent = n_recv_errors = 0; }
uint32_t getRngSeed();
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) {
// no-op
}
void powerOff() { /* no-op */ }
void init();
int recvRaw(uint8_t* bytes, int sz) override;
uint32_t getEstAirtimeFor(int len_bytes) override;

10
src/helpers/radiolib/CustomLLCC68Wrapper.h

@ -7,6 +7,15 @@
class CustomLLCC68Wrapper : public RadioLibWrapper {
public:
CustomLLCC68Wrapper(CustomLLCC68& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomLLCC68 *)_radio)->setFrequency(freq);
((CustomLLCC68 *)_radio)->setSpreadingFactor(sf);
((CustomLLCC68 *)_radio)->setBandwidth(bw);
((CustomLLCC68 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
}
bool isReceivingPacket() override {
return ((CustomLLCC68 *)_radio)->isReceiving();
}
@ -20,6 +29,7 @@ public:
int sf = ((CustomLLCC68 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
uint8_t getSpreadingFactor() const override { return ((CustomLLCC68 *)_radio)->spreadingFactor; }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }

2
src/helpers/radiolib/CustomLR1110.h

@ -36,4 +36,6 @@ class CustomLR1110 : public LR1110 {
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
return detected;
}
uint8_t getSpreadingFactor() const { return spreadingFactor; }
};

13
src/helpers/radiolib/CustomLR1110Wrapper.h

@ -7,6 +7,15 @@
class CustomLR1110Wrapper : public RadioLibWrapper {
public:
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomLR1110 *)_radio)->setFrequency(freq);
((CustomLR1110 *)_radio)->setSpreadingFactor(sf);
((CustomLR1110 *)_radio)->setBandwidth(bw);
((CustomLR1110 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
}
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); }
bool isReceivingPacket() override {
return ((CustomLR1110 *)_radio)->isReceiving();
@ -19,12 +28,14 @@ public:
void onSendFinished() override {
RadioLibWrapper::onSendFinished();
_radio->setPreambleLength(16); // overcomes weird issues with small and big pkts
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
}
float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); }
uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); }
void setRxBoostedGainMode(bool en) override {
((CustomLR1110 *)_radio)->setRxBoostedGainMode(en);
}

10
src/helpers/radiolib/CustomSTM32WLxWrapper.h

@ -8,6 +8,15 @@
class CustomSTM32WLxWrapper : public RadioLibWrapper {
public:
CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomSTM32WLx *)_radio)->setFrequency(freq);
((CustomSTM32WLx *)_radio)->setSpreadingFactor(sf);
((CustomSTM32WLx *)_radio)->setBandwidth(bw);
((CustomSTM32WLx *)_radio)->setCodingRate(cr);
updatePreamble(sf);
}
bool isReceivingPacket() override {
return ((CustomSTM32WLx *)_radio)->isReceiving();
}
@ -21,6 +30,7 @@ public:
int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
uint8_t getSpreadingFactor() const override { return ((CustomSTM32WLx *)_radio)->spreadingFactor; }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
};

10
src/helpers/radiolib/CustomSX1262Wrapper.h

@ -11,6 +11,15 @@
class CustomSX1262Wrapper : public RadioLibWrapper {
public:
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomSX1262 *)_radio)->setFrequency(freq);
((CustomSX1262 *)_radio)->setSpreadingFactor(sf);
((CustomSX1262 *)_radio)->setBandwidth(bw);
((CustomSX1262 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
}
bool isReceivingPacket() override {
return ((CustomSX1262 *)_radio)->isReceiving();
}
@ -24,6 +33,7 @@ public:
int sf = ((CustomSX1262 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
uint8_t getSpreadingFactor() const override { return ((CustomSX1262 *)_radio)->spreadingFactor; }
virtual void powerOff() override {
((CustomSX1262 *)_radio)->sleep(false);
}

10
src/helpers/radiolib/CustomSX1268Wrapper.h

@ -11,6 +11,15 @@
class CustomSX1268Wrapper : public RadioLibWrapper {
public:
CustomSX1268Wrapper(CustomSX1268& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomSX1268 *)_radio)->setFrequency(freq);
((CustomSX1268 *)_radio)->setSpreadingFactor(sf);
((CustomSX1268 *)_radio)->setBandwidth(bw);
((CustomSX1268 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
}
bool isReceivingPacket() override {
return ((CustomSX1268 *)_radio)->isReceiving();
}
@ -24,6 +33,7 @@ public:
int sf = ((CustomSX1268 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
uint8_t getSpreadingFactor() const override { return ((CustomSX1268 *)_radio)->spreadingFactor; }
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }

10
src/helpers/radiolib/CustomSX1276Wrapper.h

@ -10,6 +10,15 @@
class CustomSX1276Wrapper : public RadioLibWrapper {
public:
CustomSX1276Wrapper(CustomSX1276& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
((CustomSX1276 *)_radio)->setFrequency(freq);
((CustomSX1276 *)_radio)->setSpreadingFactor(sf);
((CustomSX1276 *)_radio)->setBandwidth(bw);
((CustomSX1276 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
}
bool isReceivingPacket() override {
return ((CustomSX1276 *)_radio)->isReceiving();
}
@ -23,4 +32,5 @@ public:
int sf = ((CustomSX1276 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
uint8_t getSpreadingFactor() const override { return ((CustomSX1276 *)_radio)->spreadingFactor; }
};

10
src/helpers/radiolib/RadioLibWrappers.cpp

@ -26,6 +26,8 @@ void setFlag(void) {
void RadioLibWrapper::begin() {
_radio->setPacketReceivedAction(setFlag); // this is also SentComplete interrupt
_preamble_sf = getSpreadingFactor();
_radio->setPreambleLength(preambleLengthForSF(_preamble_sf)); // longer preamble for lower SF improves reliability
state = STATE_IDLE;
if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep)
@ -40,6 +42,14 @@ void RadioLibWrapper::begin() {
_floor_sample_sum = 0;
}
uint32_t RadioLibWrapper::getRngSeed() {
return _radio->random(0x7FFFFFFF);
}
void RadioLibWrapper::setTxPower(int8_t dbm) {
_radio->setOutputPower(dbm);
}
void RadioLibWrapper::idle() {
_radio->standby();
state = STATE_IDLE; // need another startReceive()

10
src/helpers/radiolib/RadioLibWrappers.h

@ -11,6 +11,7 @@ protected:
int16_t _noise_floor, _threshold;
uint16_t _num_floor_samples;
int32_t _floor_sample_sum;
uint8_t _preamble_sf;
void idle();
void startRecv();
@ -27,7 +28,7 @@ protected:
virtual void dispatchPendingIrq() {}
public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; }
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; }
void begin() override;
virtual void powerOff() { _radio->sleep(); }
@ -45,7 +46,14 @@ public:
return isChannelActive();
}
virtual void setParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0;
uint32_t getRngSeed();
void setTxPower(int8_t dbm);
virtual float getCurrentRSSI() =0;
virtual uint8_t getSpreadingFactor() const { return LORA_SF; }
static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; }
void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); }
int getNoiseFloor() const override { return _noise_floor; }
void triggerNoiseFloorCalibrate(int threshold) override;

733
src/helpers/sensors/EnvironmentSensorManager.cpp

@ -1,11 +1,17 @@
#include "EnvironmentSensorManager.h"
#include <Wire.h>
#if ENV_PIN_SDA && ENV_PIN_SCL
#define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors
#else
#define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors
#endif
// ============================================================
// Sensor library includes and static driver instances
// ============================================================
#ifdef ENV_INCLUDE_BME680
#ifndef TELEM_BME680_ADDRESS
#define TELEM_BME680_ADDRESS 0x76
@ -31,7 +37,7 @@ static Adafruit_AHTX0 AHTX0;
#ifndef TELEM_BME280_ADDRESS
#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address
#endif
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
#include <Adafruit_BME280.h>
static Adafruit_BME280 BME280;
#endif
@ -40,7 +46,7 @@ static Adafruit_BME280 BME280;
#ifndef TELEM_BMP280_ADDRESS
#define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address
#endif
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level
#include <Adafruit_BMP280.h>
static Adafruit_BMP280 BMP280(TELEM_WIRE);
#endif
@ -51,7 +57,7 @@ static Adafruit_SHTC3 SHTC3;
#endif
#if ENV_INCLUDE_SHT4X
#define TELEM_SHT4X_ADDRESS 0x44 //0x44 - 0x46
#define TELEM_SHT4X_ADDRESS 0x44
#include <SensirionI2cSht4x.h>
static SensirionI2cSht4x SHT4X;
#endif
@ -63,7 +69,7 @@ LPS22HBClass LPS22HB(*TELEM_WIRE);
#if ENV_INCLUDE_INA3221
#ifndef TELEM_INA3221_ADDRESS
#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address
#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address
#endif
#ifndef TELEM_INA3221_SHUNT_VALUE
#define TELEM_INA3221_SHUNT_VALUE 0.100 // most variants will have a 0.1 ohm shunts
@ -88,9 +94,9 @@ static Adafruit_INA260 INA260;
#endif
#if ENV_INCLUDE_INA226
#define TELEM_INA226_ADDRESS 0x44
#define TELEM_INA226_ADDRESS 0x44
#define TELEM_INA226_SHUNT_VALUE 0.100
#define TELEM_INA226_MAX_AMP 0.8
#define TELEM_INA226_MAX_AMP 0.8
#include <INA226.h>
static INA226 INA226(TELEM_INA226_ADDRESS, TELEM_WIRE);
#endif
@ -161,381 +167,414 @@ public:
static RAK12500LocationProvider RAK12500_provider;
#endif
bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_GPS
#ifdef RAK_WISBLOCK_GPS
rakGPSInit(); //probe base board/sockets for GPS
#else
initBasicGPS();
#endif
#endif
#if ENV_PIN_SDA && ENV_PIN_SCL
#ifdef NRF52_PLATFORM
Wire1.setPins(ENV_PIN_SDA, ENV_PIN_SCL);
Wire1.setClock(100000);
Wire1.begin();
#else
Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
#endif
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
#endif
#if ENV_INCLUDE_AHTX0
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
AHTX0_initialized = true;
} else {
AHTX0_initialized = false;
MESH_DEBUG_PRINTLN("AHT10/AHT20 was not found at I2C address %02X", TELEM_AHTX_ADDRESS);
// ============================================================
// I2C bus scanner
// Probes every valid address and records which ones ACK.
// This runs before any sensor library is touched, so a missing
// or misbehaving device cannot stall or crash the boot sequence.
// ============================================================
static void scanI2CBus(TwoWire* wire, bool found[128]) {
for (uint8_t addr = 0x08; addr < 0x78; addr++) {
wire->beginTransmission(addr);
found[addr] = (wire->endTransmission() == 0);
}
#endif
}
#if ENV_INCLUDE_BME680
if (BME680.begin(TELEM_BME680_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
BME680_initialized = true;
} else {
BME680_initialized = false;
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS);
}
#endif
// ============================================================
// Per-sensor init and query functions
//
// init(wire, address) — called only when the address was seen
// on the bus. Returns 0 on failure, or the number of
// telemetry channels the sensor will consume (1 for all
// single-output sensors; INA3221 returns one per enabled
// hardware channel; MLX90614 and RAK12035+calibration
// return 2).
//
// query(channel, sub_channel, lpp) — called once per active
// sensor entry during querySensors(). sub_channel is always
// 0 for single-output sensors.
// ============================================================
#if ENV_INCLUDE_BME280
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
// Reduce self-heating: single-shot conversions, light oversampling, long standby.
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, // temperature
Adafruit_BME280::SAMPLING_X1, // pressure
Adafruit_BME280::SAMPLING_X1, // humidity
Adafruit_BME280::FILTER_OFF,
Adafruit_BME280::STANDBY_MS_1000);
BME280_initialized = true;
} else {
BME280_initialized = false;
MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESS);
}
#endif
#if ENV_INCLUDE_AHTX0
static uint8_t init_ahtx0(TwoWire* wire, uint8_t addr) {
return AHTX0.begin(wire, 0, addr) ? 1 : 0;
}
static void query_ahtx0(uint8_t ch, uint8_t, CayenneLPP& lpp) {
sensors_event_t humidity, temp;
AHTX0.getEvent(&humidity, &temp);
lpp.addTemperature(ch, temp.temperature);
lpp.addRelativeHumidity(ch, humidity.relative_humidity);
}
#endif
#if ENV_INCLUDE_BMP280
if (BMP280.begin(TELEM_BMP280_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS);
MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID());
BMP280_initialized = true;
} else {
BMP280_initialized = false;
MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESS);
#ifdef ENV_INCLUDE_BME680
static uint8_t init_bme680(TwoWire*, uint8_t addr) {
// Wire was set in the static constructor; begin() takes address only.
return BME680.begin(addr) ? 1 : 0;
}
static void query_bme680(uint8_t ch, uint8_t, CayenneLPP& lpp) {
if (BME680.performReading()) {
lpp.addTemperature(ch, BME680.temperature);
lpp.addRelativeHumidity(ch, BME680.humidity);
lpp.addBarometricPressure(ch, BME680.pressure / 100);
lpp.addAltitude(ch, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
lpp.addAnalogInput(ch, BME680.gas_resistance);
}
#endif
}
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
SHTC3_initialized = true;
} else {
SHTC3_initialized = false;
MESH_DEBUG_PRINTLN("SHTC3 was not found at I2C address %02X", 0x70);
#if ENV_INCLUDE_BME280
static uint8_t init_bme280(TwoWire* wire, uint8_t addr) {
if (!BME280.begin(addr, wire)) return 0;
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::SAMPLING_X1,
Adafruit_BME280::FILTER_OFF,
Adafruit_BME280::STANDBY_MS_1000);
return 1;
}
static void query_bme280(uint8_t ch, uint8_t, CayenneLPP& lpp) {
if (BME280.takeForcedMeasurement()) {
lpp.addTemperature(ch, BME280.readTemperature());
lpp.addRelativeHumidity(ch, BME280.readHumidity());
lpp.addBarometricPressure(ch, BME280.readPressure() / 100);
lpp.addAltitude(ch, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
}
#endif
}
#endif
#if ENV_INCLUDE_BMP280
static uint8_t init_bmp280(TwoWire*, uint8_t addr) {
// BMP280 static instance was constructed with TELEM_WIRE; begin() uses it.
return BMP280.begin(addr) ? 1 : 0;
}
static void query_bmp280(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addTemperature(ch, BMP280.readTemperature());
lpp.addBarometricPressure(ch, BMP280.readPressure() / 100);
lpp.addAltitude(ch, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
}
#endif
#if ENV_INCLUDE_SHT4X
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
uint32_t serialNumber = 0;
int16_t sht4x_error;
sht4x_error = SHT4X.serialNumber(serialNumber);
if (sht4x_error == 0) {
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
SHT4X_initialized = true;
} else {
SHT4X_initialized = false;
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
}
#endif
#if ENV_INCLUDE_SHTC3
static uint8_t init_shtc3(TwoWire* wire, uint8_t) {
// Adafruit_SHTC3::begin() does not accept an address (fixed at 0x70).
return SHTC3.begin(wire) ? 1 : 0;
}
static void query_shtc3(uint8_t ch, uint8_t, CayenneLPP& lpp) {
sensors_event_t humidity, temp;
SHTC3.getEvent(&humidity, &temp);
lpp.addTemperature(ch, temp.temperature);
lpp.addRelativeHumidity(ch, humidity.relative_humidity);
}
#endif
#if ENV_INCLUDE_LPS22HB
if (LPS22HB.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
LPS22HB_initialized = true;
} else {
LPS22HB_initialized = false;
MESH_DEBUG_PRINTLN("LPS22HB was not found at I2C address %02X", 0x5C);
#if ENV_INCLUDE_SHT4X
static uint8_t init_sht4x(TwoWire* wire, uint8_t addr) {
// SensirionI2cSht4x::begin() does not probe the hardware; use serialNumber()
// as the actual presence check since it performs a real I2C transaction.
SHT4X.begin(*wire, addr);
uint32_t serial = 0;
return (SHT4X.serialNumber(serial) == 0) ? 1 : 0;
}
static void query_sht4x(uint8_t ch, uint8_t, CayenneLPP& lpp) {
float temperature, humidity;
if (SHT4X.measureLowestPrecision(temperature, humidity) == 0) {
lpp.addTemperature(ch, temperature);
lpp.addRelativeHumidity(ch, humidity);
}
#endif
}
#endif
#if ENV_INCLUDE_INA3221
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
#if ENV_INCLUDE_LPS22HB
static uint8_t init_lps22hb(TwoWire*, uint8_t) {
// LPS22HBClass is constructed with the wire reference; begin() uses it.
return LPS22HB.begin() ? 1 : 0;
}
static void query_lps22hb(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addTemperature(ch, LPS22HB.readTemperature());
lpp.addBarometricPressure(ch, LPS22HB.readPressure() * 10); // convert kPa to hPa
}
#endif
for(int i = 0; i < 3; i++) {
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE);
#if ENV_INCLUDE_INA3221
static uint8_t init_ina3221(TwoWire* wire, uint8_t addr) {
if (!INA3221.begin(addr, wire)) return 0;
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE);
}
// Each enabled hardware channel becomes its own telemetry channel.
uint8_t enabled = 0;
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
if (INA3221.isChannelEnabled(i)) enabled++;
}
return enabled > 0 ? enabled : 1;
}
static void query_ina3221(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
// sub_ch is the index of the nth enabled hardware channel.
uint8_t seen = 0;
for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
if (INA3221.isChannelEnabled(i)) {
if (seen == sub_ch) {
float v = INA3221.getBusVoltage(i);
float c = INA3221.getCurrentAmps(i);
lpp.addVoltage(ch, v);
lpp.addCurrent(ch, c);
lpp.addPower(ch, v * c);
return;
}
seen++;
}
INA3221_initialized = true;
} else {
INA3221_initialized = false;
MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS);
}
#endif
}
#endif
#if ENV_INCLUDE_INA219
if (INA219.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
INA219_initialized = true;
} else {
INA219_initialized = false;
MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA219
static uint8_t init_ina219(TwoWire* wire, uint8_t) {
// INA219 static instance was constructed with the address; begin() uses it.
return INA219.begin(wire) ? 1 : 0;
}
static void query_ina219(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addVoltage(ch, INA219.getBusVoltage_V());
lpp.addCurrent(ch, INA219.getCurrent_mA() / 1000.0f);
lpp.addPower(ch, INA219.getPower_mW() / 1000.0f);
}
#endif
#if ENV_INCLUDE_INA260
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
INA260_initialized = true;
} else {
INA260_initialized = false;
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA260
static uint8_t init_ina260(TwoWire* wire, uint8_t addr) {
return INA260.begin(addr, wire) ? 1 : 0;
}
static void query_ina260(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addVoltage(ch, INA260.readBusVoltage() / 1000.0f);
lpp.addCurrent(ch, INA260.readCurrent() / 1000.0f);
lpp.addPower(ch, INA260.readPower() / 1000.0f);
}
#endif
#if ENV_INCLUDE_INA226
if (INA226.begin()) {
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
INA226_initialized = true;
} else {
INA226_initialized = false;
MESH_DEBUG_PRINTLN("INA226 was not found at I2C address %02X", TELEM_INA226_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA226
static uint8_t init_ina226(TwoWire*, uint8_t) {
// INA226 static instance was constructed with address and wire.
if (!INA226.begin()) return 0;
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
return 1;
}
static void query_ina226(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addVoltage(ch, INA226.getBusVoltage());
lpp.addCurrent(ch, INA226.getCurrent_mA() / 1000.0f);
lpp.addPower(ch, INA226.getPower_mW() / 1000.0f);
}
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
MLX90614_initialized = true;
} else {
MLX90614_initialized = false;
MESH_DEBUG_PRINTLN("MLX90614 was not found at I2C address %02X", TELEM_MLX90614_ADDRESS);
}
#endif
#if ENV_INCLUDE_MLX90614
static uint8_t init_mlx90614(TwoWire* wire, uint8_t addr) {
return MLX90614.begin(addr, wire) ? 2 : 0; // 2 channels: object temp, ambient temp
}
static void query_mlx90614(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
if (sub_ch == 0)
lpp.addTemperature(ch, MLX90614.readObjectTempC());
else
lpp.addTemperature(ch, MLX90614.readAmbientTempC());
}
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
VL53L0X_initialized = true;
} else {
VL53L0X_initialized = false;
MESH_DEBUG_PRINTLN("VL53L0X was not found at I2C address %02X", TELEM_VL53L0X_ADDRESS);
}
#endif
#if ENV_INCLUDE_VL53L0X
static uint8_t init_vl53l0x(TwoWire* wire, uint8_t addr) {
return VL53L0X.begin(addr, false, wire) ? 1 : 0;
}
static void query_vl53l0x(uint8_t ch, uint8_t, CayenneLPP& lpp) {
VL53L0X_RangingMeasurementData_t measure;
VL53L0X.rangingTest(&measure, false);
lpp.addDistance(ch, measure.RangeStatus != 4 ? measure.RangeMilliMeter / 1000.0f : 0.0f);
}
#endif
#if ENV_INCLUDE_BMP085
// First argument is MODE (aka oversampling)
// choose ULTRALOWPOWER
if (BMP085.begin(0, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor BMP085");
BMP085_initialized = true;
} else {
BMP085_initialized = false;
MESH_DEBUG_PRINTLN("BMP085 was not found at I2C address %02X", 0x77);
}
#endif
#ifdef ENV_INCLUDE_BMP085
static uint8_t init_bmp085(TwoWire* wire, uint8_t) {
return BMP085.begin(0, wire) ? 1 : 0; // mode 0 = ULTRALOWPOWER
}
static void query_bmp085(uint8_t ch, uint8_t, CayenneLPP& lpp) {
lpp.addTemperature(ch, BMP085.readTemperature());
lpp.addBarometricPressure(ch, BMP085.readPressure() / 100);
lpp.addAltitude(ch, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
}
#endif
#if ENV_INCLUDE_RAK12035
RAK12035.setup(*TELEM_WIRE);
if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS);
RAK12035_initialized = true;
#if ENV_INCLUDE_RAK12035
static uint8_t init_rak12035(TwoWire* wire, uint8_t addr) {
// RAK12035 requires setup() before begin().
RAK12035.setup(*wire);
if (!RAK12035.begin(addr)) return 0;
#ifdef ENABLE_RAK12035_CALIBRATION
return 2; // moisture channel + calibration channel
#else
return 1;
#endif
}
static void query_rak12035(uint8_t ch, uint8_t sub_ch, CayenneLPP& lpp) {
if (sub_ch == 0) {
lpp.addTemperature(ch, RAK12035.get_sensor_temperature());
lpp.addPercentage(ch, RAK12035.get_sensor_moisture());
} else {
RAK12035_initialized = false;
MESH_DEBUG_PRINTLN("RAK12035 was not found at I2C address %02X", TELEM_RAK12035_ADDRESS);
#ifdef ENABLE_RAK12035_CALIBRATION
float cap = RAK12035.get_sensor_capacitance();
float wet = RAK12035.get_humidity_full();
float dry = RAK12035.get_humidity_zero();
lpp.addFrequency(ch, cap);
lpp.addTemperature(ch, wet);
lpp.addPower(ch, dry);
if (cap > dry) RAK12035.set_humidity_zero(cap);
if (cap < wet) RAK12035.set_humidity_full(cap);
#endif
}
#endif
return true;
}
#endif
bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
next_available_channel = TELEM_CHANNEL_SELF + 1;
// ============================================================
// Sensor descriptor table
//
// Each entry maps an I2C address to a sensor's init and query
// functions. Only entries whose ENV_INCLUDE_* guard is defined
// are compiled in. The sentinel at the end keeps the array
// non-empty regardless of which sensors are enabled.
//
// Ordering here determines channel assignment at runtime:
// the first detected+initialized sensor gets channel 2, the
// next gets channel 3, and so on.
// ============================================================
struct SensorDef {
uint8_t address;
const char* name;
uint8_t (*init)(TwoWire* wire, uint8_t address);
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
};
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); // allow lat/lon via telemetry even if no GPS is detected
}
static const SensorDef SENSOR_TABLE[] = {
#if ENV_INCLUDE_AHTX0
{ TELEM_AHTX_ADDRESS, "AHT10/AHT20", init_ahtx0, query_ahtx0 },
#endif
#ifdef ENV_INCLUDE_BME680
{ TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 },
#endif
#if ENV_INCLUDE_BME280
{ TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 },
#endif
#if ENV_INCLUDE_BMP280
{ TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 },
#endif
#if ENV_INCLUDE_SHTC3
{ 0x70, "SHTC3", init_shtc3, query_shtc3 },
#endif
#if ENV_INCLUDE_SHT4X
{ TELEM_SHT4X_ADDRESS, "SHT4X", init_sht4x, query_sht4x },
#endif
#if ENV_INCLUDE_LPS22HB
{ 0x5C, "LPS22HB", init_lps22hb, query_lps22hb },
#endif
#if ENV_INCLUDE_INA3221
{ TELEM_INA3221_ADDRESS, "INA3221", init_ina3221, query_ina3221 },
#endif
#if ENV_INCLUDE_INA219
{ TELEM_INA219_ADDRESS, "INA219", init_ina219, query_ina219 },
#endif
#if ENV_INCLUDE_INA260
{ TELEM_INA260_ADDRESS, "INA260", init_ina260, query_ina260 },
#endif
#if ENV_INCLUDE_INA226
{ TELEM_INA226_ADDRESS, "INA226", init_ina226, query_ina226 },
#endif
#if ENV_INCLUDE_MLX90614
{ TELEM_MLX90614_ADDRESS,"MLX90614", init_mlx90614, query_mlx90614 },
#endif
#if ENV_INCLUDE_VL53L0X
{ TELEM_VL53L0X_ADDRESS, "VL53L0X", init_vl53l0x, query_vl53l0x },
#endif
#ifdef ENV_INCLUDE_BMP085
{ 0x77, "BMP085", init_bmp085, query_bmp085 },
#endif
#if ENV_INCLUDE_RAK12035
{ TELEM_RAK12035_ADDRESS,"RAK12035", init_rak12035, query_rak12035 },
#endif
{ 0, nullptr, nullptr, nullptr } // sentinel — keeps the array non-empty
};
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TABLE[0])) - 1;
#if ENV_INCLUDE_AHTX0
if (AHTX0_initialized) {
sensors_event_t humidity, temp;
AHTX0.getEvent(&humidity, &temp);
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity);
}
#endif
// ============================================================
// begin() — scan the I2C bus, then initialize only what was
// found. A sensor whose address does not ACK during the scan
// is never touched by a library call, preventing hangs or
// crashes caused by absent or misbehaving hardware.
// ============================================================
#if ENV_INCLUDE_BME680
if (BME680_initialized) {
if (BME680.performReading()) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME680.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME680.humidity);
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME680.pressure / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
telemetry.addAnalogInput(next_available_channel, BME680.gas_resistance);
next_available_channel++;
}
}
#endif
bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_GPS
#ifdef RAK_WISBLOCK_GPS
rakGPSInit();
#else
initBasicGPS();
#endif
#endif
#if ENV_INCLUDE_BME280
if (BME280_initialized) {
if (BME280.takeForcedMeasurement()) { // trigger a fresh reading in forced mode
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature());
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
}
}
#if ENV_PIN_SDA && ENV_PIN_SCL
#ifdef NRF52_PLATFORM
Wire1.setPins(ENV_PIN_SDA, ENV_PIN_SCL);
Wire1.setClock(100000);
Wire1.begin();
#else
Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
#endif
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
#endif
#if ENV_INCLUDE_BMP280
if (BMP280_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP280.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
// Scan the I2C bus before touching any sensor library.
bool detected[128] = {};
scanI2CBus(TELEM_WIRE, detected);
// Walk the sensor table and initialize only detected devices.
_active_sensor_count = 0;
for (size_t i = 0; i < SENSOR_TABLE_SIZE && _active_sensor_count < MAX_ACTIVE_SENSORS; i++) {
const SensorDef& def = SENSOR_TABLE[i];
if (!detected[def.address]) {
MESH_DEBUG_PRINTLN("%s not detected at I2C address %02X", def.name, def.address);
continue;
}
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3_initialized) {
sensors_event_t humidity, temp;
SHTC3.getEvent(&humidity, &temp);
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity);
uint8_t n = def.init(TELEM_WIRE, def.address);
if (n == 0) {
MESH_DEBUG_PRINTLN("%s found at %02X but failed to initialize", def.name, def.address);
continue;
}
#endif
#if ENV_INCLUDE_SHT4X
if (SHT4X_initialized) {
float sht4x_humidity, sht4x_temperature;
int16_t sht4x_error;
sht4x_error = SHT4X.measureLowestPrecision(sht4x_temperature, sht4x_humidity);
if (sht4x_error == 0) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, sht4x_temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, sht4x_humidity);
}
MESH_DEBUG_PRINTLN("Found %s at address: %02X", def.name, def.address);
for (uint8_t sub = 0; sub < n && _active_sensor_count < MAX_ACTIVE_SENSORS; sub++) {
_active_sensors[_active_sensor_count++] = { def.query, sub };
}
#endif
}
#if ENV_INCLUDE_LPS22HB
if (LPS22HB_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, LPS22HB.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, LPS22HB.readPressure() * 10); // convert kPa to hPa
}
#endif
return true;
}
#if ENV_INCLUDE_INA3221
if (INA3221_initialized) {
for(int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
// add only enabled INA3221 channels to telemetry
if (INA3221.isChannelEnabled(i)) {
float voltage = INA3221.getBusVoltage(i);
float current = INA3221.getCurrentAmps(i);
telemetry.addVoltage(next_available_channel, voltage);
telemetry.addCurrent(next_available_channel, current);
telemetry.addPower(next_available_channel, voltage * current);
next_available_channel++;
}
}
}
#endif
// ============================================================
// querySensors() — GPS stays on channel 1; each active sensor
// gets the next available channel in the order it was
// initialized.
// ============================================================
#if ENV_INCLUDE_INA219
if (INA219_initialized) {
telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V());
telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000);
telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000);
next_available_channel++;
}
#endif
bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
next_available_channel = TELEM_CHANNEL_SELF + 1;
#if ENV_INCLUDE_INA260
if (INA260_initialized) {
telemetry.addVoltage(next_available_channel, INA260.readBusVoltage() / 1000);
telemetry.addCurrent(next_available_channel, INA260.readCurrent() / 1000);
telemetry.addPower(next_available_channel, INA260.readPower() / 1000);
next_available_channel++;
}
#endif
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
}
#if ENV_INCLUDE_INA226
if (INA226_initialized) {
telemetry.addVoltage(next_available_channel, INA226.getBusVoltage());
telemetry.addCurrent(next_available_channel, INA226.getCurrent_mA() / 1000.0);
telemetry.addPower(next_available_channel, INA226.getPower_mW() / 1000.0);
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
for (int i = 0; i < _active_sensor_count; i++) {
_active_sensors[i].query(next_available_channel, _active_sensors[i].sub_channel, telemetry);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, MLX90614.readObjectTempC());
telemetry.addTemperature(TELEM_CHANNEL_SELF + 1, MLX90614.readAmbientTempC());
}
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X_initialized) {
VL53L0X_RangingMeasurementData_t measure;
VL53L0X.rangingTest(&measure, false); // pass in 'true' to get debug data
if (measure.RangeStatus != 4) { // phase failures
telemetry.addDistance(TELEM_CHANNEL_SELF, measure.RangeMilliMeter / 1000.0f); // convert mm to m
} else {
telemetry.addDistance(TELEM_CHANNEL_SELF, 0.0f); // no valid measurement
}
}
#endif
#if ENV_INCLUDE_BMP085
if (BMP085_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP085.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP085.readPressure() / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
}
#endif
#if ENV_INCLUDE_RAK12035
if (RAK12035_initialized) {
// RAK12035 Telemetry is Channel 2
telemetry.addTemperature(2, RAK12035.get_sensor_temperature());
telemetry.addPercentage(2, RAK12035.get_sensor_moisture());
// RAK12035 CALIBRATION Telemetry is Channel 3, if enabled
#ifdef ENABLE_RAK12035_CALIBRATION
// Calibration Data Screen is Channel 3
float cap = RAK12035.get_sensor_capacitance();
float _wet = RAK12035.get_humidity_full();
float _dry = RAK12035.get_humidity_zero();
telemetry.addFrequency(3, cap);
telemetry.addTemperature(3, _wet);
telemetry.addPower(3, _dry);
if(cap > _dry){
RAK12035.set_humidity_zero(cap);
}
if(cap < _wet){
RAK12035.set_humidity_full(cap);
}
#endif
}
#endif
}
return true;
}
@ -555,8 +594,6 @@ const char* EnvironmentSensorManager::getSettingName(int i) const {
return "gps";
}
#endif
// convenient way to add params (needed for some tests)
// if (i == settings++) return "param.2";
return NULL;
}
@ -567,8 +604,6 @@ const char* EnvironmentSensorManager::getSettingValue(int i) const {
return gps_active ? "1" : "0";
}
#endif
// convenient way to add params ...
// if (i == settings++) return "2";
return NULL;
}
@ -584,11 +619,7 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val
}
if (strcmp(name, "gps_interval") == 0) {
uint32_t interval_seconds = atoi(value);
if (interval_seconds > 0) {
gps_update_interval_sec = interval_seconds;
} else {
gps_update_interval_sec = 1; // Default to 1 second if 0
}
gps_update_interval_sec = interval_seconds > 0 ? interval_seconds : 1;
return true;
}
#endif
@ -652,16 +683,10 @@ void EnvironmentSensorManager::rakGPSInit(){
//search for the correct IO standby pin depending on socket used
if(gpsIsAwake(WB_IO2)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19007/10");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket A");
}
else if(gpsIsAwake(WB_IO4)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19003/9");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket C");
}
else if(gpsIsAwake(WB_IO5)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19001/11");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket F");
}
else{
MESH_DEBUG_PRINTLN("No GPS found");
@ -708,15 +733,17 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
return true;
} else if (Serial1.available()) {
MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on");
#ifdef PIN_GPS_EN
if(PIN_GPS_EN){
gpsResetPin = PIN_GPS_EN;
}
#endif
serialGPSFlag = true;
gps_active = true;
gps_detected = true;
return true;
}
pinMode(ioPin, INPUT);
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
return false;

34
src/helpers/sensors/EnvironmentSensorManager.h

@ -6,27 +6,22 @@
class EnvironmentSensorManager : public SensorManager {
protected:
int next_available_channel = TELEM_CHANNEL_SELF + 1;
static const int MAX_ACTIVE_SENSORS = 16;
bool AHTX0_initialized = false;
bool BME280_initialized = false;
bool BMP280_initialized = false;
bool INA3221_initialized = false;
bool INA219_initialized = false;
bool INA260_initialized = false;
bool INA226_initialized = false;
bool SHTC3_initialized = false;
bool LPS22HB_initialized = false;
bool MLX90614_initialized = false;
bool VL53L0X_initialized = false;
bool SHT4X_initialized = false;
bool BME680_initialized = false;
bool BMP085_initialized = false;
bool RAK12035_initialized = false;
// Query function pointer + sub-channel index (for multi-channel sensors like INA3221).
// Sub-channel is 0 for all single-output sensors.
struct ActiveSensor {
void (*query)(uint8_t channel, uint8_t sub_channel, CayenneLPP& telemetry);
uint8_t sub_channel;
};
bool gps_detected = false;
bool gps_active = false;
uint32_t gps_update_interval_sec = 1; // Default 1 second
ActiveSensor _active_sensors[MAX_ACTIVE_SENSORS];
int _active_sensor_count = 0;
uint8_t next_available_channel = TELEM_CHANNEL_SELF + 1;
bool gps_detected = false;
bool gps_active = false;
uint32_t gps_update_interval_sec = 1;
#if ENV_INCLUDE_GPS
LocationProvider* _location;
@ -39,7 +34,6 @@ protected:
#endif
#endif
public:
#if ENV_INCLUDE_GPS
EnvironmentSensorManager(LocationProvider &location): _location(&location){};

2
src/helpers/sensors/RAK12035_SoilMoisture.cpp

@ -431,7 +431,7 @@ bool RAK12035_SoilMoisture::sensor_on()
delay(10); // Wait for the sensor code to complete initialization
*/
uint8_t v = 0;
time_t timeout = millis();
uint32_t timeout = millis();
while ((!query_sensor())) //Wait for sensor to respond to I2C commands,
{ //indicating it is ready
if ((millis() - timeout) > 50){ //0.5 second timeout for sensor to respond

8
src/helpers/ui/ST7789LCDDisplay.cpp

@ -27,12 +27,6 @@ bool ST7789LCDDisplay::begin() {
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
}
if (PIN_TFT_RST != -1) {
pinMode(PIN_TFT_RST, OUTPUT);
digitalWrite(PIN_TFT_RST, LOW);
delay(10);
digitalWrite(PIN_TFT_RST, HIGH);
}
// Im not sure if this is just a t-deck problem or not, if your display is slow try this.
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
@ -166,4 +160,4 @@ uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
void ST7789LCDDisplay::endFrame() {
// display.display();
}
}

1
src/helpers/ui/buzzer.cpp

@ -12,7 +12,6 @@ void genericBuzzer::begin() {
quiet(false);
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW); // need to pull low by default to avoid extreme power draw
startup();
}
void genericBuzzer::play(const char *melody) {

13
test/mocks/AES.h

@ -0,0 +1,13 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// Mock AES128 class for testing
// Provides minimal interface to allow Utils.cpp to compile
class AES128 {
public:
void setKey(const uint8_t* key, size_t keySize) {}
void encryptBlock(uint8_t* output, const uint8_t* input) {}
void decryptBlock(uint8_t* output, const uint8_t* input) {}
};

14
test/mocks/SHA256.h

@ -0,0 +1,14 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// Mock SHA256 class for testing
// Provides minimal interface to allow Utils.cpp to compile
class SHA256 {
public:
void update(const uint8_t* data, size_t len) {}
void finalize(uint8_t* hash, size_t hashLen) {}
void resetHMAC(const uint8_t* key, size_t keyLen) {}
void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {}
};

10
test/mocks/Stream.h

@ -0,0 +1,10 @@
#pragma once
// Mock Stream class for native testing
// Provides minimal interface needed by Utils.h
class Stream {
public:
virtual void print(char c) {}
virtual void print(const char* str) {}
};

57
test/test_utils/test_tohex.cpp

@ -0,0 +1,57 @@
#include <gtest/gtest.h>
#include "Utils.h"
using namespace mesh;
#define HEX_BUFFER_SIZE(input) (sizeof(input) * 2 + 1)
TEST(UtilsToHex, ConvertSingleByte) {
uint8_t input[] = {0xAB};
char output[HEX_BUFFER_SIZE(input)];
Utils::toHex(output, input, sizeof(input));
EXPECT_STREQ("AB", output);
}
TEST(UtilsToHex, ConvertMultipleBytes) {
uint8_t input[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
char output[HEX_BUFFER_SIZE(input)];
Utils::toHex(output, input, sizeof(input));
EXPECT_STREQ("0123456789ABCDEF", output);
}
TEST(UtilsToHex, ConvertZeroByte) {
uint8_t input[] = {0x00};
char output[HEX_BUFFER_SIZE(input)];
Utils::toHex(output, input, sizeof(input));
EXPECT_STREQ("00", output);
}
TEST(UtilsToHex, ConvertMaxByte) {
uint8_t input[] = {0xFF};
char output[HEX_BUFFER_SIZE(input)];
Utils::toHex(output, input, sizeof(input));
EXPECT_STREQ("FF", output);
}
TEST(UtilsToHex, NullTerminatesOnEmptyInput) {
uint8_t input[] = {0xAB};
char output[] = "X"; // Pre-fill with X.
Utils::toHex(output, input, 0);
// Should just null-terminate at position 0
EXPECT_EQ('\0', output[0]);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

5
variants/ebyte_eora_s3/platformio.ini

@ -135,3 +135,8 @@ build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
lib_deps =
${Ebyte_EoRa-S3.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:Ebyte_EoRa-S3_kiss_modem]
extends = Ebyte_EoRa-S3
build_src_filter = ${Ebyte_EoRa-S3.build_src_filter}
+<../examples/kiss_modem/>

15
variants/ebyte_eora_s3/target.cpp

@ -64,21 +64,6 @@ bool radio_init() {
return true; // success
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/ebyte_eora_s3/target.h

@ -23,7 +23,4 @@ extern SensorManager sensors;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

4
variants/gat562_30s_mesh_kit/platformio.ini

@ -112,3 +112,7 @@ lib_deps =
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:GAT562_30S_Mesh_Kit_kiss_modem]
extends = GAT562_30S_Mesh_Kit
build_src_filter = ${GAT562_30S_Mesh_Kit.build_src_filter}
+<../examples/kiss_modem/>

15
variants/gat562_30s_mesh_kit/target.cpp

@ -38,21 +38,6 @@ bool radio_init() {
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/gat562_30s_mesh_kit/target.h

@ -24,7 +24,4 @@ extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

5
variants/gat562_mesh_evb_pro/platformio.ini

@ -50,3 +50,8 @@ build_flags =
; -D MESH_DEBUG=1
build_src_filter = ${GAT562_Mesh_EVB_Pro.build_src_filter}
+<../examples/simple_room_server>
[env:GAT562_Mesh_EVB_Pro_kiss_modem]
extends = GAT562_Mesh_EVB_Pro
build_src_filter = ${GAT562_Mesh_EVB_Pro.build_src_filter}
+<../examples/kiss_modem/>

15
variants/gat562_mesh_evb_pro/target.cpp

@ -37,21 +37,6 @@ bool radio_init() {
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/gat562_mesh_evb_pro/target.h

@ -23,7 +23,4 @@ extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

4
variants/gat562_mesh_tracker_pro/platformio.ini

@ -106,3 +106,7 @@ lib_deps =
${GAT562_Mesh_Tracker_Pro.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:GAT562_Mesh_Tracker_Pro_kiss_modem]
extends = GAT562_Mesh_Tracker_Pro
build_src_filter = ${GAT562_Mesh_Tracker_Pro.build_src_filter}
+<../examples/kiss_modem/>

15
variants/gat562_mesh_tracker_pro/target.cpp

@ -38,21 +38,6 @@ bool radio_init() {
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/gat562_mesh_tracker_pro/target.h

@ -24,7 +24,4 @@ extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

4
variants/gat562_mesh_watch13/platformio.ini

@ -87,3 +87,7 @@ lib_deps =
${GAT562_Mesh_Watch13.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:GAT562_Mesh_Watch13_kiss_modem]
extends = GAT562_Mesh_Watch13
build_src_filter = ${GAT562_Mesh_Watch13.build_src_filter}
+<../examples/kiss_modem/>

15
variants/gat562_mesh_watch13/target.cpp

@ -29,21 +29,6 @@ bool radio_init() {
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/gat562_mesh_watch13/target.h

@ -25,7 +25,4 @@ extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

5
variants/generic-e22/platformio.ini

@ -95,6 +95,11 @@ lib_deps =
${Generic_E22.lib_deps}
${esp32_ota.lib_deps}
[env:Generic_E22_kiss_modem]
extends = Generic_E22
build_src_filter = ${Generic_E22.build_src_filter}
+<../examples/kiss_modem/>
[env:Generic_E22_sx1268_repeater]
extends = Generic_E22
build_src_filter = ${Generic_E22.build_src_filter}

15
variants/generic-e22/target.cpp

@ -27,21 +27,6 @@ bool radio_init() {
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/generic-e22/target.h

@ -15,8 +15,5 @@ extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

12
variants/generic_espnow/target.cpp

@ -17,18 +17,6 @@ bool radio_init() {
return true; // success
}
uint32_t radio_get_rng_seed() {
return millis() + radio_driver.intID(); // TODO: where to get some entropy?
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
// no-op
}
void radio_set_tx_power(int8_t dbm) {
radio_driver.setTxPower(dbm);
}
// NOTE: as we are using the WiFi radio, the ESP_IDF will have enabled hardware RNG:
// https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/random.html
class ESP_RNG : public mesh::RNG {

3
variants/generic_espnow/target.h

@ -10,7 +10,4 @@ extern ESP32RTCClock rtc_clock;
extern SensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

5
variants/heltec_ct62/platformio.ini

@ -150,3 +150,8 @@ build_src_filter = ${Heltec_ct62.build_src_filter}
lib_deps =
${Heltec_ct62.lib_deps}
${esp32_ota.lib_deps}
[env:Heltec_ct62_kiss_modem]
extends = Heltec_ct62
build_src_filter = ${Heltec_ct62.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_ct62/target.cpp

@ -16,21 +16,6 @@ bool radio_init() {
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_ct62/target.h

@ -14,7 +14,4 @@ extern AutoDiscoverRTCClock rtc_clock;
extern SensorManager sensors;
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

5
variants/heltec_e213/platformio.ini

@ -166,3 +166,8 @@ lib_deps =
${Heltec_E213_base.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:Heltec_E213_kiss_modem]
extends = Heltec_E213_base
build_src_filter = ${Heltec_E213_base.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_e213/target.cpp

@ -33,21 +33,6 @@ bool radio_init() {
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_e213/target.h

@ -23,7 +23,4 @@ extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

5
variants/heltec_e290/platformio.ini

@ -162,3 +162,8 @@ lib_deps =
${Heltec_E290_base.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:Heltec_E290_kiss_modem]
extends = Heltec_E290_base
build_src_filter = ${Heltec_E290_base.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_e290/target.cpp

@ -33,21 +33,6 @@ bool radio_init() {
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_e290/target.h

@ -23,7 +23,4 @@ extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

7
variants/heltec_mesh_solar/platformio.ini

@ -92,4 +92,9 @@ build_src_filter = ${Heltec_mesh_solar.build_src_filter}
+<../examples/companion_radio/*.cpp>
lib_deps =
${Heltec_mesh_solar.lib_deps}
densaugeo/base64 @ ~1.4.0
densaugeo/base64 @ ~1.4.0
[env:Heltec_mesh_solar_kiss_modem]
extends = Heltec_mesh_solar
build_src_filter = ${Heltec_mesh_solar.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_mesh_solar/target.cpp

@ -23,21 +23,6 @@ bool radio_init() {
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_mesh_solar/target.h

@ -40,7 +40,4 @@ extern SolarSensorManager sensors;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

2
variants/heltec_t096/LoRaFEMControl.cpp

@ -9,7 +9,7 @@ void LoRaFEMControl::init(void)
pinMode(P_LORA_KCT8103L_PA_CSD, OUTPUT);
digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH);
pinMode(P_LORA_KCT8103L_PA_CTX, OUTPUT);
digitalWrite(P_LORA_KCT8103L_PA_CTX, HIGH);
digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH);
setLnaCanControl(true);
}

9
variants/heltec_t096/platformio.ini

@ -92,7 +92,7 @@ build_src_filter = ${Heltec_t096.build_src_filter}
[env:Heltec_t096_room_server]
extends = Heltec_t096
build_src_filter = ${Heltec_t096.build_src_filter}
+<../examples/simple_room_server>
+<../examples/simple_room_server>
build_flags =
${Heltec_t096.build_flags}
-D ADVERT_NAME='"Heltec_t096 Room"'
@ -145,4 +145,9 @@ build_src_filter = ${Heltec_t096.build_src_filter}
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_t096.lib_deps}
densaugeo/base64 @ ~1.4.0
densaugeo/base64 @ ~1.4.0
[env:Heltec_t096_kiss_modem]
extends = Heltec_t096
build_src_filter = ${Heltec_t096.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_t096/target.cpp

@ -43,21 +43,6 @@ bool radio_init() {
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_t096/target.h

@ -27,7 +27,4 @@ extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

12
variants/heltec_t114/platformio.ini

@ -50,7 +50,6 @@ upload_protocol = nrfutil
extends = Heltec_t114
build_src_filter = ${Heltec_t114.build_src_filter}
+<../examples/simple_repeater>
build_flags =
${Heltec_t114.build_flags}
-D ADVERT_NAME='"Heltec_T114 Repeater"'
@ -127,10 +126,6 @@ build_flags =
-D DISPLAY_CLASS=NullDisplayDriver
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; -D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_t114.build_src_filter}
+<helpers/nrf52/*.cpp>
+<../examples/companion_radio/*.cpp>
@ -251,4 +246,9 @@ build_src_filter = ${Heltec_t114_with_display.build_src_filter}
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_t114_with_display.lib_deps}
densaugeo/base64 @ ~1.4.0
densaugeo/base64 @ ~1.4.0
[env:Heltec_t114_kiss_modem]
extends = Heltec_t114
build_src_filter = ${Heltec_t114.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_t114/target.cpp

@ -43,21 +43,6 @@ bool radio_init() {
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_t114/target.h

@ -29,7 +29,4 @@ extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

5
variants/heltec_t190/platformio.ini

@ -153,3 +153,8 @@ build_src_filter = ${Heltec_T190_base.build_src_filter}
lib_deps =
${Heltec_T190_base.lib_deps}
${esp32_ota.lib_deps}
[env:Heltec_T190_kiss_modem]
extends = Heltec_T190_base
build_src_filter = ${Heltec_T190_base.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_t190/target.cpp

@ -33,21 +33,6 @@ bool radio_init() {
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_t190/target.h

@ -23,7 +23,4 @@ extern MomentaryButton user_btn;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

5
variants/heltec_tracker/platformio.ini

@ -186,3 +186,8 @@ build_src_filter = ${Heltec_tracker_base.build_src_filter}
lib_deps =
${Heltec_tracker_base.lib_deps}
${esp32_ota.lib_deps}
[env:Heltec_Wireless_Tracker_kiss_modem]
extends = Heltec_tracker_base
build_src_filter = ${Heltec_tracker_base.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_tracker/target.cpp

@ -37,21 +37,6 @@ bool radio_init() {
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_tracker/target.h

@ -42,7 +42,4 @@ extern HWTSensorManager sensors;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

2
variants/heltec_tracker_v2/LoRaFEMControl.cpp

@ -14,7 +14,7 @@ void LoRaFEMControl::init(void)
pinMode(P_LORA_KCT8103L_PA_CSD, OUTPUT);
digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH);
pinMode(P_LORA_KCT8103L_PA_CTX, OUTPUT);
digitalWrite(P_LORA_KCT8103L_PA_CTX, HIGH);
digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH);
setLnaCanControl(true);
}

7
variants/heltec_tracker_v2/platformio.ini

@ -187,7 +187,7 @@ build_flags =
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
-D OFFLINE_QUEUE_SIZE=256
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_tracker_v2.build_src_filter}
@ -217,3 +217,8 @@ build_src_filter = ${Heltec_tracker_v2.build_src_filter}
lib_deps =
${Heltec_tracker_v2.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_tracker_v2_kiss_modem]
extends = Heltec_tracker_v2
build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+<../examples/kiss_modem/>

15
variants/heltec_tracker_v2/target.cpp

@ -39,21 +39,6 @@ bool radio_init() {
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity

3
variants/heltec_tracker_v2/target.h

@ -24,8 +24,5 @@ extern EnvironmentSensorManager sensors;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save