Skip to content
Open
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
2 changes: 2 additions & 0 deletions build_as_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
src_filter.append("+<helpers/stm32/*>")
elif item == "ESP32":
src_filter.append("+<helpers/esp32/*>")
src_filter.append("+<helpers/wifi/*>")
elif item == "NRF52_PLATFORM":
src_filter.append("+<helpers/nrf52/*>")
elif item == "RP2040_PLATFORM":
src_filter.append("+<helpers/rp2040/*>")
src_filter.append("+<helpers/wifi/*>")

# DISPLAY HANDLING
elif isinstance(item, tuple) and item[0] == "DISPLAY_CLASS":
Expand Down
35 changes: 35 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
_iter_started = false;
_cli_rescue = false;
cli_command[0] = 0;
offline_queue_len = 0;
app_target_ver = 0;
clearPendingReqs();
Expand Down Expand Up @@ -2156,6 +2157,35 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char*
return true;
}

#ifdef WIFI_SSID
// local console only: these are credentials, and remote admin has no business with them
if (sender_timestamp == 0) {
if (memcmp(command, "set wifi.ssid ", 14) == 0) {
StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid));
savePrefs();
sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid);
return true;
}
if (memcmp(command, "set wifi.pwd ", 13) == 0) {
StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd));
savePrefs();
strcpy(reply, "> wifi.pwd updated (reboot to apply)");
return true;
}
if (strcmp(command, "set wifi.clear") == 0) {
_prefs.wifi_ssid[0] = 0;
_prefs.wifi_pwd[0] = 0;
savePrefs();
strcpy(reply, "> wifi config cleared, using build-time credentials (reboot to apply)");
return true;
}
if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design
sprintf(reply, "> %s", _prefs.wifi_ssid[0] ? _prefs.wifi_ssid : "(build-time)");
return true;
}
}
#endif

