Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion .github/workflows/pr-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,52 @@ on:
- '.github/workflows/pr-build-check.yml'

jobs:
webui:
runs-on: ubuntu-latest
defaults:
run:
working-directory: examples/companion_radio/webui

steps:
- name: Clone Repo
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Build and verify embedded WebUI
run: |
npm ci
npm test
git diff --exit-code -- .

build:
needs: webui
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
environment:
- heltec_rcc6_companion_radio_ble
- heltec_rcc6_companion_radio_web_ap

steps:
- name: Clone Repo
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment

- name: Build RCC6 companion firmware
run: pio run -e heltec_rcc6_companion_radio_ble
run: pio run -e ${{ matrix.environment }}

- name: Upload exact firmware candidate
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.environment }}-${{ github.event.pull_request.head.sha || github.sha }}
path: |
.pio/build/${{ matrix.environment }}/firmware.bin
.pio/build/${{ matrix.environment }}/firmware.elf
.pio/build/${{ matrix.environment }}/bootloader.bin
.pio/build/${{ matrix.environment }}/partitions.bin
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# RadioCore²-RCC6

MeshCore Companion Bluetooth firmware for the **Heltec RadioCore RCC6** with its native **220×128 NV3001B TFT**.
MeshCore Companion firmware for the **Heltec RadioCore RCC6** with its native **220×128 NV3001B TFT**. Two firmware modes are provided: secure BLE companion mode, and Wi-Fi Web/AP mode with an offline WebUI plus raw TCP/5000 access for trusted private networks.

[![Release](https://img.shields.io/github/v/release/n30nex/RadioCore2-RCC6?include_prereleases&label=firmware)](https://github.com/n30nex/RadioCore2-RCC6/releases)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](license.txt)
Expand Down Expand Up @@ -33,21 +33,24 @@ MeshCore Companion Bluetooth firmware for the **Heltec RadioCore RCC6** with its
- BLE, battery, RF, unread, RX/TX, RSSI, and SNR status at a glance.
- Recent nodes, radio settings, Bluetooth, Advert, Power, and message views.
- Color-coded visual notifications for messages, nearby nodes, BLE state, adverts, and low battery.
- One-button Quick Menu and screen-off/wake behavior.
- One-button press/double-press/hold controls and screen-off/wake behavior.
- Subtle page transitions and notification motion.
- RGB565 framebuffer with 8-row delta flushing; measured RC1 delta frame: **121 ms** versus **342–360 ms** for a full frame.
- Static BLE frame queues sized for companion synchronization bursts.
- Responsive iPhone-dark WebUI for phones and desktops, served directly by the RCC6.
- 2.4 GHz setup AP, local Wi-Fi setup wizard, DHCP address on the TFT, and automatic setup-AP fallback.
- One exclusive WebUI or raw TCP/5000 companion session at a time.
- DIO flash mode for RCC6 compatibility.

## Controls

| Action | Result |
|---|---|
| Press while screen is off | Wake the display; the press is consumed |
| Press while screen is on | Next page or next menu item |
| Hold | Open Quick Menu |
| Press in Quick Menu | Move the highlight |
| Hold in Quick Menu | Select the highlighted action |
| Press while screen is on | Next page or next message page |
| Double-press | Run the action shown on the current page |
| Hold | Open the Power confirmation page |
| Hold again within eight seconds | Power off after the button is released |

Wait at least eight seconds after boot before using Hold; the early-boot hold remains MeshCore's CLI rescue gesture.

Expand All @@ -56,7 +59,8 @@ Wait at least eight seconds after boot before using Hold; the early-boot hold re
- Heltec **RadioCore RCC6** / ESP32-C6
- RCC6 LoRa module configuration used by the upstream `heltec_rcc6` target
- Attached Heltec NV3001B **220×128 TFT**
- BLE companion mode
- BLE companion firmware mode
- Wi-Fi Web/AP companion firmware mode with HTTP/80 and TCP/5000

> [!CAUTION]
> This build targets **RCC6 only**. Do not flash it onto RadioCore RC32, RC52, or unrelated ESP32 boards.
Expand All @@ -75,14 +79,18 @@ This repository is based on MeshCore `v1.16.0` development source at upstream co
git clone https://github.com/n30nex/RadioCore2-RCC6.git
cd RadioCore2-RCC6
pio run -e heltec_rcc6_companion_radio_ble
pio run -e heltec_rcc6_companion_radio_web_ap
```

The application binary is produced under:

```text
.pio/build/heltec_rcc6_companion_radio_ble/firmware.bin
.pio/build/heltec_rcc6_companion_radio_web_ap/firmware.bin
```

The Web/AP image starts a WPA-protected setup network and serves its WebUI at `http://192.168.4.1`. After the Home-page setup wizard joins a local 2.4 GHz network, the TFT shows the DHCP address. Station-mode HTTP uses username `meshcore` and the eight-letter device key shown on the TFT. Raw TCP/5000 exposes the full MeshCore companion/admin protocol without application authentication, so use station mode only on a trusted private LAN.

## RC1 validation

Validated on physical hardware:
Expand All @@ -94,7 +102,7 @@ Validated on physical hardware:
- Stable USB enumeration across repeated observation windows and manual resets.
- BLE advertisement as `MeshCore-<node name>` with Nordic UART service `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`.
- Secure six-digit PIN authentication, node load in the companion app, disconnect, re-advertise, and reconnect.
- Native landscape UI and Quick Menu opening on the device.
- Native landscape UI and one-button navigation on the device.
- Battery-powered boot and display operation.

Still to qualify before final 1.0:
Expand Down
84 changes: 60 additions & 24 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>

#ifdef RCC6_WEB_AP
#include <helpers/esp32/SerialWebInterface.h>
extern SerialWebInterface serial_interface;
#endif

#define CMD_APP_START 1
#define CMD_SEND_TXT_MSG 2
#define CMD_SEND_CHANNEL_TXT_MSG 3
Expand Down Expand Up @@ -240,20 +245,6 @@ void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) {
}
}

