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
76 changes: 71 additions & 5 deletions .github/workflows/neonpocket-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,115 @@ on:
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Verify exact checkout
run: test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}"
- uses: ./.github/actions/setup-build-environment
- name: Run upstream unit tests
run: pio test -e native -vv

webui:
runs-on: ubuntu-latest
defaults:
run:
working-directory: examples/companion_radio/webui
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Verify exact checkout
run: test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}"
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: examples/companion_radio/webui/package-lock.json
- name: Build and verify embedded WebUI
run: |
npm ci
npm test
git diff --exit-code -- Rcc6WebUiAssets.h

firmware:
needs: [tests, webui]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- environment: heltec_v3_neonpocket_companion_ble
transport: BLE
ultimate: false
- environment: heltec_v3_neonpocket_companion_usb
transport: USB
ultimate: false
- environment: heltec_v3_ultimate_companion_ble
transport: Ultimate-BLE
ultimate: true
- environment: heltec_v3_ultimate_companion_web
transport: Ultimate-Web
ultimate: true
- environment: heltec_v3_ultimate_repeater_web
transport: Ultimate-Repeater-Web
ultimate: true
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Verify exact checkout
run: test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}"
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install PlatformIO
run: pip install platformio
- name: Verify product contract
run: python scripts/verify_neonpocket_mono.py
- name: Verify product contracts
run: |
python scripts/verify_neonpocket_mono.py
python scripts/verify_ultimate_mono.py
- name: Build application and recovery images
env:
ULTIMATE_BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: pio run -e ${{ matrix.environment }} -t mergebin
- name: Package checksums
- name: Verify and package images
shell: bash
run: |
out=.pio/build/${{ matrix.environment }}
python - "$out/firmware.bin" "$out/firmware-merged.bin" <<'PY'
from pathlib import Path
import sys
app, merged = map(lambda value: Path(value).read_bytes(), sys.argv[1:])
assert app and app[0] == 0xE9, "not an ESP application image"
assert len(app) < 3 * 1024 * 1024, "application exceeds 3 MiB budget"
assert len(merged) > len(app), "recovery image is incomplete"
PY
mkdir -p dist
cp "$out/firmware.bin" "dist/NeonPocketMC-Heltec-V3-${{ matrix.transport }}-app.bin"
cp "$out/firmware-merged.bin" "dist/NeonPocketMC-Heltec-V3-${{ matrix.transport }}-recovery-preserves-settings.bin"
(cd dist && sha256sum *.bin > SHA256SUMS.txt)
- uses: actions/upload-artifact@v4
with:
name: NeonPocketMC-Heltec-V3-${{ matrix.transport }}-${{ github.sha }}
name: NeonPocketMC-Heltec-V3-${{ matrix.transport }}-${{ github.event.pull_request.head.sha || github.sha }}
path: dist/*
if-no-files-found: error
retention-days: 14

required-firmware:
name: required-firmware
if: ${{ always() }}
needs: firmware
runs-on: ubuntu-latest
steps:
- name: Require both firmware images
- name: Require every firmware image
env:
FIRMWARE_RESULT: ${{ needs.firmware.result }}
run: test "$FIRMWARE_RESULT" = success
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

# NeonPocketMC for Heltec V3

Experimental MeshCore BLE or native-USB companion firmware for the **Heltec WiFi LoRa 32 V3 with its integrated 128×64 SSD1306 OLED and SX1262**.
Experimental MeshCore BLE, native-USB, and Ultimate Wi-Fi/Web companion firmware for the **Heltec WiFi LoRa 32 V3 with its integrated 128×64 SSD1306 OLED and SX1262**.

> [!WARNING]
> **Heltec V3 only. Do not flash this on Heltec V4, RCC6, RC52, Wireless Tracker, or a board with different display/radio hardware.** This repository ships separate BLE and native-USB companion images—no Wi-Fi, repeater, or room-server firmware.
> **Heltec V3 only. Do not flash this on Heltec V4, RCC6, RC52, Wireless Tracker, or a board with different display/radio hardware.** BLE, USB, and Web are separate images. Never flash a build for another board.

**Guided install:** [flasher.canadaverse.org](https://flasher.canadaverse.org/)

Expand All @@ -20,6 +20,22 @@ Experimental MeshCore BLE or native-USB companion firmware for the **Heltec WiFi
- 60-second OLED timeout while BLE and LoRa continue running.
- Fail-closed storage mounting. Existing identity, contacts, channels, and preferences are not silently formatted after a mount error.

## Ultimate experimental builds

Three additional targets bring the portable RCC6 Ultimate experience to the 128×64 OLED:

- `heltec_v3_ultimate_companion_ble`: standard MeshCore BLE companion compatibility plus persistent history, metrics, recent-node signal tracking, composer state, and diagnostics.
- `heltec_v3_ultimate_companion_web`: setup AP, local 2.4 GHz Wi-Fi mode, authenticated offline WebUI, full TCP companion protocol on port 5000, history/export/settings/location APIs, and signed app-slot OTA support.
- `heltec_v3_ultimate_repeater_web`: a dedicated MeshCore repeater with an authenticated live dashboard, recent-neighbor view, radio/packet/battery/airtime graphs, flood-advert controls, restricted radio/location/name configuration, and AP-to-LAN onboarding.

Both use 350 contacts, 40 channels, 256 pending companion frames, a 32-event callback queue, a 64-node recent-heard cache, selectable 0/128/512/2048-message history, seven-day hourly metrics, a 32 KiB runtime allocation gate, and the existing NeonPocket demo-scene startup. The OLED adds compact Network and Ultimate status pages; the full composer, charts, history manager, location transfer, and Wi-Fi onboarding live in the WebUI.

BLE and Wi-Fi never run together. In Web mode, HTTP is authenticated, but TCP port 5000 intentionally exposes the complete MeshCore companion/admin protocol to the trusted local LAN. Do not use station mode on an untrusted network.

The Ultimate Repeater is a separate role image: it does not expose the companion TCP protocol and it never runs BLE. On first boot, join the `NeonPocket-XXXXXX` setup AP and open `http://192.168.4.1`. The OLED and USB serial output show the generated 12-character key; use username `neonpocket`. After local Wi-Fi is saved, the OLED shows the LAN IP at boot. If the saved network cannot be reached within ten seconds, the private setup AP returns. Wi-Fi stays awake in this build, so use the standard repeater image for solar/battery deployments.

The RCC6 indexed color framebuffer, 220×128 tile animations, and color-only notification effects are hardware-specific and are not copied to this monochrome OLED build.

## Button controls

- Screen off: the first gesture only wakes the OLED.
Expand Down
22 changes: 22 additions & 0 deletions examples/companion_radio/AbstractUITask.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ enum class UIEventType {
ack
};

enum class UIRadioEvent : uint8_t {
Rx,
TxComplete,
TxFailed
};

class AbstractUITask {
protected:
mesh::MainBoard* _board;
Expand All @@ -36,11 +42,27 @@ class AbstractUITask {
void setHasConnection(bool connected) { _connected = connected; }
bool hasConnection() const { return _connected; }
uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); }
bool isSerialEnabled() const { return _interfaceManager->isEnabled(); }
void enableSerial() { _interfaceManager->enable(); }
void disableSerial() { _interfaceManager->disable(); }
bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); }
void enableBluetooth() { _interfaceManager->enableBluetooth(); }
void disableBluetooth() { _interfaceManager->disableBluetooth(); }
virtual void msgRead(int msgcount) = 0;
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
virtual void newMsgWithEvent(uint8_t path_len, const char* from_name, const char* text,
int msgcount, UIEventType type) {
(void) type;
newMsg(path_len, from_name, text, msgcount);
}
virtual void notify(UIEventType t = UIEventType::none) = 0;
virtual void onNewContactVisual() { }
virtual void onRadioEvent(UIRadioEvent event, uint8_t payload_type,
int16_t rssi_dbm = 0, int16_t snr_quarter_db = 0) {
(void) event;
(void) payload_type;
(void) rssi_dbm;
(void) snr_quarter_db;
}
virtual void loop() = 0;
};
Loading
Loading