mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-08-29 10:48:19 +00:00
Merge pull request #25 from agessaman/feat/flex-ipv6
feat(mqtt): add dual-stack IPv4/IPv6 support to MQTT bridge
This commit is contained in:
@@ -249,6 +249,26 @@ The MQTT bridge comes with the following defaults for fresh installs (unless ove
|
|||||||
- **Timezone Offset**: 0 (fallback, no offset, unless `MQTT_DEFAULT_TIMEZONE_OFFSET` is set)
|
- **Timezone Offset**: 0 (fallback, no offset, unless `MQTT_DEFAULT_TIMEZONE_OFFSET` is set)
|
||||||
- **Repeat (forwarding)**: On (set `repeat off` for receive-only observers)
|
- **Repeat (forwarding)**: On (set `repeat off` for receive-only observers)
|
||||||
|
|
||||||
|
## IPv6 Support
|
||||||
|
|
||||||
|
Observer builds run **dual-stack**: IPv4 continues to work exactly as before, and the node
|
||||||
|
*additionally* acquires an IPv6 address when the network supports it. This is fully automatic
|
||||||
|
and requires no configuration.
|
||||||
|
|
||||||
|
- **How it works**: once WiFi has an IPv4 address, the node enables IPv6 and obtains a global
|
||||||
|
address via Router Advertisement / SLAAC. A dual-stack router with RA/SLAAC is required;
|
||||||
|
on IPv4-only networks the node simply stays IPv4-only (graceful degradation).
|
||||||
|
- **Visibility**: the global IPv6 address appears in `get wifi.status` once assigned, e.g.
|
||||||
|
`> connected, IP: 192.168.1.42, IPv6: 2001:db8::abcd, RSSI: -62 dBm, uptime: ...`.
|
||||||
|
The field is omitted when no global address is present (link-local is not reported). IPv6 is
|
||||||
|
CLI/serial-visible only — it is not shown on the OLED.
|
||||||
|
- **Custom broker over IPv6**: use a bracketed literal in a full URI
|
||||||
|
(`set mqttN.server mqtts://[2001:db8::1]:8883`), or a bare literal
|
||||||
|
(`set mqttN.server 2001:db8::1`) which is bracketed automatically. Hostname presets need no
|
||||||
|
changes — DNS resolves AAAA records automatically once the stack is dual-stack.
|
||||||
|
- **Cost**: none to budget. IPv6 is already compiled into the ESP32 Arduino lwIP that every
|
||||||
|
build links; enabling it at runtime only adds a couple of address slots.
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
### MQTT Slot Commands
|
### MQTT Slot Commands
|
||||||
|
|||||||
@@ -780,7 +780,26 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
|
|||||||
default: status_str = "unknown"; break;
|
default: status_str = "unknown"; break;
|
||||||
}
|
}
|
||||||
if (status == WL_CONNECTED) {
|
if (status == WL_CONNECTED) {
|
||||||
sprintf(reply, "> %s, IP: %s, RSSI: %d dBm", status_str, WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
// reply points at the caller's char[160] command buffer (see main.cpp).
|
||||||
|
const size_t kReplyBufSize = 160;
|
||||||
|
sprintf(reply, "> %s, IP: %s", status_str, WiFi.localIP().toString().c_str());
|
||||||
|
#ifdef WITH_MQTT_BRIDGE
|
||||||
|
// Group IPv6 directly after IPv4 when a global/ULA address is assigned.
|
||||||
|
char v6[46];
|
||||||
|
if (MQTTBridge::getGlobalIPv6(v6, sizeof(v6))) {
|
||||||
|
size_t v6_len = strlen(reply);
|
||||||
|
if (v6_len < kReplyBufSize) {
|
||||||
|
snprintf(reply + v6_len, kReplyBufSize - v6_len, ", IPv6: %s", v6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// RSSI right after the IP addresses.
|
||||||
|
{
|
||||||
|
size_t rssi_len = strlen(reply);
|
||||||
|
if (rssi_len < kReplyBufSize) {
|
||||||
|
snprintf(reply + rssi_len, kReplyBufSize - rssi_len, ", RSSI: %d dBm", WiFi.RSSI());
|
||||||
|
}
|
||||||
|
}
|
||||||
#ifdef WITH_MQTT_BRIDGE
|
#ifdef WITH_MQTT_BRIDGE
|
||||||
unsigned long connect_at = MQTTBridge::getWifiConnectedAtMillis();
|
unsigned long connect_at = MQTTBridge::getWifiConnectedAtMillis();
|
||||||
if (connect_at != 0) {
|
if (connect_at != 0) {
|
||||||
@@ -791,7 +810,7 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
|
|||||||
unsigned long m = (uptime_sec % 3600) / 60;
|
unsigned long m = (uptime_sec % 3600) / 60;
|
||||||
unsigned long s = uptime_sec % 60;
|
unsigned long s = uptime_sec % 60;
|
||||||
size_t len = strlen(reply);
|
size_t len = strlen(reply);
|
||||||
const size_t reply_remaining = 128;
|
const size_t reply_remaining = (len < kReplyBufSize) ? (kReplyBufSize - len) : 0;
|
||||||
if (d > 0) {
|
if (d > 0) {
|
||||||
snprintf(reply + len, reply_remaining, ", uptime: %lud %luh %lum %lus", d, h, m, s);
|
snprintf(reply + len, reply_remaining, ", uptime: %lud %luh %lum %lus", d, h, m, s);
|
||||||
} else if (h > 0) {
|
} else if (h > 0) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
#include <esp_wifi.h>
|
#include <esp_wifi.h>
|
||||||
|
#include <esp_netif.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
@@ -179,6 +180,10 @@ static unsigned long s_wifi_connected_at = 0;
|
|||||||
static uint8_t s_wifi_disconnect_reason = 0;
|
static uint8_t s_wifi_disconnect_reason = 0;
|
||||||
static unsigned long s_wifi_disconnect_time = 0;
|
static unsigned long s_wifi_disconnect_time = 0;
|
||||||
|
|
||||||
|
// Most recent global/unique-local IPv6 address (SLAAC), as a string; empty when none.
|
||||||
|
// Populated from the ARDUINO_EVENT_WIFI_STA_GOT_IP6 handler; cleared on WiFi disconnect.
|
||||||
|
static char s_global_ipv6[46] = "";
|
||||||
|
|
||||||
#ifdef MQTT_MEMORY_DEBUG
|
#ifdef MQTT_MEMORY_DEBUG
|
||||||
// #region agent log
|
// #region agent log
|
||||||
static void agentLogHeap(const char* location, const char* message, const char* hypothesisId,
|
static void agentLogHeap(const char* location, const char* message, const char* hypothesisId,
|
||||||
@@ -201,6 +206,17 @@ unsigned long MQTTBridge::getWifiConnectedAtMillis() {
|
|||||||
return s_wifi_connected_at;
|
return s_wifi_connected_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MQTTBridge::getGlobalIPv6(char* buf, size_t len) {
|
||||||
|
if (buf == nullptr || len == 0) return false;
|
||||||
|
if (s_global_ipv6[0] == '\0') {
|
||||||
|
buf[0] = '\0';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
strncpy(buf, s_global_ipv6, len - 1);
|
||||||
|
buf[len - 1] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPrefs* obs) {
|
void MQTTBridge::formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPrefs* obs) {
|
||||||
if (buf == nullptr || bufsize == 0) return;
|
if (buf == nullptr || bufsize == 0) return;
|
||||||
const char* msgs = (obs && obs->mqtt_status_enabled) ? "on" : "off";
|
const char* msgs = (obs && obs->mqtt_status_enabled) ? "on" : "off";
|
||||||
@@ -829,11 +845,23 @@ void MQTTBridge::initializeWiFiInTask() {
|
|||||||
switch(event) {
|
switch(event) {
|
||||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||||
MQTT_DEBUG_PRINTLN("WiFi connected: %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
|
MQTT_DEBUG_PRINTLN("WiFi connected: %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
|
||||||
|
// Kick off IPv6 link-local + RA/SLAAC (additive; IPv4 path unchanged). Idempotent.
|
||||||
|
WiFi.enableIpV6();
|
||||||
// Set flag to trigger NTP sync from loop() instead of doing it here
|
// Set flag to trigger NTP sync from loop() instead of doing it here
|
||||||
if (!_ntp_synced && !_ntp_sync_pending) {
|
if (!_ntp_synced && !_ntp_sync_pending) {
|
||||||
_ntp_sync_pending = true;
|
_ntp_sync_pending = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case ARDUINO_EVENT_WIFI_STA_GOT_IP6: {
|
||||||
|
// Store only global/unique-local addresses; link-local isn't useful for diagnostics.
|
||||||
|
esp_ip6_addr_t ip6 = info.got_ip6.ip6_info.ip;
|
||||||
|
esp_ip6_addr_type_t type = esp_netif_ip6_get_addr_type(&ip6);
|
||||||
|
if (type == ESP_IP6_ADDR_IS_GLOBAL || type == ESP_IP6_ADDR_IS_UNIQUE_LOCAL) {
|
||||||
|
snprintf(s_global_ipv6, sizeof(s_global_ipv6), IPV6STR, IPV62STR(ip6));
|
||||||
|
MQTT_DEBUG_PRINTLN("WiFi IPv6: %s", s_global_ipv6);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||||
s_wifi_disconnect_reason = info.wifi_sta_disconnected.reason;
|
s_wifi_disconnect_reason = info.wifi_sta_disconnected.reason;
|
||||||
s_wifi_disconnect_time = millis();
|
s_wifi_disconnect_time = millis();
|
||||||
@@ -1325,7 +1353,16 @@ void MQTTBridge::setupSlot(int index) {
|
|||||||
} else if (slot.port == 443) {
|
} else if (slot.port == 443) {
|
||||||
proto = "wss";
|
proto = "wss";
|
||||||
}
|
}
|
||||||
snprintf(slot.broker_uri, sizeof(slot.broker_uri), "%s://%s:%d", proto, slot.host, slot.port);
|
// Bare IPv6 literals (contain ':' but no '.', not already bracketed) must be wrapped
|
||||||
|
// in [..] so the ":port" suffix isn't mistaken for part of the address.
|
||||||
|
bool bare_ipv6 = (slot.host[0] != '[') &&
|
||||||
|
(strchr(slot.host, ':') != nullptr) &&
|
||||||
|
(strchr(slot.host, '.') == nullptr);
|
||||||
|
if (bare_ipv6) {
|
||||||
|
snprintf(slot.broker_uri, sizeof(slot.broker_uri), "%s://[%s]:%d", proto, slot.host, slot.port);
|
||||||
|
} else {
|
||||||
|
snprintf(slot.broker_uri, sizeof(slot.broker_uri), "%s://%s:%d", proto, slot.host, slot.port);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
slot.client->setServer(slot.broker_uri);
|
slot.client->setServer(slot.broker_uri);
|
||||||
MQTT_DEBUG_PRINTLN("MQTT%d custom broker URI: %s (host='%s', port=%u)",
|
MQTT_DEBUG_PRINTLN("MQTT%d custom broker URI: %s (host='%s', port=%u)",
|
||||||
@@ -2080,6 +2117,9 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
|||||||
_wifi_disconnected_time = 0;
|
_wifi_disconnected_time = 0;
|
||||||
s_wifi_connected_at = now;
|
s_wifi_connected_at = now;
|
||||||
_wifi_reconnect_backoff_attempt = 0;
|
_wifi_reconnect_backoff_attempt = 0;
|
||||||
|
// Re-arm IPv6 link-local after a reconnect (covers paths that re-run WiFi.begin
|
||||||
|
// and skip the GOT_IP event ordering). Idempotent.
|
||||||
|
WiFi.enableIpV6();
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
wifi_ps_type_t ps_mode;
|
wifi_ps_type_t ps_mode;
|
||||||
uint8_t ps_pref = _obs->wifi_power_save;
|
uint8_t ps_pref = _obs->wifi_power_save;
|
||||||
@@ -2106,6 +2146,7 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
|||||||
if (_last_wifi_status == WL_CONNECTED) {
|
if (_last_wifi_status == WL_CONNECTED) {
|
||||||
_wifi_disconnected_time = now;
|
_wifi_disconnected_time = now;
|
||||||
s_wifi_connected_at = 0;
|
s_wifi_connected_at = 0;
|
||||||
|
s_global_ipv6[0] = '\0'; // drop stale IPv6 so get wifi.status doesn't report it
|
||||||
// Disconnect all slot clients when WiFi drops
|
// Disconnect all slot clients when WiFi drops
|
||||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||||
if (_slots[i].client && _slots[i].connected) {
|
if (_slots[i].client && _slots[i].connected) {
|
||||||
|
|||||||
@@ -430,6 +430,10 @@ public:
|
|||||||
|
|
||||||
static unsigned long getWifiConnectedAtMillis();
|
static unsigned long getWifiConnectedAtMillis();
|
||||||
|
|
||||||
|
// Copies the current global/unique-local IPv6 address (string) into buf.
|
||||||
|
// Returns false (and writes an empty string) when no global IPv6 is assigned.
|
||||||
|
static bool getGlobalIPv6(char* buf, size_t len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-slot outage accessors used by AlertReporter to detect prolonged
|
* Per-slot outage accessors used by AlertReporter to detect prolonged
|
||||||
* MQTT broker outages. Indices are 0..RUNTIME_MQTT_SLOTS-1.
|
* MQTT broker outages. Indices are 0..RUNTIME_MQTT_SLOTS-1.
|
||||||
|
|||||||
Reference in New Issue
Block a user