int MyMesh::getFromOfflineQueue(uint8_t frame[]) {
if (offline_queue_len > 0) { // check offline queue
size_t len = offline_queue[0].len; // take from top of queue
memcpy(frame, offline_queue[0].buf, len);

offline_queue_len--;
for (int i = 0; i < offline_queue_len; i++) { // delete top item from queue
offline_queue[i] = offline_queue[i + 1];
}
return len;
}
return 0; // queue is empty
}

float MyMesh::getAirtimeBudgetFactor() const {
return _prefs.airtime_factor;
}
Expand Down Expand Up @@ -304,14 +295,26 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {

void MyMesh::logTx(mesh::Packet* packet, int len) {
(void) len;
if (_ui) {
#ifdef HELTEC_RCC6_NEON_UI
const bool report = packet->getPayloadType() != PAYLOAD_TYPE_ADVERT || packet == _ui_advert_packet;
if (packet == _ui_advert_packet) _ui_advert_packet = nullptr;
#else
const bool report = true;
#endif
if (_ui && report) {
_ui->onRadioEvent(UIRadioEvent::TxComplete, packet->getPayloadType());
}
}

void MyMesh::logTxFail(mesh::Packet* packet, int len) {
(void) len;
if (_ui) {
#ifdef HELTEC_RCC6_NEON_UI
const bool report = packet->getPayloadType() != PAYLOAD_TYPE_ADVERT || packet == _ui_advert_packet;
if (packet == _ui_advert_packet) _ui_advert_packet = nullptr;
#else
const bool report = true;
#endif
if (_ui && report) {
_ui->onRadioEvent(UIRadioEvent::TxFailed, packet->getPayloadType());
}
}
Expand Down Expand Up @@ -1152,7 +1155,7 @@ void MyMesh::handleCmdFrame(size_t len) {
? ERR_CODE_NOT_FOUND
: ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsupported TXT_TYPE_*
}
} else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel text msg
} else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG && len >= 7) { // send GroupChannel text msg
int i = 1;
uint8_t txt_type = cmd_frame[i++]; // should be TXT_TYPE_PLAIN
uint8_t channel_idx = cmd_frame[i++];
Expand Down Expand Up @@ -1390,12 +1393,17 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) {
int out_len;
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
_serial->writeFrame(out_frame, out_len);
if (offline_queue_len > 0) {
const size_t out_len = offline_queue[0].len;
if (_serial->writeFrame(offline_queue[0].buf, out_len) == out_len) {
offline_queue_len--;
for (int i = 0; i < offline_queue_len; i++) {
offline_queue[i] = offline_queue[i + 1];
}
#ifdef DISPLAY_CLASS
if (_ui) _ui->msgRead(offline_queue_len);
if (_ui) _ui->msgRead(offline_queue_len);
#endif
}
} else {
out_frame[0] = RESP_CODE_NO_MORE_MESSAGES;
_serial->writeFrame(out_frame, 1);
Expand Down Expand Up @@ -1937,7 +1945,14 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type
}
} else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
} else if (cmd_frame[0] == CMD_FACTORY_RESET && len >= 6 &&
memcmp(&cmd_frame[1], "reset", 5) == 0) {
#ifdef RCC6_WEB_AP
if (!serial_interface.clearStoredNetworkConfig()) {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
return;
}
#endif
if (_serial) {
MESH_DEBUG_PRINTLN("Factory reset: disabling serial interface to prevent reconnects (BLE/WiFi)");
_serial->disable(); // Phone app disconnects before we can send OK frame so it's safe here
Expand Down Expand Up @@ -2270,15 +2285,36 @@ void MyMesh::loop() {
#endif
}

bool MyMesh::advert() {
bool MyMesh::advert(bool flood) {
#ifdef HELTEC_RCC6_NEON_UI
if (_ui_advert_packet != nullptr) return false;
#endif
mesh::Packet* pkt;
if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) {
pkt = createSelfAdvert(_prefs.node_name);
} else {
pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon);
}
if (pkt) {
sendZeroHop(pkt);
if (flood) {
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
sendFloodScoped(default_scope, pkt, 0);
} else {
sendZeroHop(pkt);
}
#ifdef HELTEC_RCC6_NEON_UI
bool queued = false;
const int outbound_count = _mgr->getOutboundTotal();
for (int i = 0; i < outbound_count; i++) {
if (_mgr->getOutboundByIdx(i) == pkt) {
queued = true;
break;
}
}
if (!queued) return false;
_ui_advert_packet = pkt;
#endif
return true;
} else {
return false;
Expand Down
9 changes: 7 additions & 2 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {

void loop();
void handleCmdFrame(size_t len);
bool advert();
bool advert(bool flood = false);
#ifdef HELTEC_RCC6_NEON_UI
bool isAdvertPending() const { return _ui_advert_packet != nullptr; }
#endif
void enterCLIRescue();

int getRecentlyHeard(AdvertPath dest[], int max_num);
Expand Down Expand Up @@ -190,7 +193,6 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
void writeContactRespFrame(uint8_t code, const ContactInfo &contact);
void updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len);
void addToOfflineQueue(const uint8_t frame[], int len);
int getFromOfflineQueue(uint8_t frame[]);
int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override {
return _store->getBlobByKey(key, key_len, dest_buf);
}
Expand All @@ -214,6 +216,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
uint32_t pending_req; // pending _BINARY_REQ
BaseSerialInterface *_serial;
AbstractUITask* _ui;
#ifdef HELTEC_RCC6_NEON_UI
mesh::Packet* _ui_advert_packet = nullptr;
#endif

ContactsIterator _iter;
uint32_t _iter_filter_since;
Expand Down
51 changes: 47 additions & 4 deletions examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ static uint32_t _atoi(const char* sp) {
#endif

#ifdef ESP32
#ifdef WIFI_SSID
#ifdef RCC6_WEB_AP
#include <helpers/esp32/SerialWebInterface.h>
SerialWebInterface serial_interface;
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#elif defined(WIFI_SSID)
#include <helpers/esp32/SerialWifiInterface.h>
SerialWifiInterface serial_interface;
#ifndef TCP_PORT
Expand Down Expand Up @@ -105,7 +111,9 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
/* END GLOBAL OBJECTS */

void halt() {
while (1) ;
while (1) {
delay(1000);
}
}

/* WIFI RECONNECT TRACKERS */
Expand Down Expand Up @@ -135,7 +143,21 @@ void setup() {
}
#endif

if (!radio_init()) { halt(); }
if (!radio_init()) {
Serial.println("ERROR: radio initialization failed");
#ifdef DISPLAY_CLASS
if (disp != NULL) {
disp->startFrame();
disp->setTextSize(1);
disp->setColor(DisplayDriver::RED);
disp->drawTextCentered(disp->width() / 2, disp->height() / 2 - 10, "RADIO INIT FAILED");
disp->setColor(DisplayDriver::LIGHT);
disp->drawTextCentered(disp->width() / 2, disp->height() / 2 + 8, "Reset device");
disp->endFrame();
}
#endif
halt();
}

fast_rng.begin(radio_driver.getRngSeed());

Expand Down Expand Up @@ -208,7 +230,25 @@ void setup() {
#endif
the_mesh.startInterface(serial_interface);
#elif defined(ESP32)
#ifdef HELTEC_RCC6_NEON_UI
if (!SPIFFS.begin(false)) {
Serial.println("ERROR: SPIFFS mount failed; settings were not formatted");
#ifdef DISPLAY_CLASS
if (disp != NULL) {
disp->startFrame();
disp->setTextSize(1);
disp->setColor(DisplayDriver::RED);
disp->drawTextCentered(disp->width() / 2, disp->height() / 2 - 10, "STORAGE ERROR");
disp->setColor(DisplayDriver::LIGHT);
disp->drawTextCentered(disp->width() / 2, disp->height() / 2 + 8, "Settings preserved");
disp->endFrame();
}
#endif
halt();
}
#else
SPIFFS.begin(true);
#endif
store.begin();
the_mesh.begin(
#ifdef DISPLAY_CLASS
Expand All @@ -218,7 +258,10 @@ void setup() {
#endif
);

#ifdef WIFI_SSID
#ifdef RCC6_WEB_AP
board.setInhibitSleep(true); // prevent sleep while the access point is active
serial_interface.begin(the_mesh.getNodePrefs()->node_name, TCP_PORT);
#elif defined(WIFI_SSID)
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.setAutoReconnect(true);

Expand Down
Loading
Loading