Browse Source

Merge branch 'dev' into rak-advert-hw-encryption

pull/2824/head
NickDunklee 2 weeks ago
committed by GitHub
parent
commit
59e9a79eac
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      .github/actions/setup-build-environment/action.yml
  2. 35
      .github/workflows/build-companion-firmwares.yml
  3. 35
      .github/workflows/build-repeater-firmwares.yml
  4. 35
      .github/workflows/build-room-server-firmwares.yml
  5. 90
      .github/workflows/firmware-builder.yml
  6. 3
      .github/workflows/pr-build-check.yml
  7. 32
      .github/workflows/stale-bot.yml
  8. 57
      SECURITY.md
  9. 43
      boards/heltec-rc32.json
  10. 43
      boards/heltec_v4_r8.json
  11. 12
      build.sh
  12. 49
      docs/cli_commands.md
  13. 27
      docs/faq.md
  14. 4
      docs/qr_codes.md
  15. 10
      examples/companion_radio/MyMesh.cpp
  16. 39
      examples/companion_radio/main.cpp
  17. 13
      examples/companion_radio/ui-new/UITask.cpp
  18. 2
      examples/companion_radio/ui-orig/UITask.cpp
  19. 2
      examples/companion_radio/ui-tiny/UITask.cpp
  20. 77
      examples/simple_repeater/UITask.cpp
  21. 5
      examples/simple_repeater/UITask.h
  22. 43
      examples/simple_repeater/main.cpp
  23. 35
      examples/simple_room_server/main.cpp
  24. 63
      src/helpers/NRF52Board.cpp
  25. 1
      src/helpers/NRF52Board.h
  26. 157
      src/helpers/nrf52/EthernetCLI.h
  27. 13
      src/helpers/nrf52/EthernetMac.h
  28. 268
      src/helpers/nrf52/SerialEthernetInterface.cpp
  29. 82
      src/helpers/nrf52/SerialEthernetInterface.h
  30. 50
      src/helpers/radiolib/CustomLR1110.h
  31. 3
      src/helpers/radiolib/CustomLR1110Wrapper.h
  32. 54
      src/helpers/radiolib/CustomSX1262.h
  33. 3
      src/helpers/radiolib/CustomSX1262Wrapper.h
  34. 18
      src/helpers/radiolib/RadioLibWrappers.cpp
  35. 5
      src/helpers/radiolib/RadioLibWrappers.h
  36. 7
      src/helpers/sensors/EnvironmentSensorManager.cpp
  37. 553
      src/helpers/ui/NV3001BDisplay.cpp
  38. 68
      src/helpers/ui/NV3001BDisplay.h
  39. 18
      src/helpers/ui/RotaryInput.h
  40. 11
      src/helpers/ui/ST7735Display.cpp
  41. 8
      src/helpers/ui/ST7789LCDDisplay.cpp
  42. 4
      src/helpers/ui/ST7789LCDDisplay.h
  43. 68
      variants/heltec_rc32/HeltecRC32Board.cpp
  44. 29
      variants/heltec_rc32/HeltecRC32Board.h
  45. 122
      variants/heltec_rc32/HeltecRC32RotaryInput.cpp
  46. 26
      variants/heltec_rc32/HeltecRC32RotaryInput.h
  47. 60
      variants/heltec_rc32/pins_arduino.h
  48. 348
      variants/heltec_rc32/platformio.ini
  49. 52
      variants/heltec_rc32/target.cpp
  50. 37
      variants/heltec_rc32/target.h
  51. 29
      variants/heltec_rc32/variant.h
  52. 2
      variants/heltec_t1/platformio.ini
  53. 86
      variants/heltec_v4_r8/HeltecV4R8Board.cpp
  54. 39
      variants/heltec_v4_r8/HeltecV4R8Board.h
  55. 52
      variants/heltec_v4_r8/LoRaFEMControl.cpp
  56. 24
      variants/heltec_v4_r8/LoRaFEMControl.h
  57. 56
      variants/heltec_v4_r8/pins_arduino.h
  58. 342
      variants/heltec_v4_r8/platformio.ini
  59. 45
      variants/heltec_v4_r8/target.cpp
  60. 31
      variants/heltec_v4_r8/target.h
  61. 4
      variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h
  62. 4
      variants/lilygo_techo/TechoBoard.h
  63. 4
      variants/lilygo_techo_card/TechoCardBoard.cpp
  64. 2
      variants/lilygo_techo_card/TechoCardBoard.h
  65. 4
      variants/lilygo_techo_lite/TechoBoard.h
  66. 14
      variants/rak4631/RAK4631Board.cpp
  67. 69
      variants/rak4631/platformio.ini
  68. 2
      variants/thinknode_m7/platformio.ini
  69. 2
      variants/xiao_nrf52/XiaoNrf52Board.h
  70. 1
      variants/xiao_nrf52/target.cpp
  71. 2
      variants/xiao_nrf52/target.h

13
.github/actions/setup-build-environment/action.yml

@ -25,5 +25,14 @@ runs:
- name: Extract Version from Git Tag
shell: bash
run: |
GIT_TAG_NAME="${GITHUB_REF#refs/tags/}"
echo "GIT_TAG_VERSION=${GIT_TAG_NAME##*-}" >> $GITHUB_ENV
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
# triggered by a tag push (e.g: refs/tags/companion-v1.2.3)
GIT_TAG_NAME="${GITHUB_REF#refs/tags/}"
VERSION_STRING="${GIT_TAG_NAME##*-}"
else
# triggered by a workflow dispatch (e.g: refs/heads/main)
# strip "refs/heads/" prefix and replace any remaining "/" with "-" to protect file paths
BRANCH_NAME="${GITHUB_REF#refs/heads/}"
VERSION_STRING=$(echo "$BRANCH_NAME" | tr '/' '-')
fi
echo "GIT_TAG_VERSION=${VERSION_STRING}" >> $GITHUB_ENV

35
.github/workflows/build-companion-firmwares.yml

