feat: Add Ethernet runtime configuration CLI

- Add ip/gw/subnet/dns get/set commands with SPIFFS persistence
- Implement reconfigureEthernet() method for runtime config
- Fix TCP_CONSOLE_PORT conditional compilation
- Update platformio.ini for consistency (USE_ETHERNET)
- Document Ethernet settings and get bridge.type command
- Fixes #2196 #2197 hardware and console dependencies
This commit is contained in:
Piero Andreini 2026-04-05 12:07:11 +02:00
parent ea72dd0f97
commit 4542e6c86a
12 changed files with 295 additions and 16 deletions

View file

@ -66,6 +66,7 @@ public:
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
virtual uint8_t getShutdownReason() const { return 0; }
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
virtual void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet) { /* no op */ }
};
/**

View file

@ -26,6 +26,38 @@ static bool isValidName(const char *n) {
return true;
}
// Helper functions for IP address conversion
static uint32_t ipStringToUint32(const char* ip_str) {
uint32_t ip = 0;
uint8_t parts[4] = {0, 0, 0, 0};
sscanf(ip_str, "%hhu.%hhu.%hhu.%hhu", &parts[0], &parts[1], &parts[2], &parts[3]);
ip = ((uint32_t)parts[0] << 24) | ((uint32_t)parts[1] << 16) | ((uint32_t)parts[2] << 8) | parts[3];
return ip;
}
static void uint32ToIPString(uint32_t ip, char* buffer, size_t size) {
uint8_t b1 = (ip >> 24) & 0xFF;
uint8_t b2 = (ip >> 16) & 0xFF;
uint8_t b3 = (ip >> 8) & 0xFF;
uint8_t b4 = ip & 0xFF;
snprintf(buffer, size, "%d.%d.%d.%d", b1, b2, b3, b4);
}
// Reject 0.x.x.x and multicast/reserved (>= 224.x.x.x). 0 is valid (means DHCP/clear).
// Guards against stale bytes from older prefs-file layouts ending up at the netif.
static bool isValidUnicastIp(uint32_t ip) {
if (ip == 0) return true;
uint8_t first = (ip >> 24) & 0xFF;
return (first >= 1 && first <= 223);
}
// Valid subnet mask = contiguous 1s followed by contiguous 0s.
static bool isValidSubnetMask(uint32_t mask) {
if (mask == 0) return true;
uint32_t inv = ~mask;
return (inv & (inv + 1)) == 0;
}
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (fs->exists("/com_prefs")) {
loadPrefsInt(fs, "/com_prefs"); // new filename
@ -87,8 +119,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
// next: 291
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
file.read((uint8_t *)&_prefs->eth_ip, sizeof(_prefs->eth_ip)); // 291
file.read((uint8_t *)&_prefs->eth_gateway, sizeof(_prefs->eth_gateway)); // 295
file.read((uint8_t *)&_prefs->eth_subnet, sizeof(_prefs->eth_subnet)); // 299
file.read((uint8_t *)&_prefs->eth_dns1, sizeof(_prefs->eth_dns1)); // 303
file.read((uint8_t *)&_prefs->eth_dns2, sizeof(_prefs->eth_dns2)); // 307
// 311
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@ -119,6 +156,15 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// sanitise settings
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
// Sanitise eth_* fields: an older firmware version may have written different
// data at these offsets. Drop anything that isn't a plausible unicast IP / mask
// so we don't push garbage into esp_netif_set_ip_info() at boot.
if (!isValidUnicastIp(_prefs->eth_ip)) _prefs->eth_ip = 0;
if (!isValidUnicastIp(_prefs->eth_gateway)) _prefs->eth_gateway = 0;
if (!isValidUnicastIp(_prefs->eth_dns1)) _prefs->eth_dns1 = 0;
if (!isValidUnicastIp(_prefs->eth_dns2)) _prefs->eth_dns2 = 0;
if (!isValidSubnetMask(_prefs->eth_subnet)) _prefs->eth_subnet = 0;
file.close();
}
}
@ -178,9 +224,13 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
// next: 291
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
file.write((uint8_t *)&_prefs->eth_ip, sizeof(_prefs->eth_ip)); // 291
file.write((uint8_t *)&_prefs->eth_gateway, sizeof(_prefs->eth_gateway)); // 295
file.write((uint8_t *)&_prefs->eth_subnet, sizeof(_prefs->eth_subnet)); // 299
file.write((uint8_t *)&_prefs->eth_dns1, sizeof(_prefs->eth_dns1)); // 303
file.write((uint8_t *)&_prefs->eth_dns2, sizeof(_prefs->eth_dns2)); // 307
// 311
file.close();
}
}
@ -725,6 +775,44 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported by this board");
};
#ifdef USE_ETHERNET
} else if (memcmp(config, "ip ", 3) == 0) {
uint32_t ip = ipStringToUint32(&config[3]);
if (ip == UINT32_MAX || (ip != 0 && !isValidUnicastIp(ip))) {
strcpy(reply, "Error: invalid IP");
} else {
_prefs->eth_ip = ip;
savePrefs();
strcpy(reply, ip == 0 ? "OK - reboot to apply (will use DHCP)" : "OK - reboot to apply");
}
} else if (memcmp(config, "subnet ", 7) == 0) {
uint32_t subnet = ipStringToUint32(&config[7]);
if (subnet == UINT32_MAX || !isValidSubnetMask(subnet)) {
strcpy(reply, "Error: invalid subnet mask");
} else {
_prefs->eth_subnet = subnet;
savePrefs();
strcpy(reply, "OK - reboot to apply");
}
} else if (memcmp(config, "gw ", 3) == 0) {
uint32_t gw = ipStringToUint32(&config[3]);
if (gw == UINT32_MAX || (gw != 0 && !isValidUnicastIp(gw))) {
strcpy(reply, "Error: invalid IP");
} else {
_prefs->eth_gateway = gw;
savePrefs();
strcpy(reply, "OK - reboot to apply");
}
} else if (memcmp(config, "dns ", 4) == 0) {
uint32_t dns = ipStringToUint32(&config[4]);
if (dns == UINT32_MAX || (dns != 0 && !isValidUnicastIp(dns))) {
strcpy(reply, "Error: invalid IP");
} else {
_prefs->eth_dns1 = dns;
savePrefs();
strcpy(reply, "OK - reboot to apply");
}
#endif
} else {
sprintf(reply, "unknown config: %s", config);
}
@ -840,6 +928,24 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
} else if (memcmp(config, "bridge.secret", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_secret);
#endif
#ifdef USE_ETHERNET
} else if (memcmp(config, "ip", 2) == 0) {
char ip_str[16];
uint32ToIPString(_prefs->eth_ip, ip_str, sizeof(ip_str));
sprintf(reply, "> %s", ip_str);
} else if (memcmp(config, "subnet", 6) == 0) {
char subnet_str[16];
uint32ToIPString(_prefs->eth_subnet, subnet_str, sizeof(subnet_str));
sprintf(reply, "> %s", subnet_str);
} else if (memcmp(config, "gw", 2) == 0) {
char gw_str[16];
uint32ToIPString(_prefs->eth_gateway, gw_str, sizeof(gw_str));
sprintf(reply, "> %s", gw_str);
} else if (memcmp(config, "dns", 3) == 0) {
char dns_str[16];
uint32ToIPString(_prefs->eth_dns1, dns_str, sizeof(dns_str));
sprintf(reply, "> %s", dns_str);
#endif
} else if (memcmp(config, "bootloader.ver", 14) == 0) {
#ifdef NRF52_PLATFORM

View file

@ -61,6 +61,12 @@ struct NodePrefs { // persisted to file
uint8_t rx_boosted_gain; // power settings
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
// Ethernet settings
uint32_t eth_ip;
uint32_t eth_gateway;
uint32_t eth_subnet;
uint32_t eth_dns1;
uint32_t eth_dns2;
};
class CommonCLICallbacks {

View file

@ -130,12 +130,15 @@ void TEthEliteBoard::startEthernet() {
ETH.begin(ETH_PHY_W5500, ETH_ADDR, ETH_CS, ETH_INT, -1, SPI2_HOST, ETH_SCLK, ETH_MISO, ETH_MOSI);
delay(100);
#ifdef ETH_STATIC_IP
#ifndef USE_ETHERNET
// Used only if USE_ETHERNET is not enabled
#ifdef ETH_STATIC_IP
IPAddress ip(ETH_STATIC_IP);
IPAddress gw(ETH_GATEWAY);
IPAddress mask(ETH_SUBNET);
IPAddress dns(ETH_DNS);
ETH.config(ip, gw, mask, dns);
#endif
#endif
unsigned long t0 = millis();
@ -161,5 +164,30 @@ void TEthEliteBoard::startEthernet() {
Serial.print("ETH IP "); Serial.println(ETH.localIP());
Serial.println(ETH.linkUp() ? "ETH LINK UP" : "ETH LINK DOWN");
}
void TEthEliteBoard::reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet) {
if (ip != 0) {
uint8_t b1 = (ip >> 24) & 0xFF;
uint8_t b2 = (ip >> 16) & 0xFF;
uint8_t b3 = (ip >> 8) & 0xFF;
uint8_t b4 = ip & 0xFF;
uint8_t gw1 = (gw >> 24) & 0xFF;
uint8_t gw2 = (gw >> 16) & 0xFF;
uint8_t gw3 = (gw >> 8) & 0xFF;
uint8_t gw4 = gw & 0xFF;
uint8_t sub1 = (subnet >> 24) & 0xFF;
uint8_t sub2 = (subnet >> 16) & 0xFF;
uint8_t sub3 = (subnet >> 8) & 0xFF;
uint8_t sub4 = subnet & 0xFF;
ETH.config(
IPAddress(b1, b2, b3, b4),
IPAddress(gw1, gw2, gw3, gw4),
IPAddress(sub1, sub2, sub3, sub4)
);
Serial.printf("ETH reconfigured to %d.%d.%d.%d\n", b1, b2, b3, b4);
eth_local_ip = ETH.localIP().toString(); // Aggiorna IP locale per OTA
}
}
#endif

View file

@ -73,7 +73,8 @@ public:
void startNetwork();
void startEthernet();
void startWifi();
void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet);
void enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);

View file

@ -73,6 +73,7 @@ public:
void startNetwork();
void startEthernet();
void startWifi();
void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet);
void enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);