if (strcmp(command, "board") == 0) {
strcpy(reply, board.getManufacturerName());
return true;
Expand Down Expand Up @@ -2386,6 +2416,11 @@ void MyMesh::loop() {
checkCLIRescueCmd();
} else {
checkSerialInterface();
#if defined(WIFI_SSID) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE)
// RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a
// display + long-press), so serve config commands on the otherwise unused USB serial
checkCLIRescueCmd();
#endif
}

// is there are pending dirty contacts write needed?
Expand Down
27 changes: 26 additions & 1 deletion examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class NodePrefs : public ConfigSerializer { // persisted to file
char default_scope_name[31];
uint8_t default_scope_key[16];
int8_t tz_offset = 0;
#ifdef WIFI_SSID
char wifi_ssid[33] = {0}; // if empty, the compile-time WIFI_SSID is used
char wifi_pwd[64] = {0};
#endif

private:
class RadioPrefs : public CommonRadioPrefs {
Expand Down Expand Up @@ -160,6 +164,20 @@ class NodePrefs : public ConfigSerializer { // persisted to file

DynamicConfigSerializer custom;

#ifdef WIFI_SSID
class WiFiPrefs : public ConfigSerializer {
NodePrefs* _parent;
protected:
void structure() override {
def("ssid", _parent->wifi_ssid, sizeof(_parent->wifi_ssid));
def("pwd", _parent->wifi_pwd, sizeof(_parent->wifi_pwd));
}
public:
WiFiPrefs(NodePrefs* parent) : _parent(parent) { }
};
WiFiPrefs wifi;
#endif

protected:
void structure() override {
def("name", node_name, sizeof(node_name));
Expand All @@ -172,9 +190,16 @@ class NodePrefs : public ConfigSerializer { // persisted to file
def("repeat", repeat);
def("comp", companion);
def("custom", custom);
#ifdef WIFI_SSID
def("wifi", wifi);
#endif
}
public:
NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) {
NodePrefs() : radio(this), gps(this), companion(this), custom(&radio)
#ifdef WIFI_SSID
, wifi(this)
#endif
{
node_name[0] = 0;
default_scope_name[0] = 0;
memset(default_scope_key, 0, sizeof(default_scope_key));
Expand Down
70 changes: 59 additions & 11 deletions examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ MultiSerialInterface interface_manager;
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#ifdef ESP32
// include esp32 wifi interface
#include <helpers/esp32/SerialWifiInterface.h>
#ifndef WIFI_RETRY_INTERVAL
#define WIFI_RETRY_INTERVAL 30000 // millis between reconnect attempts
#endif
#ifndef WIFI_RETRY_TIMEOUT
#define WIFI_RETRY_TIMEOUT 5000 // RP2040: cap on how long one join may block loop()
#endif
#if defined(ESP32) || defined(RP2040_PLATFORM)
#include <helpers/wifi/SerialWifiInterface.h>
SerialWifiInterface wifi_interface;
#else
#error "SerialWifiInterface is not defined for this platform"
Expand Down Expand Up @@ -108,9 +113,12 @@ void halt() {
}

/* WIFI RECONNECT TRACKERS */
#if defined(ESP32) && defined(WIFI_SSID)
#ifdef WIFI_SSID
bool wifi_needs_reconnect = false;
unsigned long last_wifi_reconnect_attempt = 0;
const char* wifi_ssid = WIFI_SSID; // replaced by stored prefs, if set
const char* wifi_pwd = WIFI_PWD;
bool wifi_was_connected = false;
#endif

void setup() {
Expand Down Expand Up @@ -192,6 +200,7 @@ void setup() {

// add wifi interface
#ifdef WIFI_SSID
#if defined(ESP32)
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.setAutoReconnect(true);

Expand All @@ -204,8 +213,28 @@ void setup() {
wifi_needs_reconnect = false;
}
});
#endif

WiFi.begin(WIFI_SSID, WIFI_PWD);
// stored credentials win over the build-time ones ('set wifi.ssid <x>' over USB serial).
// they are taken as a pair, so 'set wifi.ssid' alone gives an open-network join, not a
// silent fallback to the build-time password of a different network.
if (the_mesh.getNodePrefs()->wifi_ssid[0]) {
wifi_ssid = the_mesh.getNodePrefs()->wifi_ssid;
wifi_pwd = the_mesh.getNodePrefs()->wifi_pwd;
}
if (wifi_pwd[0] == 0) wifi_pwd = NULL; // NULL (not "") selects an open network
WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid);

#if defined(RP2040_PLATFORM)
// ponytail: the join itself blocks inside the core (CYW43::begin busy-waits for the
// association), so every attempt stalls the mesh loop. beginNoBlock() only skips the
// extra DHCP wait. Give the first connect a full window, then bound the retries below.
// Upgrade path if the stall ever matters: run WiFi on core1.
WiFi.beginNoBlock(wifi_ssid, wifi_pwd);
last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry
#else
WiFi.begin(wifi_ssid, wifi_pwd);
#endif
wifi_interface.begin(TCP_PORT);
interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface);
#endif
Expand Down Expand Up @@ -262,12 +291,31 @@ void loop() {
#endif
}

#if defined(ESP32) && defined(WIFI_SSID)
// Safely attempt to reconnect every 10 seconds if flagged
if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) {
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect...");
WiFi.disconnect();
WiFi.reconnect();
#ifdef WIFI_SSID
// RP2040 has no WiFi event callbacks, so poll the link state instead
#if defined(RP2040_PLATFORM)
wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED);
if (wifi_was_connected == wifi_needs_reconnect) { // link state changed
wifi_was_connected = !wifi_needs_reconnect;
if (wifi_was_connected) {
WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT);
} else {
WIFI_DEBUG_PRINTLN("link lost");
}
}
#endif

// Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop
// for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall.
if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) {
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status());
#if defined(RP2040_PLATFORM)
WiFi.setTimeout(WIFI_RETRY_TIMEOUT);
WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform
#else
WiFi.disconnect();
WiFi.reconnect();
#endif
last_wifi_reconnect_attempt = millis();
}
#endif
Expand Down
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ build_flags = ${arduino_base.build_flags}
-D ESP32_PLATFORM
; -D ESP32_CPU_FREQ=80 ; change it to your need
build_src_filter = ${arduino_base.build_src_filter}
+<helpers/wifi/*.cpp>

[esp32_ota]
lib_deps =
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/ConfigSerializer.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "ConfigSerializer.h"
#include <stdlib.h> // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not)

bool ConfigSerializer::saveSerial(Stream& s) {
Context context(&s, OP::WRITE);
Expand Down Expand Up @@ -62,6 +63,7 @@ int ConfigSerializer::Context::readNext() {
case EXPECT_COMMA_OR_KEY:
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
case EXPECT_KEY:
if (rd_len == 0 && c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } // empty object, eg. 'custom:{}'
if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; }
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
Expand Down
41 changes: 41 additions & 0 deletions test/test_config_serializer/test_config_serializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,47 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
EXPECT_TRUE(match);
}

class TestNested : public ConfigSerializer {
class Inner : public ConfigSerializer {
protected:
void structure() override { } // no properties, so it writes as '{}'
};
Inner inner;
protected:
void structure() override {
def("age", age);
def("inner", inner);
def("name", name, sizeof(name)); // comes *after* the empty sub-object
}
public:
int32_t age;
char name[16];
};

TEST(ConfigSerializer, LoadSerial_EmptyObject) {
MockInputStream s("{age:" TEST_INT_S ",inner:{},name:\"Scott\"}");
TestNested data;
data.name[0] = 0;

bool success = data.loadSerial(s);
EXPECT_TRUE(success);

EXPECT_EQ(TEST_INT, data.age);
bool match = strcmp("Scott", data.name) == 0;
EXPECT_TRUE(match); // properties after an empty object must still load
}

TEST(ConfigSerializer, LoadSerial_EmptyObjectWithWhitespace) {
MockInputStream s("{age:" TEST_INT_S ",inner:{ },name:\"Scott\"}");
TestNested data;
data.name[0] = 0;

bool success = data.loadSerial(s);
EXPECT_TRUE(success);
bool match = strcmp("Scott", data.name) == 0;
EXPECT_TRUE(match);
}

TEST(DynamicConfigSerializer, GetSet_Basic) {
DynamicConfigSerializer data;

Expand Down
30 changes: 16 additions & 14 deletions variants/rpi_picow/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,22 @@ lib_ignore = BLE
; lib_deps = ${rpi_picow.lib_deps}
; densaugeo/base64 @ ~1.4.0

; [env:PicoW_companion_radio_wifi]
; extends = rpi_picow
; build_flags = ${rpi_picow.build_flags}
; -D MAX_CONTACTS=100
; -D MAX_GROUP_CHANNELS=8
; -D WIFI_DEBUG_LOGGING=1
; -D WIFI_SSID='"myssid"'
; -D WIFI_PWD='"mypwd"'
; ; -D MESH_PACKET_LOGGING=1
; ; -D MESH_DEBUG=1
; build_src_filter = ${rpi_picow.build_src_filter}
; +<../examples/companion_radio/*.cpp>
; lib_deps = ${rpi_picow.lib_deps}
; densaugeo/base64 @ ~1.4.0
[env:PicoW_companion_radio_wifi]
extends = rpi_picow
build_flags = ${rpi_picow.build_flags}
-D MAX_CONTACTS=100
-D MAX_GROUP_CHANNELS=8
-D WIFI_DEBUG_LOGGING=1
-D WIFI_SSID='"myssid"'
-D WIFI_PWD='"mypwd"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${rpi_picow.build_src_filter}
+<helpers/wifi/*.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps = ${rpi_picow.lib_deps}
densaugeo/base64 @ ~1.4.0
lib_ignore = BLE

[env:PicoW_terminal_chat]
extends = rpi_picow
Expand Down