@ -10,33 +10,8 @@ on:
- 'companion-*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmwares
env:
FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }}
run: /usr/bin/env bash build.sh build-companion-firmwares
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: companion-firmwares
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
name: Companion Firmware ${{ env.GIT_TAG_VERSION }}
body: ""
draft: true
files: out/*
build-companion-firmwares:
uses: ./.github/workflows/firmware-builder.yml
with:
firmware_type: 'companion'
release_title_prefix: 'Companion Firmware'

35
.github/workflows/build-repeater-firmwares.yml

@ -10,33 +10,8 @@ on:
- 'repeater-*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmwares
env:
FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }}
run: /usr/bin/env bash build.sh build-repeater-firmwares
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: repeater-firmwares
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
name: Repeater Firmware ${{ env.GIT_TAG_VERSION }}
body: ""
draft: true
files: out/*
build-repeater-firmwares:
uses: ./.github/workflows/firmware-builder.yml
with:
firmware_type: 'repeater'
release_title_prefix: 'Repeater Firmware'

35
.github/workflows/build-room-server-firmwares.yml

@ -10,33 +10,8 @@ on:
- 'room-server-*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmwares
env:
FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }}
run: /usr/bin/env bash build.sh build-room-server-firmwares
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: room-server-firmwares
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
name: Room Server Firmware ${{ env.GIT_TAG_VERSION }}
body: ""
draft: true
files: out/*
build-room-server-firmwares:
uses: ./.github/workflows/firmware-builder.yml
with:
firmware_type: 'room-server'
release_title_prefix: 'Room Server Firmware'

90
.github/workflows/firmware-builder.yml

@ -0,0 +1,90 @@
name: Firmware Builder
on:
workflow_call:
inputs:
firmware_type:
required: true
type: string
release_title_prefix:
required: true
type: string
jobs:
generate-build-matrix:
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.get-build-targets.outputs.targets }}
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Get Build Targets
id: get-build-targets
run: |
# get list of firmwares to build
TARGET_LIST=$(/usr/bin/env bash build.sh get-${{ inputs.firmware_type }}-firmwares-to-build)
# convert targets separated by new line into a json array string
JSON_ARRAY=$(echo "$TARGET_LIST" | jq -R -s -c 'split("\n") | map(select(length > 0))')
# use json array as targets result
echo "targets=$JSON_ARRAY" >> $GITHUB_OUTPUT
build:
needs: generate-build-matrix
runs-on: ubuntu-latest
continue-on-error: true # don't fail entire build if one board fails to build
strategy:
matrix:
target: ${{ fromJson(needs.generate-build-matrix.outputs.targets) }}
fail-fast: false # don't cancel other builds if one board fails to build
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmware
env:
FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }}
run: /usr/bin/env bash build.sh build-firmware ${{ matrix.target }}
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: "${{ matrix.target }}"
path: out
create-release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') # only create release for tagged builds
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Download All Artifacts
uses: actions/download-artifact@v8
with:
merge-multiple: true
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
with:
name: "${{ inputs.release_title_prefix }} ${{ env.GIT_TAG_VERSION }}"
body: ""
draft: true
files: out/*

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

@ -31,8 +31,11 @@ jobs:
- Heltec_v3_room_server
# nRF52
- RAK_4631_companion_radio_ble
- RAK_4631_companion_radio_ethernet
- RAK_4631_repeater
- RAK_4631_repeater_ethernet
- RAK_4631_room_server
- RAK_4631_room_server_ethernet
# RP2040
- PicoW_repeater
# STM32

32
.github/workflows/stale-bot.yml

@ -0,0 +1,32 @@
name: 'Run Stale Bot'
on:
schedule:
- cron: '30 1 * * *' # daily at 1:30am
workflow_dispatch: {}
permissions:
actions: write
issues: write
pull-requests: write
jobs:
close-issues:
# only run on main repo, not forks
if: github.repository == 'meshcore-dev/MeshCore'
runs-on: ubuntu-latest
steps:
- name: Close Stale Issues
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# auto close issues
days-before-issue-stale: 60
days-before-issue-close: 7
exempt-issue-labels: "keep-open"
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 60 days with no activity. Remove the stale label or add a comment if this issue is still relevant, otherwise this issue will automatically close in 7 days."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
# don't auto close prs
days-before-pr-stale: -1
days-before-pr-close: -1

57
SECURITY.md

@ -0,0 +1,57 @@
# Security Policy
## Supported Versions
Security fixes are applied to the latest release only. We do not backport
fixes to older versions.
| Version | Supported |
|---------|-----------|
| 1.15+ | ✅ |
| <1.15 | |
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
Use GitHub's private vulnerability reporting instead:
1. Go to the **Security** tab of this repository
2. Click **Report a vulnerability**
3. Fill in the details and submit
### What to include
A useful report tells us:
- Which component or file is affected
- What an attacker can do (impact) and under what conditions
- A minimal reproduction case or proof-of-concept if you have one
- Whether you believe it is remotely exploitable
You do not need a working exploit to report. An incomplete report is better
than no report.
## What to expect
This is a volunteer-maintained open-source project. We will do our best to
respond in a reasonable timeframe, but cannot commit to specific deadlines.
We ask that you give us a fair opportunity to investigate and address the
issue before any public disclosure. If you have not heard back after
**90 days**, feel free to follow up or proceed with disclosure at your
discretion.
## Scope
In scope:
- Remote code execution, memory corruption, or denial-of-service via crafted
radio packets
- Authentication or encryption bypasses
- Vulnerabilities in the packet routing or path handling logic
Out of scope:
- Physical access attacks (e.g., JTAG, UART extraction of keys)
- Regulatory compliance (duty cycle, frequency restrictions)
- Jamming or other physical-layer radio interference
- Issues in third-party libraries (RadioLib, Crypto, etc.) — report those
upstream
- "Best practice" suggestions without a demonstrated attack path

43
boards/heltec-rc32.json

@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_rc32"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec RC32 (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "Heltec"
}

43
boards/heltec_v4_r8.json

@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_v4_r8"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}

12
build.sh

@ -1,5 +1,8 @@
#!/usr/bin/env bash
# exit when any command fails
set -e
global_usage() {
cat - <<EOF
Usage:
@ -93,7 +96,7 @@ get_pio_envs_ending_with_string() {
# $1 should be the environment name
get_platform_for_env() {
local env_name=$1
echo "$PIO_CONFIG_JSON" | python3 -c "
printf '%s' "$PIO_CONFIG_JSON" | python3 -c "
import sys, json, re
data = json.load(sys.stdin)
for section, options in data:
@ -275,4 +278,11 @@ elif [[ $1 == "build-repeater-firmwares" ]]; then
build_repeater_firmwares
elif [[ $1 == "build-room-server-firmwares" ]]; then
build_room_server_firmwares
elif [[ $1 == "get-companion-firmwares-to-build" ]]; then
get_pio_envs_ending_with_string "_companion_radio_usb"
get_pio_envs_ending_with_string "_companion_radio_ble"
elif [[ $1 == "get-repeater-firmwares-to-build" ]]; then
get_pio_envs_ending_with_string "_repeater"
elif [[ $1 == "get-room-server-firmwares-to-build" ]]; then
get_pio_envs_ending_with_string "_room_server"
fi

49
docs/cli_commands.md

@ -19,6 +19,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
- [GPS](#gps-when-gps-support-is-compiled-in)
- [Sensors](#sensors-when-sensor-support-is-compiled-in)
- [Bridge](#bridge-when-bridge-support-is-compiled-in)
- [Ethernet](#ethernet-when-ethernet-support-is-compiled-in)
---
@ -28,12 +29,25 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Usage:**
- `reboot`
**Note:** No reply is sent.
---
### Power-off the node
**Usage:**
- `poweroff`, or
- `shutdown`
**Note:** No reply is sent.
---
### Reset the clock and reboot
**Usage:**
- `clkreboot`
**Note:** No reply is sent.
---
### Sync the clock with the remote device
@ -660,10 +674,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore
**Parameters:**
- `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set)
**Default:** `0xFF` - indicates it hasn't been set, will track flood.max until it is.
**Default:** `64` - (`0xFF` indicates it hasn't been set, will track flood.max until it is.)
**Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region.
---
#### Limit the number of hops for an advert flood message
**Usage:**
- `get flood.max.advert`
- `set flood.max.advert <value>`
**Parameters:**
- `value`: Maximum flood hop count (0-64) for an advert packet
**Default:** `8`
---
@ -1128,3 +1153,25 @@ region save
**Note:** Returns an error on boards without power management support.
---
### Ethernet (when Ethernet support is compiled in)
Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_ethernet` firmware variants (e.g. `RAK_4631_repeater_ethernet`) to enable this feature.
---
#### View Ethernet connection status
**Usage:**
- `eth.status`
**Output:**
- `ETH: <ip>:<port>` when connected (e.g. `ETH: 192.168.1.50:23`)
- `ETH: not connected` when Ethernet is not active
**Notes:**
- Available on repeater and room server firmware only. Companion radio ethernet firmware does not expose a CLI.
- The Ethernet interface obtains an IP address via DHCP automatically on boot.
- A TCP server listens on port 23 (default) for CLI connections.
- Connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial.
---

27
docs/faq.md

@ -83,6 +83,7 @@ A list of frequently-asked questions and answers for MeshCore
- [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code)
- [7.6. Q: How do I connect to the companion via Wi-Fi, e.g. using a Heltec V3?](#76-q-how-do-i-connect-to-the-companion-via-wi-fi-eg-using-a-heltec-v3)
- [7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with an EByte E22-900M30S or an EByte E22-900M33S module, what should their transmit power be set to?](#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-an-ebyte-e22-900m30s-or-an-ebyte-e22-900m33s-module-what-should-their-transmit-power-be-set-to)
- [7.8. Q: How do I use Ethernet with a RAK4631?](#78-q-how-do-i-use-ethernet-with-a-rak4631)
## 1. Introduction
@ -820,3 +821,29 @@ For companion radios, you can set these radios' transmit power in the smartphone
| **Ikoka Stick E22-900M33S** | 2W Model | 9 dBm | 2W | **DO NOT EXCEED** (Risk of burn out) [data sheet](https://www.cdebyte.com/pdf-down.aspx?id=4216) Refer to your local government's requirements |
| **Heltec V4** | Standard Output | 10 dBm | 22 dBm (~0.15W) | |
| | High Output | 22 dBm | 28 dBm (~0.5W to 0.6W) | |
---
### 7.8. Q: How do I use Ethernet with a RAK4631?
**A:**
MeshCore supports Ethernet on RAK4631 boards using the [RAK13800](https://docs.rakwireless.com/product-categories/wisblock/rak13800/datasheet/) WisBlock Ethernet module (based on the W5100S chip).
**Hardware required:**
- RAK4631 WisBlock Core
- RAK19007 or RAK19018 WisBlock Base Board (with an available IO slot)
- RAK13800 WisBlock Ethernet module
- Ethernet cable connected to a network with a DHCP server
**Firmware:**
Flash one of the Ethernet-enabled firmware variants:
- `RAK_4631_repeater_ethernet` - Repeater with Ethernet CLI access
- `RAK_4631_room_server_ethernet` - Room server with Ethernet CLI access
- `RAK_4631_companion_radio_ethernet` - Companion radio over Ethernet (replaces BLE)
**Connecting:**
- The device obtains an IP address via DHCP automatically on boot.
- For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. `nc <ip> 23` or PuTTY in raw mode). This gives you the same CLI available over serial/USB.
- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio).
- Use the `eth.status` CLI command to check connection status and see the assigned IP address.
---

4
docs/qr_codes.md

@ -12,8 +12,10 @@ meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72
**Parameters**:
- `name`: Channel name (URL-encoded if needed)
- `name`: Channel name (URL-encoded)
- `secret`: 16-byte secret represented as 32 hex characters
- `region_scope`: Region Scope (optional, URL-encoded if provided)
- Supported by MeshCore App v1.47.0+
## Add Contact

10
examples/companion_radio/MyMesh.cpp

@ -657,6 +657,11 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
// query other sensors -- target specific
sensors.querySensors(permissions, telemetry);
float temperature = board.getMCUTemperature();
if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
}
memcpy(reply, &sender_timestamp,
4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
@ -1640,6 +1645,11 @@ void MyMesh::handleCmdFrame(size_t len) {
} else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
float temperature = board.getMCUTemperature();
if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
}
// query other sensors -- target specific
sensors.querySensors(0xFF, telemetry);

39
examples/companion_radio/main.cpp

@ -18,13 +18,13 @@ static uint32_t _atoi(const char* sp) {
#include <CustomLFS_QSPIFlash.h>
DataStore store(InternalFS, QSPIFlash, rtc_clock);
#else
#if defined(EXTRAFS)
#include <CustomLFS.h>
CustomLFS ExtraFS(0xD4000, 0x19000, 128);
DataStore store(InternalFS, ExtraFS, rtc_clock);
#else
DataStore store(InternalFS, rtc_clock);
#endif
#if defined(EXTRAFS)
#include <CustomLFS.h>
CustomLFS ExtraFS(0xD4000, 0x19000, 128);
DataStore store(InternalFS, ExtraFS, rtc_clock);
#else
DataStore store(InternalFS, rtc_clock);
#endif
#endif
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
@ -74,6 +74,9 @@ static uint32_t _atoi(const char* sp) {
#ifdef BLE_PIN_CODE
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#elif defined(ETHERNET_ENABLED)
#include <helpers/nrf52/SerialEthernetInterface.h>
SerialEthernetInterface serial_interface;
#else
#include <helpers/ArduinoSerialInterface.h>
ArduinoSerialInterface serial_interface;
@ -113,7 +116,6 @@ void halt() {
void setup() {
Serial.begin(115200);
board.begin();
#ifdef HAS_EXTERNAL_WATCHDOG
@ -162,10 +164,23 @@ void setup() {
#ifdef BLE_PIN_CODE
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
the_mesh.startInterface(serial_interface);
#elif defined(ETHERNET_ENABLED)
Serial.print("Waiting for serial to connect...\n");
unsigned long timeout = millis();
while (!Serial) {
if ((millis() - timeout) < 5000) { delay(100); } else { break; }
}
Serial.println("Initializing Ethernet adapter...");
if (serial_interface.begin()) {
the_mesh.startInterface(serial_interface);
} else {
Serial.println("ETH: Init failed, continuing without Ethernet (mesh only)");
}
#else
serial_interface.begin(Serial);
#endif
the_mesh.startInterface(serial_interface);
#endif
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
store.begin();
@ -256,7 +271,11 @@ void loop() {
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
#ifdef ETHERNET_ENABLED
serial_interface.loop();
#endif
if (!the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible

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

@ -697,9 +697,6 @@ void UITask::shutdown(bool restart){
if (restart) {
_board->reboot();
} else {
// still necessary until all boards are refactored to use poweroff
_display->turnOff();
radio_driver.powerOff();
// Power off board including radio, display, GPS and components
_board->powerOff();
}
@ -750,6 +747,16 @@ void UITask::loop() {
c = handleTripleClick(KEY_SELECT);
}
#endif
#if defined(UI_HAS_ROTARY_INPUT)
RotaryInputEvent rotaryEv = rotary_input.poll();
if (c == 0 && _display != NULL && _display->isOn()) {
if (rotaryEv == RotaryInputEvent::Next) {
c = KEY_NEXT;
} else if (rotaryEv == RotaryInputEvent::Prev) {
c = KEY_PREV;
}
}
#endif
#if defined(PIN_USER_BTN_ANA)
if (abs(millis() - _analogue_pin_read_millis) > 10) {
int ev = analog_btn.check();

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

@ -307,8 +307,6 @@ void UITask::shutdown(bool restart){
if (restart) {
_board->reboot();
} else {
_display->turnOff();
radio_driver.powerOff();
// Power off board including radio, display, GPS and components
_board->powerOff();
}

2
examples/companion_radio/ui-tiny/UITask.cpp

@ -566,8 +566,6 @@ void UITask::shutdown(bool restart){
if (restart) {
_board->reboot();
} else {
_display->turnOff();
radio_driver.powerOff();
// Power off board including radio, display, GPS and components
_board->powerOff();
}

77
examples/simple_repeater/UITask.cpp

@ -1,4 +1,5 @@
#include "UITask.h"
#include "target.h"
#include <Arduino.h>
#include <helpers/CommonCLI.h>
@ -9,6 +10,8 @@
#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
#define POWEROFF_DELAY 3000
// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
@ -29,9 +32,14 @@ static const uint8_t meshcore_logo [] PROGMEM = {
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_started_at = millis();
_node_prefs = node_prefs;
_display->turnOn();
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
user_btn.begin();
#endif
// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version);
@ -47,7 +55,7 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
void UITask::renderCurrScreen() {
char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo
_display->setColor(DisplayDriver::BLUE);
int logoWidth = 128;
@ -57,24 +65,34 @@ void UITask::renderCurrScreen() {
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);
_display->drawTextCentered(_display->width() / 2, 22, website);
// version info
_display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 35);
_display->print(_version_info);
_display->drawTextCentered(_display->width() / 2, 35, _version_info);
// node type
const char* node_type = "< Repeater >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 48);
_display->print(node_type);
} else { // home screen
// node name
_display->drawTextCentered(_display->width() / 2, 48, node_type);
} else if (_powering_off_at > 0) {
// meshcore logo
_display->setColor(DisplayDriver::BLUE);
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);
_display->drawTextCentered(_display->width()/ 2, 22, website);
// Powering off
const char* poweroff_string = "Turning OFF";
uint16_t poffWidth = _display->getTextWidth(poweroff_string);
_display->setCursor((_display->width() - poffWidth) / 2, 48);
_display->drawTextCentered(_display->width()/2, 48, poweroff_string);
} else {
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
@ -94,21 +112,19 @@ void UITask::renderCurrScreen() {
}
void UITask::loop() {
#ifdef PIN_USER_BTN
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == USER_BTN_PRESSED) { // pressed?
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
}
_prevBtnState = btnState;
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_next_read = millis() + 200; // 5 reads per second
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_display->turnOn();
Serial.println("Powering Off");
_powering_off_at = millis() + POWEROFF_DELAY;
}
#endif
@ -124,4 +140,13 @@ void UITask::loop() {
_display->turnOff();
}
}
if (_powering_off_at > 0) { // power off timer armed
#ifdef LED_PIN
digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff
#endif
if (millis() > _powering_off_at) {
_board->powerOff(); // should not return
}
}
}

5
examples/simple_repeater/UITask.h

@ -4,15 +4,18 @@
#include <helpers/CommonCLI.h>
class UITask {
mesh::MainBoard* _board;
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
NodePrefs* _node_prefs;
char _version_info[32];
unsigned long _powering_off_at = 0;
unsigned long _started_at = 0;
void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
UITask(mesh::MainBoard& board, DisplayDriver& display) : _board(&board), _display(&display) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
void loop();

43
examples/simple_repeater/main.cpp

@ -5,7 +5,12 @@
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
static UITask ui_task(board, display);
#endif
#ifdef ETHERNET_ENABLED
#define ETHERNET_CLI_BANNER "MeshCore Repeater CLI"
#include <helpers/nrf52/EthernetCLI.h>
#endif
StdRNG fast_rng;
@ -18,6 +23,9 @@ void halt() {
}
static char command[160];
#ifdef ETHERNET_ENABLED
static char ethernet_command[160];
#endif
// For power saving
unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot
@ -90,6 +98,9 @@ void setup() {
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
command[0] = 0;
#ifdef ETHERNET_ENABLED
ethernet_command[0] = 0;
#endif
sensors.begin();
@ -99,6 +110,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
#ifdef ETHERNET_ENABLED
ethernet_start_task();
#endif
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
@ -108,6 +123,7 @@ void setup() {
}
void loop() {
// Handle Serial CLI
int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read();
@ -126,7 +142,14 @@ void loop() {
Serial.print('\n');
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
reply[0] = 0;
#ifdef ETHERNET_ENABLED
if (!ethernet_handle_command(command, reply)) {
the_mesh.handleCommand(0, command, reply);
}
#else
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
#endif
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
@ -134,7 +157,20 @@ void loop() {
command[0] = 0; // reset command buffer
}
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_)
#ifdef ETHERNET_ENABLED
ethernet_loop_maintain();
if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) {
char reply[160];
reply[0] = 0;
if (!ethernet_handle_command(ethernet_command, reply)) {
the_mesh.handleCommand(0, ethernet_command, reply);
}
ethernet_send_reply(reply);
ethernet_command[0] = 0;
}
#endif
#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) && !defined(DISPLAY_CLASS)
// Hold the user button to power off the SenseCAP Solar repeater.
int btnState = digitalRead(PIN_USER_BTN);
if (btnState == LOW) {
@ -160,9 +196,6 @@ void loop() {
external_watchdog.loop();
#endif
if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) {
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.feed();
#endif
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
#else

35
examples/simple_room_server/main.cpp

@ -3,6 +3,11 @@
#include "MyMesh.h"
#ifdef ETHERNET_ENABLED
#define ETHERNET_CLI_BANNER "MeshCore Room Server CLI"
#include <helpers/nrf52/EthernetCLI.h>
#endif
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
@ -17,6 +22,9 @@ void halt() {
}
static char command[MAX_POST_TEXT_LEN+1];
#ifdef ETHERNET_ENABLED
static char ethernet_command[MAX_POST_TEXT_LEN+1];
#endif
void setup() {
Serial.begin(115200);
@ -71,6 +79,9 @@ void setup() {
mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
command[0] = 0;
#ifdef ETHERNET_ENABLED
ethernet_command[0] = 0;
#endif
sensors.begin();
@ -80,6 +91,10 @@ void setup() {
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
#ifdef ETHERNET_ENABLED
ethernet_start_task();
#endif
// send out initial zero hop Advertisement to the mesh
#if ENABLE_ADVERT_ON_BOOT == 1
the_mesh.sendSelfAdvertisement(16000, false);
@ -105,7 +120,14 @@ void loop() {
if (len > 0 && command[len - 1] == '\r') { // received complete line
command[len - 1] = 0; // replace newline with C string null terminator
char reply[160];
reply[0] = 0;
#ifdef ETHERNET_ENABLED
if (!ethernet_handle_command(command, reply)) {
the_mesh.handleCommand(0, command, reply);
}
#else
the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial!
#endif
if (reply[0]) {
Serial.print(" -> "); Serial.println(reply);
}
@ -113,6 +135,19 @@ void loop() {
command[0] = 0; // reset command buffer
}
#ifdef ETHERNET_ENABLED
ethernet_loop_maintain();
if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) {
char reply[160];
reply[0] = 0;
if (!ethernet_handle_command(ethernet_command, reply)) {
the_mesh.handleCommand(0, ethernet_command, reply);
}
ethernet_send_reply(reply);
ethernet_command[0] = 0;
}
#endif
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS

63
src/helpers/NRF52Board.cpp

@ -280,16 +280,29 @@ void NRF52Board::sleep(uint32_t secs) {
// Temperature from NRF52 MCU
float NRF52Board::getMCUTemperature() {
NRF_TEMP->TASKS_START = 1; // Start temperature measurement
long startTime = millis();
while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us
if(millis() - startTime > 5) { // To wait 5ms just in case
NRF_TEMP->TASKS_STOP = 1;
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
uint32_t err_code;
int32_t temp;
err_code = sd_temp_get(&temp);
if (err_code == NRF_SUCCESS) {
return (float)temp * 0.25f;
} else {
return NAN;
}
} else {
NRF_TEMP->TASKS_START = 1; // Start temperature measurement
long startTime = millis();
while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us
if(millis() - startTime > 5) { // To wait 5ms just in case
NRF_TEMP->TASKS_STOP = 1;
return NAN;
}
}
}
NRF_TEMP->EVENTS_DATARDY = 0; // Clear event flag
int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units
@ -298,17 +311,41 @@ float NRF52Board::getMCUTemperature() {
return temp * 0.25f; // Convert to *C
}
void NRF52Board::powerOff() {
void NRF52Board::shutdownPeripherals() {
// Power off the display if any
#ifdef DISPLAY_CLASS
display.turnOff();
if (display.isOn()) {
display.turnOff();
}
#endif
// Prep LoRa radio for power down
#ifdef P_LORA_RESET
digitalWrite(P_LORA_RESET, HIGH); // preload OUT latch so pinMode can't glitch NRESET low
pinMode(P_LORA_RESET, OUTPUT);
digitalWrite(P_LORA_RESET, LOW); // deliberate hardware reset (datasheet: >=100us)
delayMicroseconds(200);
digitalWrite(P_LORA_RESET, HIGH);
#endif
#if defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI)
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin(); // SPI may not be started on some shutdown paths, need it to shut down radio
#endif
#ifdef P_LORA_BUSY
pinMode(P_LORA_BUSY, INPUT);
uint32_t started_at = millis();
while (digitalRead(P_LORA_BUSY) && millis() - started_at < 10) {} //wait for radio to be ready
#endif
#ifdef P_LORA_NSS
pinMode(P_LORA_NSS, OUTPUT);
digitalWrite(P_LORA_NSS, HIGH);
#endif
// Power off LoRa
radio_driver.powerOff();
// Keep LoRa inactive during deepsleep
digitalWrite(P_LORA_NSS, HIGH);
#ifdef P_LORA_NSS
digitalWrite(P_LORA_NSS, HIGH);
#endif
// Power off GPS if any
if(sensors.getLocationProvider() != NULL) {
@ -318,6 +355,10 @@ void NRF52Board::powerOff() {
// Flush serial buffers
Serial.flush();
delay(100);
}
void NRF52Board::powerOff() {
shutdownPeripherals();
// Enter SYSTEMOFF
uint8_t sd_enabled = 0;

1
src/helpers/NRF52Board.h

@ -50,6 +50,7 @@ public:
virtual uint8_t getStartupReason() const override { return startup_reason; }
virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); }
virtual void shutdownPeripherals();
virtual void powerOff() override;
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
virtual bool startOTAUpdate(const char *id, char reply[]) override;

157
src/helpers/nrf52/EthernetCLI.h

@ -0,0 +1,157 @@
#pragma once
#ifdef ETHERNET_ENABLED
#include <Arduino.h>
#include <SPI.h>
#include <RAK13800_W5100S.h>
#include <helpers/nrf52/EthernetMac.h>
#define PIN_SPI1_MISO (29)
#define PIN_SPI1_MOSI (30)
#define PIN_SPI1_SCK (3)
static SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI);
#define PIN_ETHERNET_POWER_EN WB_IO2
#define PIN_ETHERNET_RESET 21
#define PIN_ETHERNET_SS 26
#ifndef ETHERNET_TCP_PORT
#define ETHERNET_TCP_PORT 23 // telnet port for CLI access
#endif
#ifndef ETHERNET_CLI_BANNER
#define ETHERNET_CLI_BANNER "MeshCore CLI"
#endif
#define ETHERNET_RETRY_INTERVAL_MS 30000
static EthernetServer ethernet_server(ETHERNET_TCP_PORT);
static EthernetClient ethernet_client;
static volatile bool ethernet_running = false;
// FreeRTOS task: handles hw init, DHCP, and retries in the background
static void ethernet_task(void* param) {
(void)param;
Serial.println("ETH: Initializing hardware");
// WB_IO2 (power enable) is already driven HIGH by early constructor
// in RAK4631Board.cpp to support POE boot.
// Skip hardware reset — the W5100S comes out of power-on reset cleanly,
// and toggling reset kills the PHY link which breaks POE power.
pinMode(PIN_ETHERNET_RESET, OUTPUT);
digitalWrite(PIN_ETHERNET_RESET, HIGH);
ETHERNET_SPI_PORT.begin();
Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS);
uint8_t mac[6];
generateEthernetMac(mac);
Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// Retry loop: keep trying until we get an IP
while (!ethernet_running) {
Serial.println("ETH: Attempting DHCP...");
if (Ethernet.begin(mac, 10000, 2000) == 0) {
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println("ETH: Hardware not found, giving up");
vTaskDelete(NULL);
return;
}
if (Ethernet.linkStatus() == LinkOFF) {
Serial.println("ETH: Cable not connected, will retry");
} else {
Serial.println("ETH: DHCP failed, will retry");
}
vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS));
continue;
}
IPAddress ip = Ethernet.localIP();
Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);
Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT);
ethernet_server.begin();
ethernet_running = true;
}
// DHCP succeeded, task is done
vTaskDelete(NULL);
}
static void ethernet_start_task() {
xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL);
}
// Format ethernet status into reply buffer. Returns true if command was handled.
static bool ethernet_handle_command(const char* command, char* reply) {
if (strcmp(command, "eth.status") == 0) {
if (!ethernet_running) {
strcpy(reply, "ETH: not connected");
} else {
IPAddress ip = Ethernet.localIP();
sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT);
}
return true;
}
return false;
}
// Check for new TCP client connections, replacing any existing connection.
// Use accept() (not available()) so we only see newly-accepted sockets;
// available() also returns existing connected sockets that have data, which
// would force us to disambiguate every inbound packet from a real new client.
static void ethernet_check_client() {
auto newClient = ethernet_server.accept();
if (newClient) {
if (ethernet_client) ethernet_client.stop();
ethernet_client = newClient;
IPAddress ip = ethernet_client.remoteIP();
Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]);
ethernet_client.println(ETHERNET_CLI_BANNER);
}
}
// Call from loop() to maintain DHCP and check for new clients
static void ethernet_loop_maintain() {
if (ethernet_running) {
ethernet_check_client();
Ethernet.maintain();
}
}
// Read a line from the Ethernet client into the command buffer.
// Returns true when a complete line is ready to process (command is null-terminated).
// The caller should process the command and then reset ethernet_command[0] = 0.
static bool ethernet_read_line(char* ethernet_command, size_t buf_size) {
if (!ethernet_running || !ethernet_client || !ethernet_client.connected()) return false;
int elen = strlen(ethernet_command);
while (ethernet_client.available() && elen < (int)buf_size - 1) {
char c = ethernet_client.read();
if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF)
if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; }
ethernet_command[elen++] = c;
ethernet_command[elen] = 0;
}
if (elen == (int)buf_size - 1) {
ethernet_command[buf_size - 1] = '\r';
}
if (elen > 0 && ethernet_command[elen - 1] == '\r') {
ethernet_command[elen - 1] = 0;
ethernet_client.println();
return true;
}
return false;
}
// Send a reply to the Ethernet client
static void ethernet_send_reply(const char* reply) {
if (reply[0]) {
ethernet_client.print(" -> "); ethernet_client.println(reply);
}
}
#endif // ETHERNET_ENABLED

13
src/helpers/nrf52/EthernetMac.h

@ -0,0 +1,13 @@
#pragma once
#include <Arduino.h>
static inline void generateEthernetMac(uint8_t mac[6]) {
uint32_t device_id = NRF_FICR->DEVICEID[0];
mac[0] = 0x02;
mac[1] = 0x92;
mac[2] = 0x1F;
mac[3] = (device_id >> 16) & 0xFF;
mac[4] = (device_id >> 8) & 0xFF;
mac[5] = device_id & 0xFF;
}

268
src/helpers/nrf52/SerialEthernetInterface.cpp

@ -0,0 +1,268 @@
#ifdef ETHERNET_ENABLED
#include "SerialEthernetInterface.h"
#include "EthernetMac.h"
#include <SPI.h>
#include <EthernetUdp.h>
#define PIN_SPI1_MISO (29) // (0 + 29)
#define PIN_SPI1_MOSI (30) // (0 + 30)
#define PIN_SPI1_SCK (3) // (0 + 3)
SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI);
#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable
#define PIN_ETHERNET_RESET 21
#define PIN_ETHERNET_SS 26
#define RECV_STATE_IDLE 0
#define RECV_STATE_HDR_FOUND 1
#define RECV_STATE_LEN1_FOUND 2
#define RECV_STATE_LEN2_FOUND 3
bool SerialEthernetInterface::begin() {
ETHERNET_DEBUG_PRINTLN("Ethernet initializing");
// WB_IO2 (power enable) is already driven HIGH by early constructor
// in RAK4631Board.cpp to support POE boot.
// Skip hardware reset — the W5100S comes out of power-on reset cleanly,
// and toggling reset kills the PHY link which breaks POE power.
#ifdef PIN_ETHERNET_RESET
pinMode(PIN_ETHERNET_RESET, OUTPUT);
digitalWrite(PIN_ETHERNET_RESET, HIGH);
#endif
uint8_t mac[6];
generateEthernetMac(mac);
ETHERNET_DEBUG_PRINTLN(
"Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X",
mac[0],
mac[1],
mac[2],
mac[3],
mac[4],
mac[5]);
ETHERNET_DEBUG_PRINTLN("Init");
ETHERNET_SPI_PORT.begin();
Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS);
// Use static IP if build flags are defined, otherwise DHCP
#if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS)
IPAddress ip(ETHERNET_STATIC_IP);
IPAddress gateway(ETHERNET_STATIC_GATEWAY);
IPAddress subnet(ETHERNET_STATIC_SUBNET);
IPAddress dns(ETHERNET_STATIC_DNS);
Ethernet.begin(mac, ip, dns, gateway, subnet);
#else
ETHERNET_DEBUG_PRINTLN("Begin");
if (Ethernet.begin(mac) == 0) {
ETHERNET_DEBUG_PRINTLN("Begin failed.");
// DHCP failed -- let's figure out why
if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present.
{
ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found.");
return false;
}
if (Ethernet.linkStatus() == LinkOFF) // No physical connection
{
ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected.");
return false;
}
ETHERNET_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason.");
return false;
}
#endif
ETHERNET_DEBUG_PRINTLN("Ethernet begin complete");
ETHERNET_DEBUG_PRINT_IP("IP", Ethernet.localIP());
ETHERNET_DEBUG_PRINT_IP("Subnet", Ethernet.subnetMask());
ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP());
server.begin(); // start listening for clients
ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT);
return true;
}
void SerialEthernetInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
}
void SerialEthernetInterface::disable() {
_isEnabled = false;
}
size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len);
return 0;
}
if (deviceConnected && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
send_queue[send_queue_len].len = len; // add to send queue
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}
return 0;
}
bool SerialEthernetInterface::isWriteBusy() const {
return false;
}
size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) {
// Use accept() (not available()) so we only see newly-accepted sockets.
// available() also returns existing connected sockets that have data,
// which would cause us to treat each inbound packet as a "new client"
// and stop() the underlying socket — disconnecting the companion.
auto newClient = server.accept();
if (newClient) {
IPAddress new_ip = newClient.remoteIP();
uint16_t new_port = newClient.remotePort();
ETHERNET_DEBUG_PRINTLN(
"New client accepted %u.%u.%u.%u:%u",
new_ip[0],
new_ip[1],
new_ip[2],
new_ip[3],
new_port);
deviceConnected = false;
if (client) {
ETHERNET_DEBUG_PRINTLN("Closing previous client");
client.stop();
}
_state = RECV_STATE_IDLE;
_frame_len = 0;
_rx_len = 0;
client = newClient;
ETHERNET_DEBUG_PRINTLN("Switched to new client");
}
if (client.connected()) {
if (!deviceConnected) {
ETHERNET_DEBUG_PRINTLN(
"Got connection %u.%u.%u.%u:%u",
client.remoteIP()[0],
client.remoteIP()[1],
client.remoteIP()[2],
client.remoteIP()[3],
client.remotePort());
deviceConnected = true;
}
} else {
if (deviceConnected) {
deviceConnected = false;
ETHERNET_DEBUG_PRINTLN("Disconnected");
}
}
if (deviceConnected) {
if (send_queue_len > 0) { // first, check send queue
_last_write = millis();
int len = send_queue[0].len;
#if ETHERNET_RAW_LINE
ETHERNET_DEBUG_PRINTLN("TX line len=%d", len);
client.write(send_queue[0].buf, len);
client.write("\r\n", 2);
#else
uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames
pkt[0] = '>';
pkt[1] = (len & 0xFF); // LSB
pkt[2] = (len >> 8); // MSB
memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len);
ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len);
#if ETHERNET_DEBUG_LOGGING && ARDUINO
ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len);
#endif
client.write(pkt, 3 + len);
#endif
send_queue_len--;
for (int i = 0; i < send_queue_len; i++) { // delete top item from queue
send_queue[i] = send_queue[i + 1];
}
} else {
while (client.available()) {
int c = client.read();
if (c < 0) break;
#if ETHERNET_RAW_LINE
if (c == '\r' || c == '\n') {
if (_rx_len == 0) {
continue;
}
uint16_t out_len = _rx_len;
if (out_len > MAX_FRAME_SIZE) {
out_len = MAX_FRAME_SIZE;
}
memcpy(dest, _rx_buf, out_len);
_rx_len = 0;
return out_len;
}
if (_rx_len < MAX_FRAME_SIZE) {
_rx_buf[_rx_len] = (uint8_t)c;
_rx_len++;
}
#else
switch (_state) {
case RECV_STATE_IDLE:
if (c == '<') {
_state = RECV_STATE_HDR_FOUND;
}
break;
case RECV_STATE_HDR_FOUND:
_frame_len = (uint8_t)c;
_state = RECV_STATE_LEN1_FOUND;
break;
case RECV_STATE_LEN1_FOUND:
_frame_len |= ((uint16_t)c) << 8;
_rx_len = 0;
_state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE;
break;
default:
if (_rx_len < MAX_FRAME_SIZE) {
_rx_buf[_rx_len] = (uint8_t)c;
}
_rx_len++;
if (_rx_len >= _frame_len) {
if (_frame_len > MAX_FRAME_SIZE) {
_frame_len = MAX_FRAME_SIZE;
}
#if ETHERNET_DEBUG_LOGGING && ARDUINO
ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len);
#endif
memcpy(dest, _rx_buf, _frame_len);
_state = RECV_STATE_IDLE;
return _frame_len;
}
}
#endif
}
}
}
return 0;
}
bool SerialEthernetInterface::isConnected() const {
return deviceConnected;
}
void SerialEthernetInterface::loop() {
Ethernet.maintain();
}
#endif // ETHERNET_ENABLED

82
src/helpers/nrf52/SerialEthernetInterface.h

@ -0,0 +1,82 @@
#pragma once
#ifdef ETHERNET_ENABLED
#include "helpers/BaseSerialInterface.h"
#include <SPI.h>
#include <RAK13800_W5100S.h>
#ifndef ETHERNET_TCP_PORT
#define ETHERNET_TCP_PORT 5000
#endif
// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets
class SerialEthernetInterface : public BaseSerialInterface {
bool deviceConnected;
bool _isEnabled;
unsigned long _last_write;
uint8_t _state;
uint16_t _frame_len;
uint16_t _rx_len;
uint8_t _rx_buf[MAX_FRAME_SIZE];
EthernetServer server;
EthernetClient client;
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
#define FRAME_QUEUE_SIZE 4
int send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers() {
send_queue_len = 0;
_state = 0;
_frame_len = 0;
_rx_len = 0;
}
protected:
public:
SerialEthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) {
deviceConnected = false;
_isEnabled = false;
_last_write = 0;
send_queue_len = 0;
_state = 0;
_frame_len = 0;
_rx_len = 0;
}
bool begin();
// BaseSerialInterface methods
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
void loop();
};
#if ETHERNET_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__)
#define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__)
#define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3])
#else
#define ETHERNET_DEBUG_PRINT(...) {}
#define ETHERNET_DEBUG_PRINTLN(...) {}
#define ETHERNET_DEBUG_PRINT_IP(...) {}
#endif
#endif // ETHERNET_ENABLED

50
src/helpers/radiolib/CustomLR1110.h

@ -4,6 +4,10 @@
#include "MeshCore.h"
class CustomLR1110 : public LR1110 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
bool _rx_boosted = false;
public:
@ -32,9 +36,49 @@ class CustomLR1110 : public LR1110 {
bool getRxBoostedGainMode() const { return _rx_boosted; }
bool isReceiving() {
uint16_t irq = getIrqStatus();
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
return detected;
uint32_t irq = getIrqStatus();
bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5
bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6
uint32_t now = millis();
if (hdrErr) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
}
uint8_t getSpreadingFactor() const { return spreadingFactor; }

3
src/helpers/radiolib/CustomLR1110Wrapper.h

@ -14,6 +14,9 @@ public:
((CustomLR1110 *)_radio)->setBandwidth(bw);
((CustomLR1110 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomLR1110 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomLR1110 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
}
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); }

54
src/helpers/radiolib/CustomSX1262.h

@ -3,10 +3,12 @@
#include <RadioLib.h>
#include "MeshCore.h"
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
#define SX126X_IRQ_PREAMBLE_DETECTED 0x04
class CustomSX1262 : public SX1262 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
public:
CustomSX1262(Module *mod) : SX1262(mod) { }
@ -99,9 +101,49 @@ class CustomSX1262 : public SX1262 {
}
bool isReceiving() {
uint16_t irq = getIrqFlags();
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED);
return detected;
uint32_t irq = getIrqFlags();
bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2
bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4
bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5
uint32_t now = millis();
if (hdrErr) {
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID);
_activityAt = 0;
_headerSeen = false;
return false;
}
if (header) {
if (!_headerSeen) { _headerSeen = true; _activityAt = now; };
if (now - _activityAt > _maxPayloadMillis) {
MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis);
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID);
_activityAt = 0; _headerSeen = false;
return false;
}
return true;
}
if (preamble) {
if (_activityAt == 0) _activityAt = now;
if (now - _activityAt > _preambleMillis) {
clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED);
_activityAt = 0;
MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis);
return false;
}
return true;
}
_activityAt = 0; _headerSeen = false;
return false;
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
}
void setMaxPayloadMillis(uint32_t payloadMillis) {
_maxPayloadMillis = payloadMillis;
MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis);
}
bool getRxBoostedGainMode() {

3
src/helpers/radiolib/CustomSX1262Wrapper.h

@ -18,6 +18,9 @@ public:
((CustomSX1262 *)_radio)->setBandwidth(bw);
((CustomSX1262 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomSX1262 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomSX1262 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
}
bool isReceivingPacket() override {

18
src/helpers/radiolib/RadioLibWrappers.cpp

@ -228,3 +228,21 @@ float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) {
return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty));
}
PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols) {
// based on RadioLib's calculateTimeOnAir()
uint32_t tsym_us = ((uint32_t)10000 << sf) / (bw * 10);
uint32_t sfCoeff1_x4 = (sf == 5 || sf == 6) ? 25 : 17; // 6.25 : 4.25, semtech magic numbers to account for sync word + sfd
// preamble + syncword + sfd + header
uint32_t preamble_us = (((preambleSymbols + 8) * 4 + sfCoeff1_x4) * tsym_us) / 4;
// airtime for max packet at current radio settings
uint32_t total_us = _radio->getTimeOnAir(MAX_TRANS_UNIT);
// airtime for payload only (no preamble, header or SOF)
uint32_t payload_us = total_us > preamble_us ? total_us - preamble_us : 4000 - preamble_us; // fallback to 4 secs at worst case
// rescale payload_us for max possible CR
if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; }
return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000};
}

5
src/helpers/radiolib/RadioLibWrappers.h

@ -6,6 +6,10 @@
#ifdef USE_CC310_HW_CRYPTO
#include <Adafruit_nRFCrypto.h>
#endif
struct PacketMillis {
uint32_t preambleMillis; // preamble-detect -> header-valid deadline
uint32_t payloadMillis; // header-valid -> rx-done deadline
};
class RadioLibWrapper : public mesh::Radio {
protected:
@ -51,6 +55,7 @@ public:
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)); }
PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols);
virtual int16_t performChannelScan();
int getNoiseFloor() const override { return _noise_floor; }

7
src/helpers/sensors/EnvironmentSensorManager.cpp

@ -807,6 +807,13 @@ void EnvironmentSensorManager::rakGPSInit(){
bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
#if defined(ETHERNET_ENABLED) && defined(RAK_BOARD)
if (ioPin == WB_IO2) {
// WB_IO2 powers the Ethernet module on RAK baseboards.
return false;
}
#endif
//set initial waking state
pinMode(ioPin,OUTPUT);
digitalWrite(ioPin,LOW);

553
src/helpers/ui/NV3001BDisplay.cpp

@ -0,0 +1,553 @@
#include "NV3001BDisplay.h"
#include <Arduino.h>
#include <string.h>
#ifndef SPI_FREQUENCY
#define SPI_FREQUENCY 8000000
#endif
#ifndef PIN_TFT_SCL
#error "PIN_TFT_SCL must be defined"
#endif
#ifndef PIN_TFT_SDA
#error "PIN_TFT_SDA must be defined"
#endif
#ifndef PIN_TFT_CS
#error "PIN_TFT_CS must be defined"
#endif
#ifndef PIN_TFT_DC
#error "PIN_TFT_DC must be defined"
#endif
#ifndef PIN_TFT_MISO
#define PIN_TFT_MISO -1
#endif
#ifndef PIN_TFT_RST
#define PIN_TFT_RST -1
#endif
#ifndef PIN_TFT_EN
#define PIN_TFT_EN -1
#endif
#ifndef PIN_TFT_BL
#define PIN_TFT_BL -1
#endif
#ifndef PIN_TFT_EN_ACTIVE
#define PIN_TFT_EN_ACTIVE LOW
#endif
#ifndef PIN_TFT_BL_ACTIVE
#define PIN_TFT_BL_ACTIVE HIGH
#endif
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 0
#endif
#ifndef NV3001B_SCREEN_WIDTH
#define NV3001B_SCREEN_WIDTH 220
#endif
#ifndef NV3001B_SCREEN_HEIGHT
#define NV3001B_SCREEN_HEIGHT 128
#endif
#ifndef DISPLAY_SCALE_X
#define DISPLAY_SCALE_X ((float)NV3001B_SCREEN_WIDTH / NV3001B_LOGICAL_WIDTH)
#endif
#ifndef DISPLAY_SCALE_Y
#define DISPLAY_SCALE_Y ((float)NV3001B_SCREEN_HEIGHT / NV3001B_LOGICAL_HEIGHT)
#endif
#define NV3001B_SWRESET 0x01
#define NV3001B_SLPOUT 0x11
#define NV3001B_DISPON 0x29
#define NV3001B_CASET 0x2A
#define NV3001B_RASET 0x2B
#define NV3001B_RAMWR 0x2C
#define NV3001B_MADCTL 0x36
#define NV3001B_COLMOD 0x3A
#define NV3001B_MADCTL_MY 0x80
#define NV3001B_MADCTL_MX 0x40
#define NV3001B_MADCTL_MV 0x20
#define NV3001B_MADCTL_RGB 0x00
#ifndef NV3001B_TEXT_SIZE1_SCALE_X
#define NV3001B_TEXT_SIZE1_SCALE_X 1
#endif
#ifndef NV3001B_TEXT_SIZE1_SCALE_Y
#define NV3001B_TEXT_SIZE1_SCALE_Y 2
#endif
#ifndef NV3001B_TEXT_SIZE2_SCALE_X
#define NV3001B_TEXT_SIZE2_SCALE_X 2
#endif
#ifndef NV3001B_TEXT_SIZE2_SCALE_Y
#define NV3001B_TEXT_SIZE2_SCALE_Y 3
#endif
static uint16_t mapColor(DisplayDriver::Color c) {
switch (c) {
case DisplayDriver::DARK: return 0x0000;
case DisplayDriver::LIGHT: return 0xffff;
case DisplayDriver::RED: return 0xf800;
case DisplayDriver::GREEN: return 0x07e0;
case DisplayDriver::BLUE: return 0x001f;
case DisplayDriver::YELLOW: return 0xffe0;
case DisplayDriver::ORANGE: return 0xfd20;
default: return 0xffff;
}
}
static int scaleX(int x) {
return (int)(x * DISPLAY_SCALE_X);
}
static int scaleY(int y) {
return (int)(y * DISPLAY_SCALE_Y);
}
static int scaleWidth(int x, int w) {
if (w <= 0) return 0;
int scaled = scaleX(x + w) - scaleX(x);
return scaled > 0 ? scaled : 1;
}
static int scaleHeight(int y, int h) {
if (h <= 0) return 0;
int scaled = scaleY(y + h) - scaleY(y);
return scaled > 0 ? scaled : 1;
}
static uint8_t nv3001bMADCTL(uint8_t rotation) {
uint8_t madctl;
switch (rotation & 3) {
case 0:
madctl = NV3001B_MADCTL_MY | NV3001B_MADCTL_MV | NV3001B_MADCTL_RGB;
break;
case 1:
madctl = NV3001B_MADCTL_MY | NV3001B_MADCTL_MX | NV3001B_MADCTL_RGB;
break;
case 2:
madctl = NV3001B_MADCTL_RGB;
break;
default:
madctl = NV3001B_MADCTL_MX | NV3001B_MADCTL_MV | NV3001B_MADCTL_RGB;
break;
}
return madctl;
}
static const uint8_t font5x7[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x14,
0x7f, 0x14, 0x7f, 0x14, 0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x23, 0x13, 0x08, 0x64, 0x62, 0x36, 0x49,
0x55, 0x22, 0x50, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, 0x1c, 0x22, 0x41, 0x00, 0x00, 0x41, 0x22,
0x1c, 0x00, 0x14, 0x08, 0x3e, 0x08, 0x14, 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00, 0x50, 0x30, 0x00,
0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x60, 0x60, 0x00, 0x00, 0x20, 0x10, 0x08, 0x04, 0x02,
0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00, 0x42, 0x7f, 0x40, 0x00, 0x42, 0x61, 0x51, 0x49, 0x46, 0x21,
0x41, 0x45, 0x4b, 0x31, 0x18, 0x14, 0x12, 0x7f, 0x10, 0x27, 0x45, 0x45, 0x45, 0x39, 0x3c, 0x4a,
0x49, 0x49, 0x30, 0x01, 0x71, 0x09, 0x05, 0x03, 0x36, 0x49, 0x49, 0x49, 0x36, 0x06, 0x49, 0x49,
0x29, 0x1e, 0x00, 0x36, 0x36, 0x00, 0x00, 0x00, 0x56, 0x36, 0x00, 0x00, 0x08, 0x14, 0x22, 0x41,
0x00, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, 0x41, 0x22, 0x14, 0x08, 0x02, 0x01, 0x51, 0x09, 0x06,
0x32, 0x49, 0x79, 0x41, 0x3e, 0x7e, 0x11, 0x11, 0x11, 0x7e, 0x7f, 0x49, 0x49, 0x49, 0x36, 0x3e,
0x41, 0x41, 0x41, 0x22, 0x7f, 0x41, 0x41, 0x22, 0x1c, 0x7f, 0x49, 0x49, 0x49, 0x41, 0x7f, 0x09,
0x09, 0x09, 0x01, 0x3e, 0x41, 0x49, 0x49, 0x7a, 0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00, 0x41, 0x7f,
0x41, 0x00, 0x20, 0x40, 0x41, 0x3f, 0x01, 0x7f, 0x08, 0x14, 0x22, 0x41, 0x7f, 0x40, 0x40, 0x40,
0x40, 0x7f, 0x02, 0x0c, 0x02, 0x7f, 0x7f, 0x04, 0x08, 0x10, 0x7f, 0x3e, 0x41, 0x41, 0x41, 0x3e,
0x7f, 0x09, 0x09, 0x09, 0x06, 0x3e, 0x41, 0x51, 0x21, 0x5e, 0x7f, 0x09, 0x19, 0x29, 0x46, 0x46,
0x49, 0x49, 0x49, 0x31, 0x01, 0x01, 0x7f, 0x01, 0x01, 0x3f, 0x40, 0x40, 0x40, 0x3f, 0x1f, 0x20,
0x40, 0x20, 0x1f, 0x3f, 0x40, 0x38, 0x40, 0x3f, 0x63, 0x14, 0x08, 0x14, 0x63, 0x07, 0x08, 0x70,
0x08, 0x07, 0x61, 0x51, 0x49, 0x45, 0x43, 0x00, 0x7f, 0x41, 0x41, 0x00, 0x02, 0x04, 0x08, 0x10,
0x20, 0x00, 0x41, 0x41, 0x7f, 0x00, 0x04, 0x02, 0x01, 0x02, 0x04, 0x40, 0x40, 0x40, 0x40, 0x40,
0x00, 0x01, 0x02, 0x04, 0x00, 0x20, 0x54, 0x54, 0x54, 0x78, 0x7f, 0x48, 0x44, 0x44, 0x38, 0x38,
0x44, 0x44, 0x44, 0x20, 0x38, 0x44, 0x44, 0x48, 0x7f, 0x38, 0x54, 0x54, 0x54, 0x18, 0x08, 0x7e,
0x09, 0x01, 0x02, 0x0c, 0x52, 0x52, 0x52, 0x3e, 0x7f, 0x08, 0x04, 0x04, 0x78, 0x00, 0x44, 0x7d,
0x40, 0x00, 0x20, 0x40, 0x44, 0x3d, 0x00, 0x7f, 0x10, 0x28, 0x44, 0x00, 0x00, 0x41, 0x7f, 0x40,
0x00, 0x7c, 0x04, 0x18, 0x04, 0x78, 0x7c, 0x08, 0x04, 0x04, 0x78, 0x38, 0x44, 0x44, 0x44, 0x38,
0x7c, 0x14, 0x14, 0x14, 0x08, 0x08, 0x14, 0x14, 0x18, 0x7c, 0x7c, 0x08, 0x04, 0x04, 0x08, 0x48,
0x54, 0x54, 0x54, 0x20, 0x04, 0x3f, 0x44, 0x40, 0x20, 0x3c, 0x40, 0x40, 0x20, 0x7c, 0x1c, 0x20,
0x40, 0x20, 0x1c, 0x3c, 0x40, 0x30, 0x40, 0x3c, 0x44, 0x28, 0x10, 0x28, 0x44, 0x0c, 0x50, 0x50,
0x50, 0x3c, 0x44, 0x64, 0x54, 0x4c, 0x44, 0x00, 0x08, 0x36, 0x41, 0x00, 0x00, 0x00, 0x7f, 0x00,
0x00, 0x00, 0x41, 0x36, 0x08, 0x00, 0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00, 0x06, 0x09, 0x09, 0x06
};
static int textPixelScaleX(uint8_t size) {
return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_X : NV3001B_TEXT_SIZE2_SCALE_X;
}
static int textPixelScaleY(uint8_t size) {
return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_Y : NV3001B_TEXT_SIZE2_SCALE_Y;
}
static void setupOptionalOutput(int pin, int level) {
if (pin < 0) return;
pinMode(pin, OUTPUT);
digitalWrite(pin, level);
}
static void writeOptionalPin(int pin, int level) {
if (pin < 0) return;
digitalWrite(pin, level);
}
void NV3001BDisplay::writeCommand(uint8_t cmd) {
spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0));
digitalWrite(PIN_TFT_DC, LOW);
digitalWrite(PIN_TFT_CS, LOW);
spi.transfer(cmd);
digitalWrite(PIN_TFT_CS, HIGH);
spi.endTransaction();
}
void NV3001BDisplay::writeBytes(const uint8_t* data, size_t len) {
if (!data || len == 0) return;
spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0));
digitalWrite(PIN_TFT_DC, HIGH);
digitalWrite(PIN_TFT_CS, LOW);
for (size_t i = 0; i < len; i++) {
spi.transfer(data[i]);
}
digitalWrite(PIN_TFT_CS, HIGH);
spi.endTransaction();
}
void NV3001BDisplay::writeCommandData(uint8_t cmd, const uint8_t* data, size_t len) {
writeCommand(cmd);
writeBytes(data, len);
}
void NV3001BDisplay::setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
uint16_t x2 = x + w - 1;
uint16_t y2 = y + h - 1;
uint8_t data[4];
data[0] = x >> 8;
data[1] = x & 0xff;
data[2] = x2 >> 8;
data[3] = x2 & 0xff;
writeCommandData(NV3001B_CASET, data, sizeof(data));
data[0] = y >> 8;
data[1] = y & 0xff;
data[2] = y2 >> 8;
data[3] = y2 & 0xff;
writeCommandData(NV3001B_RASET, data, sizeof(data));
writeCommand(NV3001B_RAMWR);
}
void NV3001BDisplay::writeColor(uint16_t rgb, uint32_t count) {
uint8_t hi = rgb >> 8;
uint8_t lo = rgb & 0xff;
spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0));
digitalWrite(PIN_TFT_DC, HIGH);
digitalWrite(PIN_TFT_CS, LOW);
while (count--) {
spi.transfer(hi);
spi.transfer(lo);
}
digitalWrite(PIN_TFT_CS, HIGH);
spi.endTransaction();
}
void NV3001BDisplay::initPanel() {
#define CMD0(C) do { writeCommand(C); } while (0)
#define CMD1(C, A) do { const uint8_t d[] = { A }; writeCommandData(C, d, sizeof(d)); } while (0)
#define CMD2(C, A, B) do { const uint8_t d[] = { A, B }; writeCommandData(C, d, sizeof(d)); } while (0)
CMD0(NV3001B_SWRESET);
delay(120);
CMD1(0xFF, 0xA5);
CMD1(0x41, 0x00);
CMD1(0x50, 0x02);
CMD1(0x52, 0x6E);
CMD1(0x57, 0x02);
CMD1(0x46, 0x11);
CMD2(0x47, 0x00, 0x01);
CMD2(0x8F, 0x22, 0x03);
CMD1(0x9A, 0x78);
CMD1(0x9B, 0x78);
CMD1(0x9C, 0xA0);
CMD1(0x9D, 0x17);
CMD1(0x9E, 0xC1);
CMD1(0x83, 0x5A);
CMD1(0x84, 0xB6);
CMD1(0xFF, 0xA5);
CMD1(0x85, 0x5F);
CMD1(0x6E, 0x0F);
CMD1(0x7E, 0x0F);
CMD1(0x60, 0x00);
CMD1(0x70, 0x00);
CMD1(0x6D, 0x33);
CMD1(0x7D, 0x37);
CMD1(0x61, 0x09);
CMD1(0x71, 0x0A);
CMD1(0x6C, 0x2A);
CMD1(0x7C, 0x36);
CMD1(0x62, 0x11);
CMD1(0x72, 0x10);
CMD1(0x68, 0x4E);
CMD1(0x78, 0x4E);
CMD1(0x66, 0x36);
CMD1(0x76, 0x3C);
CMD1(0x1A, 0x1C);
CMD1(0x7B, 0x14);
CMD1(0x63, 0x0D);
CMD1(0x73, 0x0A);
CMD1(0x6A, 0x16);
CMD1(0x7A, 0x12);
CMD1(0x64, 0x0B);
CMD1(0x74, 0x0A);
CMD1(0x69, 0x08);
CMD1(0x79, 0x0A);
CMD1(0x65, 0x06);
CMD1(0x75, 0x07);
CMD1(0x67, 0x23);
CMD1(0x77, 0x44);
CMD1(0xE0, 0x00);
CMD1(0xE9, 0x30);
CMD1(0xEB, 0xB7);
CMD1(0xEC, 0x00);
CMD1(0xED, 0x11);
CMD1(0xF0, 0xB7);
CMD1(0x53, 0x04);
CMD1(0x54, 0x04);
CMD1(0x55, 0x40);
CMD1(0x56, 0x40);
CMD2(0xA0, 0x60, 0x01);
CMD1(0xA1, 0x84);
CMD1(0xA2, 0x85);
CMD2(0xAB, 0x00, 0x02);
CMD2(0xAC, 0x00, 0x06);
CMD2(0xAD, 0x00, 0x03);
CMD2(0xAE, 0x00, 0x07);
CMD1(0xC7, 0x01);
CMD1(0xB9, 0x82);
CMD1(0xBA, 0x83);
CMD1(0xBB, 0x00);
CMD1(0xBC, 0x81);
CMD1(0xBD, 0x02);
CMD1(0xBE, 0x01);
CMD1(0xBF, 0x04);
CMD1(0xC0, 0x03);
CMD1(0xC8, 0x55);
CMD1(0xC9, 0xC9);
CMD1(0xCA, 0xC8);
CMD1(0xCB, 0xCB);
CMD1(0xCC, 0xCA);
CMD1(0xCD, 0x55);
CMD1(0xCE, 0xCE);
CMD1(0xCF, 0xCD);
CMD1(0xD0, 0xD0);
CMD1(0xD1, 0xCF);
CMD1(0xF2, 0x46);
CMD1(0xA8, 0x04);
CMD1(0xA9, 0xB0);
CMD1(0xAA, 0xA3);
CMD1(0xB6, 0x00);
CMD1(0xB7, 0xB0);
CMD1(0xB8, 0xA3);
CMD1(0xC4, 0x03);
CMD1(0xC5, 0xB0);
CMD1(0xC6, 0xA3);
CMD1(0x80, 0x10);
CMD1(0xFF, 0x00);
CMD1(0x35, 0x00);
CMD0(NV3001B_SLPOUT);
delay(120);
CMD1(NV3001B_COLMOD, 0x05);
CMD1(NV3001B_MADCTL, nv3001bMADCTL(DISPLAY_ROTATION));
CMD0(NV3001B_DISPON);
delay(10);
#undef CMD0
#undef CMD1
#undef CMD2
}
void NV3001BDisplay::fillPhysicalRect(int x, int y, int w, int h) {
if (!is_on || w <= 0 || h <= 0) return;
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (x + w > NV3001B_SCREEN_WIDTH) w = NV3001B_SCREEN_WIDTH - x;
if (y + h > NV3001B_SCREEN_HEIGHT) h = NV3001B_SCREEN_HEIGHT - y;
if (w <= 0 || h <= 0) return;
setAddrWindow(x, y, w, h);
writeColor(color, (uint32_t)w * h);
}
void NV3001BDisplay::drawChar(int x, int y, char ch) {
if (ch < 32 || ch > 127) ch = '?';
uint16_t index = (uint16_t)(ch - 32) * 5;
int scale_x = textPixelScaleX(text_size);
int scale_y = textPixelScaleY(text_size);
for (int col = 0; col < 5; col++) {
uint8_t line = pgm_read_byte(font5x7 + index + col);
for (int row = 0; row < 7; row++) {
if (line & (1 << row)) {
fillPhysicalRect(x + col * scale_x, y + row * scale_y, scale_x, scale_y);
}
}
}
}
bool NV3001BDisplay::begin() {
if (is_on) return true;
if (periph_power) periph_power->claim();
setupOptionalOutput(PIN_TFT_EN, PIN_TFT_EN_ACTIVE);
setupOptionalOutput(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE);
pinMode(PIN_TFT_CS, OUTPUT);
pinMode(PIN_TFT_DC, OUTPUT);
digitalWrite(PIN_TFT_CS, HIGH);
digitalWrite(PIN_TFT_DC, HIGH);
delay(20);
spi.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS);
if (PIN_TFT_RST >= 0) {
pinMode(PIN_TFT_RST, OUTPUT);
digitalWrite(PIN_TFT_RST, HIGH);
delay(10);
digitalWrite(PIN_TFT_RST, LOW);
delay(20);
digitalWrite(PIN_TFT_RST, HIGH);
delay(120);
}
initPanel();
is_on = true;
color = 0x0000;
fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT);
color = 0xffff;
text_size = 1;
cursor_x = 0;
cursor_y = 0;
writeOptionalPin(PIN_TFT_BL, PIN_TFT_BL_ACTIVE);
return true;
}
void NV3001BDisplay::turnOn() {
begin();
}
void NV3001BDisplay::turnOff() {
if (!is_on) return;
writeOptionalPin(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE);
writeOptionalPin(PIN_TFT_EN, !PIN_TFT_EN_ACTIVE);
is_on = false;
if (periph_power) periph_power->release();
}
void NV3001BDisplay::clear() {
uint16_t saved = color;
color = 0x0000;
fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT);
color = saved;
}
void NV3001BDisplay::startFrame(Color bkg) {
color = mapColor(bkg);
fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT);
color = 0xffff;
text_size = 1;
cursor_x = 0;
cursor_y = 0;
}
void NV3001BDisplay::setTextSize(int sz) {
text_size = sz < 1 ? 1 : sz;
}
void NV3001BDisplay::setColor(Color c) {
color = mapColor(c);
}
void NV3001BDisplay::setCursor(int x, int y) {
cursor_x = scaleX(x);
cursor_y = scaleY(y);
}
void NV3001BDisplay::print(const char* str) {
if (!str || !is_on) return;
int scale_x = textPixelScaleX(text_size);
int scale_y = textPixelScaleY(text_size);
while (*str) {
if (*str == '\n') {
cursor_x = 0;
cursor_y += 8 * scale_y;
} else if (*str == '\r') {
cursor_x = 0;
} else {
drawChar(cursor_x, cursor_y, *str);
cursor_x += 6 * scale_x;
}
str++;
}
}
void NV3001BDisplay::fillRect(int x, int y, int w, int h) {
fillPhysicalRect(scaleX(x), scaleY(y), scaleWidth(x, w), scaleHeight(y, h));
}
void NV3001BDisplay::drawRect(int x, int y, int w, int h) {
int x1 = scaleX(x);
int y1 = scaleY(y);
int sw = scaleWidth(x, w);
int sh = scaleHeight(y, h);
fillPhysicalRect(x1, y1, sw, 1);
fillPhysicalRect(x1, y1 + sh - 1, sw, 1);
fillPhysicalRect(x1, y1, 1, sh);
fillPhysicalRect(x1 + sw - 1, y1, 1, sh);
}
void NV3001BDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
if (!bits || !is_on) return;
int byte_width = (w + 7) / 8;
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
uint8_t byte = pgm_read_byte(bits + j * byte_width + i / 8);
if (byte & (0x80 >> (i & 7))) {
fillPhysicalRect(scaleX(x + i), scaleY(y + j), scaleWidth(x + i, 1), scaleHeight(y + j, 1));
}
}
}
}
uint16_t NV3001BDisplay::getTextWidth(const char* str) {
if (!str) return 0;
uint16_t len = 0;
while (str[len] && str[len] != '\n' && str[len] != '\r') len++;
return (uint16_t)((len * 6 * textPixelScaleX(text_size)) / DISPLAY_SCALE_X);
}
void NV3001BDisplay::endFrame() {
}

68
src/helpers/ui/NV3001BDisplay.h

@ -0,0 +1,68 @@
#pragma once
#include "DisplayDriver.h"
#include <SPI.h>
#include <helpers/RefCountedDigitalPin.h>
#ifndef NV3001B_LOGICAL_WIDTH
#define NV3001B_LOGICAL_WIDTH 128
#endif
#ifndef NV3001B_LOGICAL_HEIGHT
#define NV3001B_LOGICAL_HEIGHT 64
#endif
#ifndef NV3001B_PANEL_WIDTH
#define NV3001B_PANEL_WIDTH 128
#endif
#ifndef NV3001B_PANEL_HEIGHT
#define NV3001B_PANEL_HEIGHT 220
#endif
#ifndef NV3001B_SPI_HOST
#define NV3001B_SPI_HOST HSPI
#endif
class NV3001BDisplay : public DisplayDriver {
SPIClass spi;
RefCountedDigitalPin* periph_power;
bool is_on = false;
uint16_t color = 0xffff;
uint8_t text_size = 1;
int cursor_x = 0;
int cursor_y = 0;
void writeCommand(uint8_t cmd);
void writeBytes(const uint8_t* data, size_t len);
void writeCommandData(uint8_t cmd, const uint8_t* data, size_t len);
void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
void writeColor(uint16_t rgb, uint32_t count);
void fillPhysicalRect(int x, int y, int w, int h);
void initPanel();
void drawChar(int x, int y, char ch);
public:
NV3001BDisplay(RefCountedDigitalPin* power = nullptr) :
DisplayDriver(NV3001B_LOGICAL_WIDTH, NV3001B_LOGICAL_HEIGHT), spi(NV3001B_SPI_HOST), periph_power(power) { }
bool begin();
static const char* driverName() { return "NV3001B"; }
static uint16_t physicalWidth() { return NV3001B_PANEL_WIDTH; }
static uint16_t physicalHeight() { return NV3001B_PANEL_HEIGHT; }
bool isOn() override { return is_on; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
};

18
src/helpers/ui/RotaryInput.h

@ -0,0 +1,18 @@
#pragma once
#include <Arduino.h>
enum class RotaryInputEvent : uint8_t {
None,
Next,
Prev,
};
class RotaryInput {
public:
virtual ~RotaryInput() = default;
virtual bool begin() = 0;
virtual RotaryInputEvent poll() = 0;
virtual bool isReady() const = 0;
};

11
src/helpers/ui/ST7735Display.cpp

@ -255,6 +255,10 @@ static const uint8_t PROGMEM
0x00, 0x00, // XSTART = 0
0x00, 0x9F }, // XEND = 159
Rcmd2invert[] = { // Tracker V1, part 2
1, // 1 command in list:
ST77XX_INVON, 0 }, // 1: Display is inverted
Rcmd3[] = { // 7735R init, part 3 (red or green tab)
2, // 2 commands in list:
ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay:
@ -447,8 +451,13 @@ bool ST7735Display::begin() {
_height = 80;
_width = 160;
#if defined(HELTEC_LORA_V3) // Tracker v1
_colstart = 26;
_rowstart = 1;
#else
_colstart = 24;
_rowstart = 0;
#endif
_resetAndInit();
@ -474,6 +483,8 @@ void ST7735Display::_resetAndInit() {
displayInit(Rcmd2green160x80);
//uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR
//display.sendCommand(ST77XX_MADCTL, &madctl, 1);
#elif defined(HELTEC_LORA_V3) // Tracker v1
displayInit(Rcmd2invert); // invert RGB
#endif
displayInit(Rcmd3);
setRotation(DISPLAY_ROTATION);

8
src/helpers/ui/ST7789LCDDisplay.cpp

@ -1,5 +1,9 @@
#include "ST7789LCDDisplay.h"
#ifndef PIN_TFT_MISO
#define PIN_TFT_MISO -1
#endif
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 3
#endif
@ -29,8 +33,8 @@ bool ST7789LCDDisplay::begin() {
}
// 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)
displaySPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS);
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
displaySPI.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS);
#endif
display.init(DISPLAY_WIDTH, DISPLAY_HEIGHT);

4
src/helpers/ui/ST7789LCDDisplay.h

@ -8,7 +8,7 @@
#include <helpers/RefCountedDigitalPin.h>
class ST7789LCDDisplay : public DisplayDriver {
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
SPIClass displaySPI;
#endif
Adafruit_ST7789 display;
@ -25,7 +25,7 @@ public:
{
_isOn = false;
}
#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT)
#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
displaySPI(HSPI),
display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),

68
variants/heltec_rc32/HeltecRC32Board.cpp

@ -0,0 +1,68 @@
#include "HeltecRC32Board.h"
void HeltecRC32Board::begin() {
ESP32Board::begin();
pinMode(PIN_ADC_CTRL, OUTPUT);
digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED);
#ifdef SENSOR_RST_PIN
pinMode(SENSOR_RST_PIN, OUTPUT);
digitalWrite(SENSOR_RST_PIN, HIGH);
#endif
#ifdef LED_POWER
pinMode(LED_POWER, OUTPUT);
digitalWrite(LED_POWER, LOW);
#endif
periph_power.begin();
periph_power.claim();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1L << P_LORA_DIO_1)) {
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void HeltecRC32Board::powerOff() {
enterDeepSleep(0);
}
void HeltecRC32Board::onBeforeTransmit() {
digitalWrite(P_LORA_TX_LED, HIGH);
}
void HeltecRC32Board::onAfterTransmit() {
digitalWrite(P_LORA_TX_LED, LOW);
}
uint16_t HeltecRC32Board::getBattMilliVolts() {
analogReadResolution(12);
analogSetAttenuation(ADC_2_5db);
digitalWrite(PIN_ADC_CTRL, ADC_CTRL_ENABLED);
delay(10);
uint32_t raw = 0;
for (int i = 0; i < 8; i++) {
raw += analogReadMilliVolts(PIN_VBAT_READ);
}
raw = raw / 8;
digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED);
return (adc_mult * raw);
}
bool HeltecRC32Board::setAdcMultiplier(float multiplier) {
adc_mult = multiplier == 0.0f ? ADC_MULTIPLIER : multiplier;
return true;
}
const char* HeltecRC32Board::getManufacturerName() const {
return "Heltec RC32";
}

29
variants/heltec_rc32/HeltecRC32Board.h

@ -0,0 +1,29 @@
#pragma once
#include <Arduino.h>
#include <driver/rtc_io.h>
#include <helpers/ESP32Board.h>
#include <helpers/RefCountedDigitalPin.h>
#ifndef ADC_MULTIPLIER
#define ADC_MULTIPLIER 4.9f
#endif
class HeltecRC32Board : public ESP32Board {
protected:
float adc_mult = ADC_MULTIPLIER;
public:
RefCountedDigitalPin periph_power;
HeltecRC32Board() : periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON){}
void begin();
void onBeforeTransmit() override;
void onAfterTransmit() override;
void powerOff() override;
uint16_t getBattMilliVolts() override;
bool setAdcMultiplier(float multiplier) override;
float getAdcMultiplier() const override { return adc_mult; }
const char* getManufacturerName() const override;
};

122
variants/heltec_rc32/HeltecRC32RotaryInput.cpp

@ -0,0 +1,122 @@
#include "HeltecRC32RotaryInput.h"
#include <Wire.h>
namespace {
constexpr uint8_t TCA6408_ADDR = 0x20;
constexpr uint8_t TCA6408_INPUT_REG = 0x00;
constexpr uint8_t TCA6408_POLARITY_REG = 0x02;
constexpr uint8_t TCA6408_CONFIG_REG = 0x03;
constexpr uint8_t TCA6408_ROTARY_A_MASK = 0x01;
constexpr uint8_t TCA6408_ROTARY_B_MASK = 0x02;
constexpr uint8_t TCA6408_ROTARY_MASK = TCA6408_ROTARY_A_MASK | TCA6408_ROTARY_B_MASK;
constexpr uint32_t TCA6408_DEBOUNCE_MS = 5;
}
bool HeltecRC32RotaryInput::begin() {
initialized = true;
ready = false;
input_state = TCA6408_ROTARY_MASK;
active_low_phase = false;
if (periph_power && !power_claimed) {
periph_power->claim();
power_claimed = true;
delay(12);
}
if (!writeRegister(TCA6408_POLARITY_REG, 0x00) || !writeRegister(TCA6408_CONFIG_REG, 0xFF)) {
return false;
}
uint8_t state = 0;
if (!readInput(state)) {
return false;
}
input_state = state & TCA6408_ROTARY_MASK;
ready = true;
return true;
}
RotaryInputEvent HeltecRC32RotaryInput::poll() {
if (!initialized) {
begin();
return RotaryInputEvent::None;
}
if (!ready) {
return RotaryInputEvent::None;
}
uint8_t new_state = 0;
if (!readInput(new_state)) {
return RotaryInputEvent::None;
}
new_state &= TCA6408_ROTARY_MASK;
RotaryInputEvent event = handleTransition(new_state);
input_state = new_state;
return event;
}
bool HeltecRC32RotaryInput::writeRegister(uint8_t reg, uint8_t value) {
Wire.beginTransmission(TCA6408_ADDR);
Wire.write(reg);
Wire.write(value);
return Wire.endTransmission() == 0;
}
bool HeltecRC32RotaryInput::readInput(uint8_t& value) {
Wire.beginTransmission(TCA6408_ADDR);
Wire.write(TCA6408_INPUT_REG);
if (Wire.endTransmission(false) != 0) {
return false;
}
if (Wire.requestFrom(TCA6408_ADDR, static_cast<uint8_t>(1)) != 1) {
return false;
}
value = Wire.read();
return true;
}
RotaryInputEvent HeltecRC32RotaryInput::handleTransition(uint8_t newState) {
uint8_t changed = (input_state ^ newState) & TCA6408_ROTARY_MASK;
RotaryInputEvent event = RotaryInputEvent::None;
bool a_low = (newState & TCA6408_ROTARY_A_MASK) == 0;
bool b_low = (newState & TCA6408_ROTARY_B_MASK) == 0;
if (!a_low && !b_low) {
active_low_phase = false;
}
if (!active_low_phase && (changed & TCA6408_ROTARY_A_MASK) && a_low && !b_low) {
event = RotaryInputEvent::Prev;
active_low_phase = true;
} else if (!active_low_phase && (changed & TCA6408_ROTARY_B_MASK) && b_low && !a_low) {
event = RotaryInputEvent::Next;
active_low_phase = true;
}
if (event == RotaryInputEvent::None && !active_low_phase && (changed & TCA6408_ROTARY_A_MASK)) {
bool a_rising = (newState & TCA6408_ROTARY_A_MASK) != 0;
if (a_rising && b_low) {
event = RotaryInputEvent::Prev;
}
}
if (event == RotaryInputEvent::None && !active_low_phase && (changed & TCA6408_ROTARY_B_MASK)) {
bool b_rising = (newState & TCA6408_ROTARY_B_MASK) != 0;
if (b_rising && a_low) {
event = RotaryInputEvent::Next;
}
}
if (event == RotaryInputEvent::None || (millis() - last_event_ms) < TCA6408_DEBOUNCE_MS) {
return RotaryInputEvent::None;
}
last_event_ms = millis();
return event;
}

26
variants/heltec_rc32/HeltecRC32RotaryInput.h

@ -0,0 +1,26 @@
#pragma once
#include <helpers/RefCountedDigitalPin.h>
#include <helpers/ui/RotaryInput.h>
class HeltecRC32RotaryInput : public RotaryInput {
public:
explicit HeltecRC32RotaryInput(RefCountedDigitalPin* periphPower = nullptr) : periph_power(periphPower) { }
bool begin() override;
RotaryInputEvent poll() override;
bool isReady() const override { return ready; }
private:
bool writeRegister(uint8_t reg, uint8_t value);
bool readInput(uint8_t& value);
RotaryInputEvent handleTransition(uint8_t newState);
uint8_t input_state = 0x03;
uint32_t last_event_ms = 0;
bool ready = false;
bool initialized = false;
bool power_claimed = false;
bool active_low_phase = false;
RefCountedDigitalPin* periph_power = nullptr;
};

60
variants/heltec_rc32/pins_arduino.h

@ -0,0 +1,60 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
static const uint8_t TX = 43;
static const uint8_t RX = 44;
static const uint8_t SDA = 21;
static const uint8_t SCL = 18;
static const uint8_t SS = 10;
static const uint8_t MOSI = 12;
static const uint8_t MISO = 13;
static const uint8_t SCK = 11;
static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
static const uint8_t A6 = 7;
static const uint8_t A7 = 8;
static const uint8_t A8 = 9;
static const uint8_t A9 = 10;
static const uint8_t A10 = 11;
static const uint8_t A11 = 12;
static const uint8_t A12 = 13;
static const uint8_t A13 = 14;
static const uint8_t A14 = 15;
static const uint8_t A15 = 16;
static const uint8_t A16 = 17;
static const uint8_t A17 = 18;
static const uint8_t A18 = 19;
static const uint8_t A19 = 20;
static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
static const uint8_t T7 = 7;
static const uint8_t T8 = 8;
static const uint8_t T9 = 9;
static const uint8_t T10 = 10;
static const uint8_t T11 = 11;
static const uint8_t T12 = 12;
static const uint8_t T13 = 13;
static const uint8_t T14 = 14;
static const uint8_t RST_LoRa = 9;
static const uint8_t BUSY_LoRa = 1;
static const uint8_t DIO1_LoRa = 14;
#endif

348
variants/heltec_rc32/platformio.ini

@ -0,0 +1,348 @@
[Heltec_RC32]
extends = esp32_base
board = heltec-rc32
board_build.partitions = default_16MB.csv
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
-I variants/heltec_rc32
-I src/helpers/ui
-D HELTEC_RC32
-D USE_SX1262
-D ESP32_CPU_FREQ=160
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D P_LORA_DIO_1=14
-D P_LORA_NSS=10
-D P_LORA_RESET=9
-D P_LORA_BUSY=1
-D P_LORA_SCLK=11
-D P_LORA_MISO=13
-D P_LORA_MOSI=12
-D LORA_TX_POWER=22
-D PIN_USER_BTN=0
-D PIN_BOARD_SDA=21
-D PIN_BOARD_SCL=18
-D SENSOR_POWER_CTRL_PIN=46
-D SENSOR_POWER_ON=HIGH
-D SENSOR_RST_PIN=2
-D P_LORA_TX_LED=47
-D PIN_BUZZER=48
-D PIN_TFT_SCL=17
-D PIN_TFT_SDA=38
-D PIN_TFT_CS=39
-D PIN_TFT_DC=16
-D PIN_TFT_RST=4
-D PIN_TFT_EN=6
-D PIN_TFT_EN_ACTIVE=LOW
-D PIN_TFT_BL=5
-D PIN_TFT_BL_ACTIVE=HIGH
-D SPI_FREQUENCY=8000000
-D PIN_GPS_TX=44
-D PIN_GPS_RX=43
-D PIN_GPS_EN=45
-D PIN_GPS_EN_ACTIVE=HIGH
-D PIN_GPS_RESET=40
-D PIN_GPS_RESET_ACTIVE=LOW
-D PIN_GPS_PPS=41
-D GPS_BAUD_RATE=9600
-D ENV_INCLUDE_GPS=1
-D PIN_ADC_CTRL=15
-D PIN_VBAT_READ=7
-D ADC_CTRL_ENABLED=HIGH
-D ADC_MULTIPLIER=4.9
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/heltec_rc32>
+<helpers/sensors>
lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
[Heltec_RC32_with_display]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-D HELTEC_RC32_WITH_DISPLAY
-D DISPLAY_CLASS=NV3001BDisplay
build_src_filter = ${Heltec_RC32.build_src_filter}
+<helpers/ui/NV3001BDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
lib_deps =
${Heltec_RC32.lib_deps}
[env:heltec_rc32_without_display_repeater]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-D ADVERT_NAME='"Heltec RC32 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
build_src_filter = ${Heltec_RC32.build_src_filter}
+<../examples/simple_repeater>
lib_deps =
${Heltec_RC32.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:heltec_rc32_without_display_repeater_bridge_espnow]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_ESPNOW_BRIDGE=1
build_src_filter = ${Heltec_RC32.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<../examples/simple_repeater>
lib_deps =
${Heltec_RC32.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_rc32_without_display_room_server]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-D ADVERT_NAME='"Heltec RC32 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
build_src_filter = ${Heltec_RC32.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${Heltec_RC32.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_rc32_without_display_companion_radio_usb]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=NullDisplayDriver
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
build_src_filter = ${Heltec_RC32.build_src_filter}
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_RC32.lib_deps}
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_rc32_without_display_companion_radio_ble]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=NullDisplayDriver
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${Heltec_RC32.build_src_filter}
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_RC32.lib_deps}
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_rc32_without_display_companion_radio_wifi]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=NullDisplayDriver
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${Heltec_RC32.build_src_filter}
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_RC32.lib_deps}
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_rc32_without_display_sensor]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-D ADVERT_NAME='"Heltec RC32 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ENV_PIN_SDA=21
-D ENV_PIN_SCL=18
build_src_filter = ${Heltec_RC32.build_src_filter}
+<../examples/simple_sensor>
lib_deps =
${Heltec_RC32.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_rc32_repeater]
extends = Heltec_RC32_with_display
build_flags =
${Heltec_RC32_with_display.build_flags}
-D ADVERT_NAME='"Heltec RC32 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
+<../examples/simple_repeater>
lib_deps =
${Heltec_RC32_with_display.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:heltec_rc32_repeater_bridge_espnow]
extends = Heltec_RC32_with_display
build_flags =
${Heltec_RC32_with_display.build_flags}
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_ESPNOW_BRIDGE=1
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<../examples/simple_repeater>
lib_deps =
${Heltec_RC32_with_display.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_rc32_room_server]
extends = Heltec_RC32_with_display
build_flags =
${Heltec_RC32_with_display.build_flags}
-D ADVERT_NAME='"Heltec RC32 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${Heltec_RC32_with_display.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_rc32_terminal_chat]
extends = Heltec_RC32
build_flags =
${Heltec_RC32.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=1
build_src_filter = ${Heltec_RC32.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${Heltec_RC32.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_rc32_companion_radio_usb]
extends = Heltec_RC32_with_display
build_flags =
${Heltec_RC32_with_display.build_flags}
-I examples/companion_radio/ui-new
-D UI_HAS_ROTARY_INPUT
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
+<helpers/ui/buzzer.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_RC32_with_display.lib_deps}
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_rc32_companion_radio_ble]
extends = Heltec_RC32_with_display
build_flags =
${Heltec_RC32_with_display.build_flags}
-I examples/companion_radio/ui-new
-D UI_HAS_ROTARY_INPUT
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
+<helpers/ui/buzzer.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_RC32_with_display.lib_deps}
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_rc32_companion_radio_wifi]
extends = Heltec_RC32_with_display
build_flags =
${Heltec_RC32_with_display.build_flags}
-I examples/companion_radio/ui-new
-D UI_HAS_ROTARY_INPUT
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
+<helpers/ui/buzzer.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${Heltec_RC32_with_display.lib_deps}
densaugeo/base64 @ ~1.4.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_rc32_sensor]
extends = Heltec_RC32_with_display
build_flags =
${Heltec_RC32_with_display.build_flags}
-D ADVERT_NAME='"Heltec RC32 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ENV_PIN_SDA=21
-D ENV_PIN_SCL=18
build_src_filter = ${Heltec_RC32_with_display.build_src_filter}
+<../examples/simple_sensor>
lib_deps =
${Heltec_RC32_with_display.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_rc32_kiss_modem]
extends = Heltec_RC32
build_src_filter = ${Heltec_RC32.build_src_filter}
+<../examples/kiss_modem/>

52
variants/heltec_rc32/target.cpp

@ -0,0 +1,52 @@
#include <Arduino.h>
#include "target.h"
#if defined(UI_HAS_ROTARY_INPUT)
#include "HeltecRC32RotaryInput.h"
#endif
HeltecRC32Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi(FSPI);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#if ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#if defined(UI_HAS_ROTARY_INPUT)
static HeltecRC32RotaryInput rotaryInputImpl(&board.periph_power);
RotaryInput& rotary_input = rotaryInputImpl;
#endif
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}

37
variants/heltec_rc32/target.h

@ -0,0 +1,37 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <HeltecRC32Board.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/MomentaryButton.h>
#if defined(UI_HAS_ROTARY_INPUT)
#include <helpers/ui/RotaryInput.h>
#endif
#ifdef HELTEC_RC32_WITH_DISPLAY
#include <helpers/ui/NV3001BDisplay.h>
#else
#include <helpers/ui/NullDisplayDriver.h>
#endif
#endif
extern HeltecRC32Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#if defined(UI_HAS_ROTARY_INPUT)
extern RotaryInput& rotary_input;
#endif
#endif
bool radio_init();
mesh::LocalIdentity radio_new_identity();

29
variants/heltec_rc32/variant.h

@ -0,0 +1,29 @@
#ifndef _VARIANT_HELTEC_RC32_
#define _VARIANT_HELTEC_RC32_
#define BUTTON_PIN 0
#define I2C_SCL 18
#define I2C_SDA 21
#define SENSOR_INT_PIN 42
#define PERIPHERAL_WARMUP_MS 100
#define LORA_SCK 11
#define LORA_MISO 13
#define LORA_MOSI 12
#define LORA_CS 10
#define LORA_DIO0 RADIOLIB_NC
#define LORA_DIO1 14
#define LORA_RESET 9
#define SX126X_CS LORA_CS
#define SX126X_DIO1 LORA_DIO1
#define SX126X_BUSY 1
#define SX126X_RESET LORA_RESET
#define BATTERY_PIN 7
#define ADC_CHANNEL ADC_CHANNEL_6
#define ADC_CTRL 15
#define ADC_ATTENUATION ADC_ATTEN_DB_2_5
#endif

2
variants/heltec_t1/platformio.ini

@ -23,7 +23,7 @@ build_src_filter = ${nrf52_base.build_src_filter}
lib_deps =
${nrf52_base.lib_deps}
${sensor_base.lib_deps}
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
bodmer/TFT_eSPI @ ^2.4.31
debug_tool = jlink
upload_protocol = nrfutil

86
variants/heltec_v4_r8/HeltecV4R8Board.cpp

@ -0,0 +1,86 @@
#include "HeltecV4R8Board.h"
void HeltecV4R8Board::begin() {
ESP32Board::begin();
periph_power.begin();
periph_power.claim(); // R8 VEXT also feeds the LoRa antenna boost rail.
loRaFEMControl.init();
#ifdef PIN_TOUCH_RST
pinMode(PIN_TOUCH_RST, OUTPUT);
digitalWrite(PIN_TOUCH_RST, HIGH);
delay(10);
digitalWrite(PIN_TOUCH_RST, LOW);
delay(100);
digitalWrite(PIN_TOUCH_RST, HIGH);
#endif
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) {
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void HeltecV4R8Board::onBeforeTransmit(void) {
digitalWrite(P_LORA_TX_LED, HIGH);
loRaFEMControl.setTxModeEnable();
}
void HeltecV4R8Board::onAfterTransmit(void) {
digitalWrite(P_LORA_TX_LED, LOW);
loRaFEMControl.setRxModeEnable();
}
void HeltecV4R8Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
loRaFEMControl.setRxModeEnableWhenMCUSleep();
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);
} else {
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
esp_deep_sleep_start();
}
void HeltecV4R8Board::powerOff() {
enterDeepSleep(0);
}
uint16_t HeltecV4R8Board::getBattMilliVolts() {
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < 8; i++) {
raw += analogReadMilliVolts(PIN_VBAT_READ);
}
raw = raw / 8;
return (adc_mult * raw);
}
const char* HeltecV4R8Board::getManufacturerName() const {
#ifdef HELTEC_V4_R8_TFT
return "Heltec V4 R8 TFT";
#else
return "Heltec V4 R8 OLED";
#endif
}

39
variants/heltec_v4_r8/HeltecV4R8Board.h

@ -0,0 +1,39 @@
#pragma once
#include <Arduino.h>
#include <driver/rtc_io.h>
#include <helpers/ESP32Board.h>
#include <helpers/RefCountedDigitalPin.h>
#include "LoRaFEMControl.h"
#ifndef ADC_MULTIPLIER
#define ADC_MULTIPLIER (4.9f * 1.035f)
#endif
class HeltecV4R8Board : public ESP32Board {
protected:
float adc_mult = ADC_MULTIPLIER;
public:
RefCountedDigitalPin periph_power;
LoRaFEMControl loRaFEMControl;
HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { }
void begin();
void onBeforeTransmit(void) override;
void onAfterTransmit(void) override;
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
void powerOff() override;
uint16_t getBattMilliVolts() override;
bool setAdcMultiplier(float multiplier) override {
if (multiplier == 0.0f) {
adc_mult = ADC_MULTIPLIER;
} else {
adc_mult = multiplier;
}
return true;
}
float getAdcMultiplier() const override { return adc_mult; }
const char* getManufacturerName() const override;
};

52
variants/heltec_v4_r8/LoRaFEMControl.cpp

@ -0,0 +1,52 @@
#include "LoRaFEMControl.h"
#include <Arduino.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
void LoRaFEMControl::init(void) {
pinMode(P_LORA_PA_POWER, OUTPUT);
digitalWrite(P_LORA_PA_POWER, HIGH);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER);
esp_reset_reason_t reason = esp_reset_reason();
if (reason != ESP_RST_DEEPSLEEP) {
delay(1);
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_KCT8103L_PA_CSD);
rtc_gpio_hold_dis((gpio_num_t)P_LORA_KCT8103L_PA_CTX);
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, lna_enabled ? LOW : HIGH);
}
void LoRaFEMControl::setSleepModeEnable(void) {
digitalWrite(P_LORA_KCT8103L_PA_CSD, LOW);
}
void LoRaFEMControl::setTxModeEnable(void) {
digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH);
digitalWrite(P_LORA_KCT8103L_PA_CTX, HIGH);
}
void LoRaFEMControl::setRxModeEnable(void) {
digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH);
digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH);
}
void LoRaFEMControl::setRxModeEnableWhenMCUSleep(void) {
digitalWrite(P_LORA_PA_POWER, HIGH);
rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER);
digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH);
rtc_gpio_hold_en((gpio_num_t)P_LORA_KCT8103L_PA_CSD);
digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH);
rtc_gpio_hold_en((gpio_num_t)P_LORA_KCT8103L_PA_CTX);
}
void LoRaFEMControl::setLNAEnable(bool enabled) {
lna_enabled = enabled;
}

24
variants/heltec_v4_r8/LoRaFEMControl.h

@ -0,0 +1,24 @@
#pragma once
typedef enum {
KCT8103L_PA,
OTHER_FEM_TYPES
} LoRaFEMType;
class LoRaFEMControl {
public:
LoRaFEMControl() { }
virtual ~LoRaFEMControl() { }
void init(void);
void setSleepModeEnable(void);
void setTxModeEnable(void);
void setRxModeEnable(void);
void setRxModeEnableWhenMCUSleep(void);
void setLNAEnable(bool enabled);
bool isLnaCanControl(void) { return true; }
void setLnaCanControl(bool can_control) { }
LoRaFEMType getFEMType(void) const { return KCT8103L_PA; }
private:
bool lna_enabled = false;
};

56
variants/heltec_v4_r8/pins_arduino.h

@ -0,0 +1,56 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
static const uint8_t TX = 43;
static const uint8_t RX = 44;
static const uint8_t SDA = 17;
static const uint8_t SCL = 18;
static const uint8_t SS = 8;
static const uint8_t MOSI = 10;
static const uint8_t MISO = 11;
static const uint8_t SCK = 9;
static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
static const uint8_t A6 = 7;
static const uint8_t A7 = 8;
static const uint8_t A8 = 9;
static const uint8_t A9 = 10;
static const uint8_t A10 = 11;
static const uint8_t A11 = 12;
static const uint8_t A12 = 13;
static const uint8_t A13 = 14;
static const uint8_t A14 = 15;
static const uint8_t A15 = 16;
static const uint8_t A16 = 17;
static const uint8_t A17 = 18;
static const uint8_t A18 = 19;
static const uint8_t A19 = 20;
static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
static const uint8_t T7 = 7;
static const uint8_t T8 = 8;
static const uint8_t T9 = 9;
static const uint8_t T10 = 10;
static const uint8_t T11 = 11;
static const uint8_t T12 = 12;
static const uint8_t T13 = 13;
static const uint8_t T14 = 14;
#endif

342
variants/heltec_v4_r8/platformio.ini

@ -0,0 +1,342 @@
[Heltec_v4_r8]
extends = esp32_base
board = heltec_v4_r8
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
-I variants/heltec_v4_r8
-D HELTEC_V4_R8
-D USE_SX1262
-D ESP32_CPU_FREQ=80
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D P_LORA_DIO_1=14
-D P_LORA_NSS=8
-D P_LORA_RESET=12
-D P_LORA_BUSY=13
-D P_LORA_SCLK=9
-D P_LORA_MISO=11
-D P_LORA_MOSI=10
-D P_LORA_PA_POWER=7
-D P_LORA_KCT8103L_PA_CSD=2
-D P_LORA_KCT8103L_PA_CTX=5
-D P_LORA_TX_LED=46
-D PIN_USER_BTN=0
-D PIN_VEXT_EN=40
-D PIN_VEXT_EN_ACTIVE=LOW
-D ADC_MULTIPLIER=5.0715f
-D PIN_VBAT_READ=1
-D LORA_TX_POWER=10
-D MAX_LORA_TX_POWER=22
-D SX126X_REGISTER_PATCH=1
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D PIN_GPS_RX=38
-D PIN_GPS_TX=39
-D PIN_GPS_EN=42
-D PIN_GPS_EN_ACTIVE=LOW
-D ENV_INCLUDE_GPS=1
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/heltec_v4_r8>
+<helpers/sensors>
lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
[heltec_v4_r8_oled]
extends = Heltec_v4_r8
build_flags =
${Heltec_v4_r8.build_flags}
-D HELTEC_V4_R8_OLED
-D PIN_BOARD_SDA=17
-D PIN_BOARD_SCL=18
-D PIN_OLED_RESET=21
build_src_filter = ${Heltec_v4_r8.build_src_filter}
lib_deps = ${Heltec_v4_r8.lib_deps}
[heltec_v4_r8_tft]
extends = Heltec_v4_r8
build_flags =
${Heltec_v4_r8.build_flags}
-D HELTEC_V4_R8_TFT
-D PIN_BOARD_SDA=17
-D PIN_BOARD_SCL=18
-D DISPLAY_SCALE_X=2.5
-D DISPLAY_SCALE_Y=3.75
-D PIN_TFT_RST=-1
-D PIN_TFT_VDD_CTL=-1
-D PIN_TFT_LEDA_CTL=44
-D PIN_TFT_LEDA_CTL_ACTIVE=HIGH
-D PIN_TFT_CS=47
-D PIN_TFT_DC=48
-D PIN_TFT_SCL=16
-D PIN_TFT_SDA=15
-D PIN_TFT_MISO=45
-D PIN_BUZZER=4
-D PIN_TOUCH_RST=21
build_src_filter = ${Heltec_v4_r8.build_src_filter}
+<helpers/ui/buzzer.cpp>
lib_deps =
${Heltec_v4_r8.lib_deps}
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
end2endzone/NonBlockingRTTTL@^1.3.0
[env:heltec_v4_r8_repeater]
extends = heltec_v4_r8_oled
build_flags =
${heltec_v4_r8_oled.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"Heltec R8 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
build_src_filter = ${heltec_v4_r8_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${heltec_v4_r8_oled.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:heltec_v4_r8_room_server]
extends = heltec_v4_r8_oled
build_flags =
${heltec_v4_r8_oled.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"Heltec R8 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
build_src_filter = ${heltec_v4_r8_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_room_server>
lib_deps =
${heltec_v4_r8_oled.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_r8_terminal_chat]
extends = heltec_v4_r8_oled
build_flags =
${heltec_v4_r8_oled.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=1
build_src_filter = ${heltec_v4_r8_oled.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${heltec_v4_r8_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_companion_radio_usb]
extends = heltec_v4_r8_oled
build_flags =
${heltec_v4_r8_oled.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
build_src_filter = ${heltec_v4_r8_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_r8_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_companion_radio_ble]
extends = heltec_v4_r8_oled
build_flags =
${heltec_v4_r8_oled.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=SSD1306Display
-D BLE_PIN_CODE=123456
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${heltec_v4_r8_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_r8_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_companion_radio_wifi]
extends = heltec_v4_r8_oled
build_flags =
${heltec_v4_r8_oled.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=SSD1306Display
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
build_src_filter = ${heltec_v4_r8_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_r8_oled.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_sensor]
extends = heltec_v4_r8_oled
build_flags =
${heltec_v4_r8_oled.build_flags}
-D ADVERT_NAME='"Heltec R8 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D DISPLAY_CLASS=SSD1306Display
build_src_filter = ${heltec_v4_r8_oled.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_sensor>
lib_deps =
${heltec_v4_r8_oled.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_r8_tft_repeater]
extends = heltec_v4_r8_tft
build_flags =
${heltec_v4_r8_tft.build_flags}
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADVERT_NAME='"Heltec R8 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<../examples/simple_repeater>
lib_deps =
${heltec_v4_r8_tft.lib_deps}
${esp32_ota.lib_deps}
bakercp/CRC32 @ ^2.0.0
[env:heltec_v4_r8_tft_room_server]
extends = heltec_v4_r8_tft
build_flags =
${heltec_v4_r8_tft.build_flags}
-D DISPLAY_CLASS=ST7789LCDDisplay
-D ADVERT_NAME='"Heltec R8 Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<../examples/simple_room_server>
lib_deps =
${heltec_v4_r8_tft.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_r8_tft_terminal_chat]
extends = heltec_v4_r8_tft
build_flags =
${heltec_v4_r8_tft.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=1
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${heltec_v4_r8_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_tft_companion_radio_usb]
extends = heltec_v4_r8_tft
build_flags =
${heltec_v4_r8_tft.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D DISPLAY_CLASS=ST7789LCDDisplay
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_r8_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_tft_companion_radio_ble]
extends = heltec_v4_r8_tft
build_flags =
${heltec_v4_r8_tft.build_flags}
-I examples/companion_radio/ui-new
-D DISPLAY_CLASS=ST7789LCDDisplay
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_r8_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_tft_companion_radio_wifi]
extends = heltec_v4_r8_tft
build_flags =
${heltec_v4_r8_tft.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=ST7789LCDDisplay
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/esp32/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${heltec_v4_r8_tft.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:heltec_v4_r8_tft_sensor]
extends = heltec_v4_r8_tft
build_flags =
${heltec_v4_r8_tft.build_flags}
-D ADVERT_NAME='"Heltec R8 Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D DISPLAY_CLASS=ST7789LCDDisplay
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
+<helpers/ui/ST7789LCDDisplay.cpp>
+<../examples/simple_sensor>
lib_deps =
${heltec_v4_r8_tft.lib_deps}
${esp32_ota.lib_deps}
[env:heltec_v4_r8_kiss_modem]
extends = Heltec_v4_r8
build_src_filter = ${Heltec_v4_r8.build_src_filter}
+<../examples/kiss_modem/>
[env:heltec_v4_r8_tft_kiss_modem]
extends = heltec_v4_r8_tft
build_src_filter = ${heltec_v4_r8_tft.build_src_filter}
+<../examples/kiss_modem/>

45
variants/heltec_v4_r8/target.cpp

@ -0,0 +1,45 @@
#include <Arduino.h>
#include "target.h"
HeltecV4R8Board board;
#if defined(P_LORA_SCLK)
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#if ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, GPS_RESET, GPS_EN, &board.periph_power);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display(&board.periph_power);
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}

31
variants/heltec_v4_r8/target.h

@ -0,0 +1,31 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <HeltecV4R8Board.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#ifdef HELTEC_V4_R8_OLED
#include <helpers/ui/SSD1306Display.h>
#elif defined(HELTEC_V4_R8_TFT)
#include <helpers/ui/ST7789LCDDisplay.h>
#endif
#include <helpers/ui/MomentaryButton.h>
#endif
extern HeltecV4R8Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
mesh::LocalIdentity radio_new_identity();

4
variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h

@ -47,9 +47,9 @@ public:
return "LilyGo T-Impulse-Plus";
}
void powerOff() override {
void shutdownPeripherals() override {
// power off system
NRF52Board::powerOff();
NRF52Board::shutdownPeripherals();
// turn off 3.3v
digitalWrite(RT9080_EN, LOW);

4
variants/lilygo_techo/TechoBoard.h

@ -23,8 +23,8 @@ public:
return "LilyGo T-Echo";
}
void powerOff() override {
NRF52Board::powerOff();
void shutdownPeripherals() override {
NRF52Board::shutdownPeripherals();
#ifdef LED_RED
digitalWrite(LED_RED, HIGH);
#endif

4
variants/lilygo_techo_card/TechoCardBoard.cpp

@ -87,11 +87,11 @@ void TechoCardBoard::turnOffLeds() {
}
}
void TechoCardBoard::powerOff() {
void TechoCardBoard::shutdownPeripherals() {
nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
turnOffLeds();
digitalWrite(PIN_PWR_EN, LOW);
NRF52Board::powerOff();
NRF52Board::shutdownPeripherals();
}
#endif

2
variants/lilygo_techo_card/TechoCardBoard.h

@ -28,7 +28,7 @@ public:
return "LilyGo T-Echo Card";
}
void powerOff() override;
void shutdownPeripherals() override;
void toggleTorch();
void turnOffLeds();

4
variants/lilygo_techo_lite/TechoBoard.h

@ -21,8 +21,8 @@ public:
return "LilyGo T-Echo Lite";
}
void powerOff() override {
NRF52Board::powerOff();
void shutdownPeripherals() override {
NRF52Board::shutdownPeripherals();
digitalWrite(PIN_VBAT_MEAS_EN, LOW);
#ifdef LED_RED

14
variants/rak4631/RAK4631Board.cpp

@ -1,8 +1,21 @@
#include <Arduino.h>
#include <Wire.h>
#include "nrf_gpio.h"
#include "RAK4631Board.h"
#ifdef ETHERNET_ENABLED
// Drive WB_IO2 HIGH as early as possible using direct register access.
// WB_IO2 (P1.02, Arduino pin 34) controls the WisBlock slot power switch.
// With POE through RAK13800, this must be enabled before the framework
// initializes or the board will brownout from insufficient power delivery.
// Priority 102 runs just after SystemInit.
static void __attribute__((constructor(102))) rak4631_early_poe_power() {
nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 2)); // WB_IO2 = P1.02
nrf_gpio_pin_set(NRF_GPIO_PIN_MAP(1, 2));
}
#endif
#ifdef NRF52_POWER_MANAGEMENT
// Static configuration for power management
// Values set in variant.h defines
@ -36,6 +49,7 @@ void RAK4631Board::begin() {
pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP);
#endif
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif

69
variants/rak4631/platformio.ini

@ -54,6 +54,25 @@ build_src_filter = ${rak4631.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
[env:RAK_4631_repeater_ethernet]
extends = rak4631
build_flags =
${rak4631.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"RAK4631 Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D ETHERNET_ENABLED=1
; -D MESH_DEBUG=1
build_src_filter = ${rak4631.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
lib_deps =
${rak4631.lib_deps}
https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
[env:RAK_4631_repeater_bridge_rs232_serial1]
extends = rak4631
build_flags =
@ -116,6 +135,25 @@ build_src_filter = ${rak4631.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_room_server>
[env:RAK_4631_room_server_ethernet]
extends = rak4631
build_flags =
${rak4631.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D ADVERT_NAME='"RAK4631 Room Server"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
-D ETHERNET_ENABLED=1
; -D MESH_DEBUG=1
build_src_filter = ${rak4631.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_room_server>
lib_deps =
${rak4631.lib_deps}
https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
[env:RAK_4631_companion_radio_usb]
extends = rak4631
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
@ -137,6 +175,35 @@ lib_deps =
${rak4631.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:RAK_4631_companion_radio_ethernet]
extends = rak4631
board_build.ldscript = boards/nrf52840_s140_v6.ld
board_upload.maximum_size = 712704
build_unflags =
-D EXTRAFS=1
build_flags =
${rak4631.build_flags}
-I examples/companion_radio/ui-new
-D PIN_USER_BTN=9
-D PIN_USER_BTN_ANA=31
-D DISPLAY_CLASS=SSD1306Display
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D ETHERNET_ENABLED=1
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
; -D ETHERNET_DEBUG_LOGGING=1
build_src_filter = ${rak4631.build_src_filter}
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
+<helpers/nrf52/SerialEthernetInterface.cpp>
lib_deps =
${rak4631.lib_deps}
densaugeo/base64 @ ~1.4.0
https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip
[env:RAK_4631_companion_radio_ble]
extends = rak4631
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
@ -200,4 +267,4 @@ build_flags =
build_src_filter = ${rak4631.build_src_filter}
+<../examples/kiss_modem/>
lib_deps =
${rak4631.lib_deps}
${rak4631.lib_deps}

2
variants/thinknode_m7/platformio.ini

@ -44,7 +44,7 @@ build_flags =
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=8
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
lib_deps =

2
variants/xiao_nrf52/XiaoNrf52Board.h

@ -47,7 +47,7 @@ public:
#ifdef PIN_USER_BTN
// configure button press to wake up when in powered off state
nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW);
nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
#endif
NRF52Board::powerOff();

1
variants/xiao_nrf52/target.cpp

@ -4,6 +4,7 @@
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
#endif
XiaoNrf52Board board;

2
variants/xiao_nrf52/target.h

@ -11,7 +11,9 @@
#ifdef DISPLAY_CLASS
#include <helpers/ui/NullDisplayDriver.h>
#include <helpers/ui/MomentaryButton.h>
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
extern XiaoNrf52Board board;

Loading…
Cancel
Save