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
6 changes: 3 additions & 3 deletions .github/workflows/rcc6-release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ jobs:

out = Path("release")
out.mkdir()
app_name = "NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.2-app.bin"
recovery_name = "NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.2-full-recovery-preserves-meshcore-settings.bin"
configurator_name = "NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.2-configurator.zip"
app_name = "NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.3-app.bin"
recovery_name = "NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.3-full-recovery-preserves-meshcore-settings.bin"
configurator_name = "NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.3-configurator.zip"
(out / app_name).write_bytes(app)
(out / recovery_name).write_bytes(merged)

Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The USB configurator accepts only the two full Room Server profiles (and the exi

## What each role does

- **RCC6 Ultimate observer/repeater:** forwards mesh traffic, observes packets, joins 2.4 GHz Wi-Fi, publishes to at most two MQTT brokers, and serves the full Ultimate dashboard. The human-first analytics and Nearby overhaul arrived in RC1; current `v1.3.0-rc.2` adds explicit saved-location advert sharing. Prior server/repeater releases remain available.
- **RCC6 Ultimate observer/repeater:** forwards mesh traffic, observes packets, joins 2.4 GHz Wi-Fi, publishes to at most two MQTT brokers, and serves the full Ultimate dashboard. Current `v1.3.0-rc.3` rejects impossible RCC6 battery readings while retaining explicit saved-location advert sharing. Prior server/repeater releases remain available.
- **Room Server, both sizes:** hosts the standard MeshCore room/client protocol with 32 recent posts held in RAM. A reboot clears those buffered posts. Repeating is optional but defaults off; a separate repeater is recommended.
- **Room Server, minimal:** LoRa room service and USB CLI only. It has no Wi-Fi, Web dashboard, or MQTT code to configure.
- **Room Server, full:** adds 2.4 GHz AP/STA onboarding, the authenticated dashboard, and one-way MQTT observation. MQTT data is never injected into RF.
Expand All @@ -65,15 +65,15 @@ All roles use fail-closed MeshCore storage and default their own adverts to **3-

| Image | Setup after flashing |
|---|---|
| Ultimate repeater/observer `v1.3.0-rc.2` | Supplied Windows/Linux network wizard |
| Ultimate Room Server full headless or full TFT | `v1.3.0-rc.2` network wizard; it also changes both room passwords |
| Ultimate repeater/observer `v1.3.0-rc.3` | Supplied Windows/Linux network wizard |
| Ultimate Room Server full headless or full TFT | `v1.3.0-rc.3` network wizard; it also changes both room passwords |
| Room Server minimal headless or minimal TFT | USB serial CLI or the generic MeshCore USB configurator; no network wizard |

Always attach a tuned LoRa antenna before powering or transmitting. Flash the selected application image at `0x10000`, leave USB connected, and do not deploy until the radio settings and passwords have been changed.

### Guided setup for observer and full profiles

Download `NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.2-configurator.zip` from the same release as the selected firmware.
Download `NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.3-configurator.zip` from the same release as the selected firmware.

- **Windows:** double-click `Configure-RCC6-Windows.cmd`.
- **Linux:** open the extracted folder in a terminal and run `sh configure-rcc6-linux.sh`.
Expand Down Expand Up @@ -264,18 +264,18 @@ Install [esptool](https://docs.espressif.com/projects/esptool/en/latest/esp32c6/

The current RCC6 Ultimate server release files are:

- `NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.2-app.bin`
- `NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.2-full-recovery-preserves-meshcore-settings.bin`
- `NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.2-configurator.zip`
- `NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.3-app.bin`
- `NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.3-full-recovery-preserves-meshcore-settings.bin`
- `NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.3-configurator.zip`

Room Server `v1.3.0-rc.2` uses these profile-specific names:
Room Server `v1.3.0-rc.3` uses these profile-specific names:

| Profile | Application update | Recovery image |
|---|---|---|
| Minimal headless | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-headless-v1.3.0-rc.2-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-headless-v1.3.0-rc.2-full-recovery-preserves-meshcore-settings.bin` |
| Minimal TFT | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-tft-v1.3.0-rc.2-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-tft-v1.3.0-rc.2-full-recovery-preserves-meshcore-settings.bin` |
| Full headless | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-headless-v1.3.0-rc.2-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-headless-v1.3.0-rc.2-full-recovery-preserves-meshcore-settings.bin` |
| Full TFT | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-tft-v1.3.0-rc.2-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-tft-v1.3.0-rc.2-full-recovery-preserves-meshcore-settings.bin` |
| Minimal headless | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-headless-v1.3.0-rc.3-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-headless-v1.3.0-rc.3-full-recovery-preserves-meshcore-settings.bin` |
| Minimal TFT | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-tft-v1.3.0-rc.3-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-tft-v1.3.0-rc.3-full-recovery-preserves-meshcore-settings.bin` |
| Full headless | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-headless-v1.3.0-rc.3-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-headless-v1.3.0-rc.3-full-recovery-preserves-meshcore-settings.bin` |
| Full TFT | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-tft-v1.3.0-rc.3-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-tft-v1.3.0-rc.3-full-recovery-preserves-meshcore-settings.bin` |

Do not substitute a similarly named observer, profile, or development artifact.

Expand Down
15 changes: 15 additions & 0 deletions docs/releases/1.3-RC3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# RCC6 Ultimate Observer & Room Server v1.3.0 RC3

This experimental RCC6-L62/SX1262 safety update contains the Ultimate MQTT observer/repeater and all four Room Server profiles.

## Battery-reading correction

RCC6 ADC samples above the plausible 4.50 V ceiling for a single-cell battery are now treated as unavailable. TFT and Web dashboards show an unknown state instead of an impossible voltage, stale low-battery warnings are cleared, and MQTT status omits an unavailable measurement.

## Files and flashing

The release includes observer and minimal/full Room Server app images, matching settings-preserving recovery images, the Windows/Linux configurator, exact source/licenses, and an aggregate SHA-256 manifest.

Normal updates use the selected app image at `0x10000`. Use a matching recovery image at `0x0` only for bootloader/partition recovery. Every recovery image stops before SPIFFS; never erase the whole chip.

MQTT remains observation-only and never injects broker traffic into the mesh.
3 changes: 2 additions & 1 deletion examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions

telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
const uint16_t battery_mv = board.getBattMilliVolts();
if (battery_mv) telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)battery_mv / 1000.0f);

