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
8 changes: 4 additions & 4 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.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"
app_name = "NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.4-app.bin"
recovery_name = "NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.4-full-recovery-preserves-meshcore-settings.bin"
configurator_name = "NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.4-configurator.zip"
(out / app_name).write_bytes(app)
(out / recovery_name).write_bytes(merged)

Expand Down Expand Up @@ -115,7 +115,7 @@ jobs:
build_info = (
"Git commit: ${{ github.event.pull_request.head.sha || github.sha }}\n"
"Target: heltec_rcc6_repeater_observer_mqtt\n"
"MeshCore base: official v1.17.0 + post-release SX126x RX-gain fix\n"
"MeshCore target baseline: official v1.17.1\n"
"Observer base: agessaman/MeshCore b744b42a plus reviewed stability fixes through 8d1a0eb3\n"
)
(out / "QUICK_START.txt").write_text(quick_start, encoding="utf-8")
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ MeshCore repeater and room-server firmware for the **Heltec RadioCore RCC6-L62 /

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

This project uses the official MeshCore 1.17.0 baseline, the post-release SX126x boosted-RX-gain persistence fix intended for the next receiver maintenance release, and the production MQTT observer base plus reviewed memory, reconnect, retry, and packet-path fixes from [`agessaman/MeshCore`](https://github.com/agessaman/MeshCore/tree/8d1a0eb3). There is no official upstream MeshCore 1.17.1 tag at the time of this release.
This project is synchronized with the target-relevant MeshCore 1.17.1 maintenance fixes, including SX126x boosted-RX-gain persistence and corrected server reply routing, plus the production MQTT observer base and reviewed memory, reconnect, retry, and packet-path fixes from [`agessaman/MeshCore`](https://github.com/agessaman/MeshCore/tree/8d1a0eb3).

## Choose the firmware profile

Expand Down 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. Current `v1.3.0-rc.3` rejects impossible RCC6 battery readings while retaining 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.4` fixes saved-location adverts on GPS-less builds while retaining GPS controls on supported hardware. 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.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 |
| Ultimate repeater/observer `v1.3.0-rc.4` | Supplied Windows/Linux network wizard |
| Ultimate Room Server full headless or full TFT | `v1.3.0-rc.4` 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.3-configurator.zip` from the same release as the selected firmware.
Download `NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.4-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.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`
- `NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.4-app.bin`
- `NeonPocketMC-RCC6-Ultimate-Observer-v1.3.0-rc.4-full-recovery-preserves-meshcore-settings.bin`
- `NeonPocketMC-RCC6-Ultimate-Server-v1.3.0-rc.4-configurator.zip`

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

| Profile | Application update | Recovery image |
|---|---|---|
| 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` |
| Minimal headless | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-headless-v1.3.0-rc.4-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-headless-v1.3.0-rc.4-full-recovery-preserves-meshcore-settings.bin` |
| Minimal TFT | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-tft-v1.3.0-rc.4-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-minimal-tft-v1.3.0-rc.4-full-recovery-preserves-meshcore-settings.bin` |
| Full headless | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-headless-v1.3.0-rc.4-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-headless-v1.3.0-rc.4-full-recovery-preserves-meshcore-settings.bin` |
| Full TFT | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-tft-v1.3.0-rc.4-app.bin` | `NeonPocketMC-RCC6-Ultimate-Room-Server-full-tft-v1.3.0-rc.4-full-recovery-preserves-meshcore-settings.bin` |

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

Expand All @@ -295,7 +295,7 @@ Do not erase the whole flash if you want to retain identity and settings. Never

## Source and scope

- MeshCore base: 1.17.0.
- MeshCore target baseline: 1.17.1.
- MQTT observer base: `agessaman/MeshCore` `observer-firmware` at `b744b42a`, plus selected reviewed stability fixes through `8d1a0eb3`.
- RCC6 hardware mapping provenance: the separately tested `NeonPocketMC-RCC6` companion project.
- Release targets: the established `heltec_rcc6_repeater_observer_mqtt` plus all four explicit Room Server profiles listed above.
Expand Down
9 changes: 9 additions & 0 deletions docs/releases/1.3-RC4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# NeonPocketMC RCC6 Server Suite v1.3.0-rc.4

Experimental prerelease built from the exact tagged `main` commit and MeshCore 1.17.1.

- Saved-coordinate adverts work on GPS-less RCC6 observer and Room Server profiles.
- Old live-location settings migrate to saved-coordinate mode on GPS-less builds.
- GPS-capable regression targets retain live-GPS controls.

The release contains the observer, four Room Server profiles, guided configurator, exact source/licenses bundle, and checksums.
7 changes: 7 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,10 @@ void MyMesh::begin(bool has_display) {
_store->loadPrefs(_prefs);
sensors.node_lat = _prefs.node_lat;
sensors.node_lon = _prefs.node_lon;
if (_prefs.advert_loc_policy == ADVERT_LOC_SHARE && sensors.getLocationProvider() == nullptr) {
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_store->savePrefs(_prefs);
}

// sanitise bad pref values
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
Expand Down Expand Up @@ -1448,6 +1452,9 @@ void MyMesh::handleCmdFrame(size_t len) {

if (len >= 4) {
_prefs.advert_loc_policy = cmd_frame[3];
if (_prefs.advert_loc_policy == ADVERT_LOC_SHARE && sensors.getLocationProvider() == nullptr) {
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
}
if (len >= 5) {
_prefs.multi_acks = cmd_frame[4];
}
Expand Down
3 changes: 2 additions & 1 deletion examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2

class NodePrefs : public ConfigSerializer { // persisted to file
public:
Expand Down Expand Up @@ -135,4 +136,4 @@ class NodePrefs : public ConfigSerializer { // persisted to file
// new accessor methods
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
};
};
55 changes: 36 additions & 19 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -475,24 +475,30 @@ bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[])
}

void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // if _request_ packet scope is known, send reply with same scope
TransportKey scope;
if (region_map.getTransportKeysFor(*recv_pkt_region, &scope, 1) > 0) {
sendFloodScoped(scope, packet, delay_millis, path_hash_size);
} else {
sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
}
} else {
sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
TransportKey request_scope;
const bool request_was_unscoped = recv_pkt_region != NULL && recv_pkt_region->isWildcard();
const bool request_scope_known = recv_pkt_region != NULL && !request_was_unscoped
&& region_map.getTransportKeysFor(*recv_pkt_region, &request_scope, 1) > 0;

switch (mesh::chooseReplyScope(request_scope_known, request_was_unscoped, !default_scope.isNull())) {
case mesh::REPLY_SCOPE_REQUEST:
sendFloodScoped(request_scope, packet, delay_millis, path_hash_size);
break;
case mesh::REPLY_SCOPE_DEFAULT:
sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
break;
case mesh::REPLY_SCOPE_NONE:
sendFlood(packet, delay_millis, path_hash_size);
break;
}
}

bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood()) {
if (packet->getPathHashCount() >= _prefs.flood_max) return false;
if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false;
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false;
if (packet->isRouteFlood()
&& mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max, _prefs.flood_max_unscoped,
_prefs.flood_max_advert)) {
return false;
}
if (packet->isRouteFlood() && recv_pkt_region == NULL) {
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
Expand Down Expand Up @@ -692,17 +698,28 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m

if (reply_len == 0) return; // invalid request

if (packet->isRouteFlood()) {
ClientInfo* client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
const bool have_out_path = client != NULL && client->out_path_len != OUT_PATH_UNKNOWN;
const mesh::ReplyRoute route =
mesh::chooseReplyRoute(packet->isRouteFlood(), reply_path_len != 0xFF, have_out_path);

if (route == mesh::REPLY_ROUTE_PATH_RETURN) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
} else if (reply_path_len == 0xFF) {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
return;
}

mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply == NULL) return;

if (route == mesh::REPLY_ROUTE_DIRECT_SUPPLIED) {
sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY);
} else if (route == mesh::REPLY_ROUTE_DIRECT_OUT_PATH) {
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY);
sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include <helpers/ClientACL.h>
#include <helpers/CommonCLI.h>
#include <helpers/IdentityStore.h>
#include <helpers/RoutingPolicy.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/StatsFormatHelper.h>
Expand Down
32 changes: 19 additions & 13 deletions examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,10 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {

bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood()) {
if (packet->getPathHashCount() >= _prefs.flood_max) return false;
if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false;
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false;
if (packet->isRouteFlood()
&& mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max, _prefs.flood_max_unscoped,
_prefs.flood_max_advert)) {
return false;
}
return true;
}
Expand Down Expand Up @@ -1072,15 +1072,21 @@ bool MyMesh::resolveAlertScope(TransportKey& dest) {
}

void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // if _request_ packet scope is known, send reply with same scope
TransportKey scope;
if (region_map.getTransportKeysFor(*recv_pkt_region, &scope, 1) > 0) {
sendFloodScoped(scope, packet, delay_millis, path_hash_size);
} else {
sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
}
} else {
sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
TransportKey request_scope;
const bool request_was_unscoped = recv_pkt_region != NULL && recv_pkt_region->isWildcard();
const bool request_scope_known = recv_pkt_region != NULL && !request_was_unscoped
&& region_map.getTransportKeysFor(*recv_pkt_region, &request_scope, 1) > 0;

switch (mesh::chooseReplyScope(request_scope_known, request_was_unscoped, !default_scope.isNull())) {
case mesh::REPLY_SCOPE_REQUEST:
sendFloodScoped(request_scope, packet, delay_millis, path_hash_size);
break;
case mesh::REPLY_SCOPE_DEFAULT:
sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
break;
case mesh::REPLY_SCOPE_NONE:
sendFlood(packet, delay_millis, path_hash_size);
break;
}
}

Expand Down
1 change: 1 addition & 0 deletions examples/simple_room_server/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/IdentityStore.h>
#include <helpers/RoutingPolicy.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/AlertReporter.h>
#include <helpers/TxtDataHelpers.h>
Expand Down
21 changes: 18 additions & 3 deletions scripts/verify_rcc6_room_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def main() -> None:
full_tft = section(ini, expected_envs[3])

require(common, "NEONPOCKET_RCC6_ROOM_SERVER=1", "NEONPOCKET_SAFE_SPIFFS_BOOTSTRAP=1",
"+<../examples/simple_room_server>")
"+<../examples/simple_room_server>", "FIRMWARE_VERSION='\"v1.17.1-")
require(full, "WITH_MQTT_BRIDGE=1", "WEBCONFIG_AUTO_LAN=1",
"MQTT_DEFAULT_SLOT1_PRESET", "MQTT_DEFAULT_SLOT2_PRESET",
"+<helpers/bridges/MQTTBridge.cpp>")
Expand Down Expand Up @@ -82,10 +82,24 @@ def main() -> None:

mesh_h = read("examples/simple_room_server/MyMesh.h")
mesh_cpp = read("examples/simple_room_server/MyMesh.cpp")
common_cli = read("src/helpers/CommonCLI.cpp")
routing = read("src/helpers/RoutingPolicy.h")
require(mesh_h, "struct RoomSnapshot", "void getRoomSnapshot",
"command requires USB serial or the room RF protocol")
"command requires USB serial or the room RF protocol", "helpers/RoutingPolicy.h")
require(mesh_cpp, "room_active_clients", "room_posts", "room_pushes",
"NEONPOCKET_ROOM_SERVER_PROFILE", "get room.profile")
"NEONPOCKET_ROOM_SERVER_PROFILE", "get room.profile", "chooseReplyScope",
"isFloodHopLimitExceeded")
require(routing, "chooseReplyRoute", "chooseReplyScope", "isFloodHopLimitExceeded")
share_command = common_cli.index('strcmp(command, "gps advert share")')
gps_hardware_guard = common_cli.index("#if ENV_INCLUDE_GPS == 1", share_command)
if common_cli.index('strcmp(command, "gps advert prefs")') > share_command:
raise AssertionError("saved-coordinate advert policy must not require physical GPS hardware")
if "_sensors->getLocationProvider() != NULL" not in \
common_cli[share_command:gps_hardware_guard]:
raise AssertionError("live-location advert policy must require an actual GPS provider")
require(common_cli, "normalized_advert_location",
"_prefs->advert_loc_policy = ADVERT_LOC_PREFS;",
"_sensors->getLocationProvider() == NULL")

display_h = read("src/helpers/ui/NV3001BDisplay.h")
display_cpp = read("src/helpers/ui/NV3001BDisplay.cpp")
Expand All @@ -112,6 +126,7 @@ def main() -> None:
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(repeater_mesh, "chooseReplyRoute", "chooseReplyScope", "isFloodHopLimitExceeded")
require(mesh_cpp, "if (battery_mv) telemetry.addVoltage")
require(repeater_mesh, "if (battery_mv) telemetry.addVoltage")
mqtt = read("src/helpers/bridges/MQTTBridge.cpp")
Expand Down
Loading
Loading