// query other sensors -- target specific
if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
Expand Down
3 changes: 2 additions & 1 deletion examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions

telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
const uint16_t battery_mv = board.getBattMilliVolts();
if (battery_mv) telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)battery_mv / 1000.0f);
// query other sensors -- target specific
if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
perm_mask = 0x00; // just base telemetry allowed
Expand Down
13 changes: 10 additions & 3 deletions examples/simple_room_server/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ void UITask::renderCurrScreen() {
if (room.batt_mv != 0) {
if (!_battery_low && room.batt_mv <= ROOM_LOW_BATTERY_MV) _battery_low = true;
else if (_battery_low && room.batt_mv >= ROOM_LOW_BATTERY_CLEAR_MV) _battery_low = false;
} else {
_battery_low = false;
}
char value[48];

Expand Down Expand Up @@ -144,9 +146,14 @@ void UITask::renderCurrScreen() {
_display->print(value);

_display->setCursor(3, 86);
snprintf(value, sizeof(value), "TXQ %u ERR %04X BAT %.2fV%s",
(unsigned)room.tx_queue, (unsigned)room.error_flags,
room.batt_mv / 1000.0f, _battery_low ? " LOW" : "");
if (room.batt_mv) {
snprintf(value, sizeof(value), "TXQ %u ERR %04X BAT %.2fV%s",
(unsigned)room.tx_queue, (unsigned)room.error_flags,
room.batt_mv / 1000.0f, _battery_low ? " LOW" : "");
} else {
snprintf(value, sizeof(value), "TXQ %u ERR %04X BAT --",
(unsigned)room.tx_queue, (unsigned)room.error_flags);
}
_display->setColor(room.error_flags || _battery_low ? UIColor::warning_txt
: UIColor::primary_txt);
_display->print(value);
Expand Down
14 changes: 12 additions & 2 deletions scripts/verify_rcc6_room_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ def main() -> None:
"WiFi.disconnect(true", "next_memory_probe = millis() + 30000")

ui_cpp = read("examples/simple_room_server/UITask.cpp")
board_cpp = read("variants/heltec_rcc6/heltec_rcc6.cpp")
boot_h = read("examples/simple_room_server/NeonPocketSplash.h")
require(ui_cpp, "NeonPocketSplash::drawFrame", "NeonPocketSplash::FRAME_MILLIS",
"NEONPOCKET ROOM")
"NEONPOCKET ROOM", '"TXQ %u ERR %04X BAT --"')
require(board_cpp, "measured <= 4500U")
require(boot_h, "DURATION_MILLIS = 3200", "FRAME_MILLIS = 125",
"NEONPOCKETMC", "VECTOR BOOT", "RADIO LINK", "ROOM SERVICES",
"MESH READY", "MAGENTA", "drawPocket", "MESHCORE ROOM SERVER")
Expand Down Expand Up @@ -100,14 +102,22 @@ def main() -> None:
"What this RCC6 can hear", "Signal view", "drawNeighborMap",
"renderTrafficMix", "drawSignalBars", "renderFreshBars",
"/api/neighbors", "No advertised repeater locations yet",
'data-k="gps.adv_loc"', "Share saved coordinates")
'data-k="gps.adv_loc"', "Share saved coordinates",
'battValid?battVolts.toFixed(2):"—"')
web_server = read("src/helpers/esp32/WebConfigServer.cpp")
web_keys = read("src/helpers/WebConfigKeys.h")
require(web_server, 'radio["advert_loc_policy"]', "wcIsValidAdvertLocationPolicy",
'"gps advert %s"')
require(web_keys, '"gps.adv_loc"', "wcIsValidAdvertLocationPolicy")
require(mesh_h, "buildNeighborsJson", "has_location", "latitude_e6", "longitude_e6")
require(mesh_cpp, "buildNeighborsJson", "has_location", "latitude_e6", "longitude_e6")
repeater_mesh = read("examples/simple_repeater/MyMesh.cpp")
require(mesh_cpp, "if (battery_mv) telemetry.addVoltage")
require(repeater_mesh, "if (battery_mv) telemetry.addVoltage")
mqtt = read("src/helpers/bridges/MQTTBridge.cpp")
require(mqtt, "if (measured) battery_mv = measured;")
require(web, "push(st.hist.batt,battValid?battVolts:null)",
"data.filter(Number.isFinite)", "if(!Number.isFinite(v))")
for unsafe_route in ('/api/room/post', '/api/room/delete', '/api/room/client'):
if unsafe_route in web:
raise AssertionError(f"unsafe room mutation route present: {unsafe_route}")
Expand Down
10 changes: 8 additions & 2 deletions src/helpers/bridges/MQTTBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2487,7 +2487,10 @@ void MQTTBridge::publishStatusToSlot(int index) {
int packets_sent = -1;
int packets_received = -1;

if (_board) battery_mv = _board->getBattMilliVolts();
if (_board) {
const uint16_t measured = _board->getBattMilliVolts();
if (measured) battery_mv = measured;
}
if (_ms) uptime_secs = _ms->getMillis() / 1000;
if (_dispatcher) {
errors = _dispatcher->getErrFlags();
Expand Down Expand Up @@ -3365,7 +3368,10 @@ bool MQTTBridge::publishStatus() {
int packets_sent = -1;
int packets_received = -1;

if (_board) battery_mv = _board->getBattMilliVolts();
if (_board) {
const uint16_t measured = _board->getBattMilliVolts();
if (measured) battery_mv = measured;
}
if (_ms) uptime_secs = _ms->getMillis() / 1000;
if (_dispatcher) {
errors = _dispatcher->getErrFlags();
Expand Down
3 changes: 2 additions & 1 deletion variants/heltec_rcc6/heltec_rcc6.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ uint16_t HeltecRCC6Board::getBattMilliVolts() {
raw /= 8;
digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED);

return adcMultiplier * raw;
const uint32_t measured = static_cast<uint32_t>(adcMultiplier * raw);
return measured <= 4500U ? static_cast<uint16_t>(measured) : 0;
Comment thread
n30nex marked this conversation as resolved.
}
4 changes: 2 additions & 2 deletions variants/heltec_rcc6/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ board_build.embed_files = src/certs/x509_crt_bundle.bin
build_flags =
${heltec_rcc6.build_flags}
-D NEONPOCKET_RCC6_REPEATER=1
-D FIRMWARE_VERSION='"v1.17.0-rxfix-rcc6-ultimate-v1.3.0-rc.2"'
-D FIRMWARE_VERSION='"v1.17.0-rxfix-rcc6-ultimate-v1.3.0-rc.3"'
-D ADVERT_NAME='"RCC6 Ultimate Observer"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
Expand Down Expand Up @@ -90,7 +90,7 @@ build_flags =
${heltec_rcc6.build_flags}
-D NEONPOCKET_RCC6_ROOM_SERVER=1
-D NEONPOCKET_SAFE_SPIFFS_BOOTSTRAP=1
-D FIRMWARE_VERSION='"v1.17.0-rcc6-room-v1.3.0-rc.2"'
-D FIRMWARE_VERSION='"v1.17.0-rcc6-room-v1.3.0-rc.3"'
-D ADVERT_NAME='"RCC6 Room Server"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
Expand Down
19 changes: 10 additions & 9 deletions webui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1320,9 +1320,10 @@ <h1 style="margin:16px 0 8px" id="reboot-title">Rebooting&hellip;</h1>
tile("Post deliveries",s.room_pushes,"pushes queued")+
tile("Build profile",s.profile||"full-headless",s.experimental?"experimental":"supported"):
tile("Neighbours",s.neighbors,"recently heard");
var battValid=Number(s.batt_mv)>0,battVolts=battValid?s.batt_mv/1000:null;
$("#tiles").innerHTML=
tile("Uptime",fmtUp(s.uptime_s))+
tile("Battery",(s.batt_mv/1000).toFixed(2),"V")+
tile("Battery",battValid?battVolts.toFixed(2):"—",battValid?"V":"unavailable")+
roleTiles+
tile("Last RF packet",s.recv?fmtAge(s.last_rx_age):"never")+
tile("Packet rate",rxrate.toFixed(1)+" RX","/min · "+txrate.toFixed(1)+" TX")+
Expand Down Expand Up @@ -1350,7 +1351,7 @@ <h1 style="margin:16px 0 8px" id="reboot-title">Rebooting&hellip;</h1>
push(st.hist.noise,s.noise);push(st.hist.txair,txload);push(st.hist.rxair,rxload);
push(st.hist.txq,s.tx_queue);push(st.hist.mqttq,s.mqtt_queue);push(st.hist.heap,kb);
push(st.hist.mqttok,mqttRate);push(st.hist.mqtterr,mqttErrRate);
push(st.hist.heapmin,minKb);push(st.hist.batt,s.batt_mv/1000);push(st.hist.wifi,s.wifi_rssi);
push(st.hist.heapmin,minKb);push(st.hist.batt,battValid?battVolts:null);push(st.hist.wifi,s.wifi_rssi);
renderTrafficMix(s);drawSignalBars();renderFreshBars();
var blue=color("--acc","#2b7de9"),green=color("--ok","#1d9d5f"),red=color("--err","#d64545"),orange=color("--warn","#c8871a"),mut=color("--mut","#66738a");
legend("#leg-packets",[{n:"RX",v:rxrate.toFixed(1)+"/min",c:blue},{n:"TX",v:txrate.toFixed(1)+"/min",c:green}]);
Expand All @@ -1365,7 +1366,7 @@ <h1 style="margin:16px 0 8px" id="reboot-title">Rebooting&hellip;</h1>
plot($("#spark-mqtt"),[{d:st.hist.mqttok,c:green},{d:st.hist.mqtterr,c:red}],false,0);
legend("#leg-heap",[{n:"free",v:kb+" KB",c:green},{n:"minimum",v:minKb+" KB",c:red}]);
plot($("#spark-heap"),[{d:st.hist.heap,c:green},{d:st.hist.heapmin,c:red}],false,0);
legend("#leg-link",[{n:"battery",v:(s.batt_mv/1000).toFixed(2)+" V",c:green},{n:"WiFi",v:s.wifi_rssi+" dBm",c:blue}]);
legend("#leg-link",[{n:"battery",v:battValid?battVolts.toFixed(2)+" V":"—",c:green},{n:"WiFi",v:s.wifi_rssi+" dBm",c:blue}]);
plot($("#spark-link"),[{d:st.hist.batt,c:green},{d:st.hist.wifi,c:blue}],true);
$("#stat-slots").innerHTML=sl.length?sl.map(function(x){
// "filt" is sent only when the slot is not on the all-types default. A
Expand All @@ -1384,22 +1385,22 @@ <h1 style="margin:16px 0 8px" id="reboot-title">Rebooting&hellip;</h1>
if(st.statsOn)st.statsTimer=setTimeout(pollStats,3000);
});
}
function push(a,v){a.push(Number(v)||0);if(a.length>120)a.shift()}
function push(a,v){var n=Number(v);a.push(v==null||!Number.isFinite(n)?null:n);if(a.length>120)a.shift()}
function color(name,fallback){return getComputedStyle(document.documentElement).getPropertyValue(name).trim()||fallback}
function plot(cv,series,perSeries,fixedMin,fixedMax){
var dpr=window.devicePixelRatio||1,w=cv.clientWidth,h=90;
cv.width=w*dpr;cv.height=h*dpr;
var g=cv.getContext("2d");g.scale(dpr,dpr);g.clearRect(0,0,w,h);
g.strokeStyle=color("--line","#e2e7ee");g.lineWidth=1;
[0.25,0.5,0.75].forEach(function(p){g.beginPath();g.moveTo(0,h*p);g.lineTo(w,h*p);g.stroke()});
var all=[];series.forEach(function(s){all=all.concat(s.d)});
var all=[];series.forEach(function(s){all=all.concat(s.d.filter(Number.isFinite))});
if(all.length<2)return;
var commonMin=fixedMin!=null?fixedMin:Math.min.apply(null,all),commonMax=fixedMax!=null?fixedMax:Math.max.apply(null,all);
series.forEach(function(s){var data=s.d;if(data.length<2)return;
var mn=perSeries?Math.min.apply(null,data):commonMin,mx=perSeries?Math.max.apply(null,data):commonMax;
series.forEach(function(s){var data=s.d,valid=data.filter(Number.isFinite);if(valid.length<2)return;
var mn=perSeries?Math.min.apply(null,valid):commonMin,mx=perSeries?Math.max.apply(null,valid):commonMax;
if(fixedMin!=null)mn=fixedMin;if(fixedMax!=null)mx=fixedMax;if(mx===mn){mx+=1;mn-=1}
g.strokeStyle=s.c;g.lineWidth=2;g.lineJoin="round";g.beginPath();
data.forEach(function(v,i){var x=i/(data.length-1)*(w-4)+2,y=h-4-(v-mn)/(mx-mn)*(h-10);i?g.lineTo(x,y):g.moveTo(x,y)});
g.strokeStyle=s.c;g.lineWidth=2;g.lineJoin="round";g.beginPath();var drawing=false;
data.forEach(function(v,i){if(!Number.isFinite(v)){drawing=false;return}var x=i/(data.length-1)*(w-4)+2,y=h-4-(v-mn)/(mx-mn)*(h-10);drawing?g.lineTo(x,y):g.moveTo(x,y);drawing=true});
g.stroke();
});
}
Expand Down
Loading