Complete Phase 12 dual-stack networking
Add IPv6-aware Wi-Fi state, HTTPS/SSH listeners, mDNS service reconciliation, and browser Wi-Fi administration. Include a guarded build-local fix for mDNS 1.12.0 membership handling, focused regression suites, and Phase 12 acceptance documentation.
This commit is contained in:
@@ -40,7 +40,7 @@ A complete example (values are illustrative, never defaults to install):
|
||||
{"index": 3, "enabled": false, "priority": 0, "security": "mixed", "ssid": "", "password_configured": false}
|
||||
]
|
||||
},
|
||||
"runtime": {"started": true, "state": "online", "active_profile": 0, "ip": "192.168.1.20", "ap_running": false, "ap_clients": 0, "last_error": 0},
|
||||
"runtime": {"started": true, "state": "online", "active_profile": 0, "ip": "192.168.1.20", "ipv6_linklocal": true, "ipv6_routable": true, "ap_running": false, "ap_clients": 0, "last_error": 0},
|
||||
"mdns": {"generation": 3, "suffix": "example", "hostname": "sak-example", "announced": true, "last_error": 0}
|
||||
}
|
||||
```
|
||||
@@ -51,6 +51,14 @@ Runtime states are canonical `stopped`, `starting`, `connecting`, `waiting-ip`,
|
||||
`.local`. The existing responder is STA-only. `announced` is the service's
|
||||
expected-announcement status, not a client-observed DNS verification.
|
||||
|
||||
`ip` is IPv4 only; `0.0.0.0` means absent, including when `state` is `online`.
|
||||
The required boolean `ipv6_linklocal` and `ipv6_routable` fields report preferred
|
||||
IPv6 address availability (link-local and ULA/GUA respectively). They do not
|
||||
assert a default route or Internet reachability. No actual IPv6 literal addresses
|
||||
are available in this snapshot; reporting is flags only. Settings, quick Network,
|
||||
and the main status summary label IPv4 absence separately from IPv6 availability.
|
||||
The OLED network header uses `LL` and `ULA/GUA` with `Y`/`N` (`?` if unavailable).
|
||||
|
||||
Wi-Fi working configuration and runtime are copied together under its mutex;
|
||||
mDNS is a separate consistent projection, not an atomic cross-domain snapshot.
|
||||
Both acquisitions use zero wait. Either unavailable/contended yields HTTP 503
|
||||
@@ -202,7 +210,7 @@ remain independent. No terminal lease/transport changes are made by this module.
|
||||
- 768-byte POST, at most four receives, at most 13 distinct flat keys, 64-byte
|
||||
parser value scratch; enough for one fully escaped 32-byte SSID and 63-byte
|
||||
replacement plus the typed fields. No heap JSON tree/cJSON.
|
||||
- 2,048-byte snapshot buffer. Maximum escaped fixture: 1,877 payload bytes
|
||||
- 2,048-byte snapshot buffer. Escaped fixture remains below the fixed limit with both IPv6 booleans
|
||||
(five 32-byte SSIDs at six bytes/byte, four profiles, full-width numbers,
|
||||
55-byte mDNS suffix plus hostname, longest booleans/state/security/policy).
|
||||
- 128-byte operation response buffer; one static operation and one small timer.
|
||||
|
||||
@@ -15,6 +15,7 @@ sys.dont_write_bytecode = True
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
HEADERS = {
|
||||
'sdkconfig.h': '#pragma once\n#define CONFIG_MDNS_PREDEF_NETIF_STA 1\n#define CONFIG_MDNS_PREDEF_NETIF_AP 0\n#define CONFIG_MDNS_PREDEF_NETIF_ETH 0\n#define CONFIG_LWIP_IPV6_NUM_ADDRESSES 3\n',
|
||||
'esp_err.h': '''#pragma once
|
||||
typedef int esp_err_t;
|
||||
#define ESP_OK 0
|
||||
@@ -34,6 +35,12 @@ typedef int esp_err_t;
|
||||
#include <stdint.h>
|
||||
#define pdTRUE 1
|
||||
#define portMAX_DELAY UINT32_MAX
|
||||
typedef int portMUX_TYPE;
|
||||
#define portMUX_INITIALIZER_UNLOCKED 0
|
||||
void fake_enter(portMUX_TYPE *);
|
||||
void fake_exit(portMUX_TYPE *);
|
||||
#define portENTER_CRITICAL(mux) fake_enter(mux)
|
||||
#define portEXIT_CRITICAL(mux) fake_exit(mux)
|
||||
''',
|
||||
'freertos/semphr.h': '''#pragma once
|
||||
#include <stdint.h>
|
||||
@@ -62,8 +69,33 @@ void nvs_close(nvs_handle_t);
|
||||
#define ESP_MAC_WIFI_SOFTAP 1
|
||||
esp_err_t esp_read_mac(uint8_t *,int);
|
||||
''',
|
||||
'mdns.h': '''#pragma once
|
||||
'esp_timer.h': '#pragma once\n#include <stdint.h>\nint64_t esp_timer_get_time(void);\n',
|
||||
'esp_netif.h': '''#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
typedef struct { int unused; } esp_netif_t;
|
||||
typedef struct { uint32_t addr; } esp_ip4_addr_t;
|
||||
typedef struct { uint32_t addr[4]; } esp_ip6_addr_t;
|
||||
typedef struct { esp_ip4_addr_t ip, netmask, gw; } esp_netif_ip_info_t;
|
||||
esp_netif_t *esp_netif_get_handle_from_ifkey(const char *);
|
||||
bool esp_netif_is_netif_up(esp_netif_t *);
|
||||
esp_err_t esp_netif_get_ip_info(esp_netif_t *,esp_netif_ip_info_t *);
|
||||
int esp_netif_get_all_ip6(esp_netif_t *,esp_ip6_addr_t *);
|
||||
esp_err_t esp_netif_tcpip_exec(esp_err_t (*)(void *),void *);
|
||||
''',
|
||||
'mdns.h': '''#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_netif.h"
|
||||
typedef enum {
|
||||
MDNS_EVENT_ENABLE_IP4 = 1 << 1, MDNS_EVENT_ENABLE_IP6 = 1 << 2,
|
||||
MDNS_EVENT_DISABLE_IP4 = 1 << 5, MDNS_EVENT_DISABLE_IP6 = 1 << 6
|
||||
} mdns_event_actions_t;
|
||||
esp_err_t mdns_netif_action(esp_netif_t *,mdns_event_actions_t);
|
||||
esp_err_t mdns_service_add(const char *,const char *,const char *,uint16_t,const void *,size_t);
|
||||
esp_err_t mdns_service_remove(const char *,const char *);
|
||||
esp_err_t mdns_init(void);
|
||||
esp_err_t mdns_hostname_set(const char *);
|
||||
esp_err_t mdns_instance_name_set(const char *);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "nvs.h"
|
||||
#include "mdns.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
static int wifi_mutex, mdns_mutex;
|
||||
static SemaphoreHandle_t s_mutex=&wifi_mutex;
|
||||
@@ -23,8 +25,15 @@ static void lock_shared(void) { assert(!wifi_mutex); wifi_mutex=1; }
|
||||
static void unlock_shared(void) { assert(wifi_mutex); wifi_mutex=0; }
|
||||
static int s_drop_mux, s_queue;
|
||||
static uint64_t s_queue_drops;
|
||||
#define portENTER_CRITICAL(mux) do { assert((mux)==&s_drop_mux && !s_drop_mux); s_drop_mux=1; } while (0)
|
||||
#define portEXIT_CRITICAL(mux) do { assert((mux)==&s_drop_mux && s_drop_mux); s_drop_mux=0; } while (0)
|
||||
static portMUX_TYPE *critical;
|
||||
void fake_enter(portMUX_TYPE *mux) {
|
||||
assert(!critical && !*mux);
|
||||
if(mux!=&s_drop_mux) assert(!wifi_mutex && !mdns_mutex);
|
||||
critical=mux; *mux=1;
|
||||
}
|
||||
void fake_exit(portMUX_TYPE *mux) {
|
||||
assert(critical==mux && *mux); *mux=0; critical=NULL;
|
||||
}
|
||||
static int xQueueSend(int queue,const manager_message_t *message,uint32_t wait) {
|
||||
assert(queue==s_queue && !wait && wifi_mutex && !s_drop_mux);
|
||||
if(queue_fail) return 0;
|
||||
@@ -32,6 +41,7 @@ static int xQueueSend(int queue,const manager_message_t *message,uint32_t wait)
|
||||
}
|
||||
SemaphoreHandle_t xSemaphoreCreateMutex(void) { return &mdns_mutex; }
|
||||
int xSemaphoreTake(SemaphoreHandle_t mutex,uint32_t wait) {
|
||||
assert(!critical);
|
||||
if(wait==0 && (snapshot_contention || *mutex)) return 0;
|
||||
assert(!*mutex); *mutex=1; return pdTRUE;
|
||||
}
|
||||
@@ -63,6 +73,39 @@ esp_err_t nvs_set_blob(nvs_handle_t handle,const char *key,const void *data,size
|
||||
}
|
||||
esp_err_t nvs_commit(nvs_handle_t handle) { (void)handle; ++commits; return commit_error; }
|
||||
void nvs_close(nvs_handle_t handle) { (void)handle; }
|
||||
/* Minimal synchronous netif boundary; queue-loss/repair faults live in mdns_phase12. */
|
||||
static esp_netif_t sta;
|
||||
static bool tcpip;
|
||||
static unsigned netif_families=3, requested_families;
|
||||
static int64_t clock_us;
|
||||
int64_t esp_timer_get_time(void) { assert(!mdns_mutex && !critical); return clock_us; }
|
||||
esp_netif_t *esp_netif_get_handle_from_ifkey(const char *key) {
|
||||
assert(!mdns_mutex && !critical && !strcmp(key,"WIFI_STA_DEF")); return &sta;
|
||||
}
|
||||
bool esp_netif_is_netif_up(esp_netif_t *netif) { assert(tcpip && netif==&sta); return true; }
|
||||
esp_err_t esp_netif_get_ip_info(esp_netif_t *netif,esp_netif_ip_info_t *ip) {
|
||||
assert(tcpip && netif==&sta); memset(ip,0,sizeof(*ip));
|
||||
ip->ip.addr=(netif_families&1) ? 1 : 0; return ESP_OK;
|
||||
}
|
||||
int esp_netif_get_all_ip6(esp_netif_t *netif,esp_ip6_addr_t *ip) {
|
||||
assert(tcpip && netif==&sta);
|
||||
if(!(netif_families&2)) return 0;
|
||||
memset(ip,0,sizeof(*ip)); ip->addr[0]=0xfe80; return 1;
|
||||
}
|
||||
esp_err_t esp_netif_tcpip_exec(esp_err_t (*callback)(void *),void *context) {
|
||||
assert(!mdns_mutex && !critical && !tcpip); tcpip=true;
|
||||
esp_err_t error=callback(context); tcpip=false; return error;
|
||||
}
|
||||
esp_err_t mdns_netif_action(esp_netif_t *netif,mdns_event_actions_t action) {
|
||||
assert(netif==&sta && !mdns_mutex && !critical && !tcpip);
|
||||
assert(action && !(action & ~(MDNS_EVENT_ENABLE_IP4 | MDNS_EVENT_ENABLE_IP6 |
|
||||
MDNS_EVENT_DISABLE_IP4 | MDNS_EVENT_DISABLE_IP6)));
|
||||
if(action&MDNS_EVENT_ENABLE_IP4) requested_families|=1;
|
||||
if(action&MDNS_EVENT_ENABLE_IP6) requested_families|=2;
|
||||
if(action&MDNS_EVENT_DISABLE_IP4) requested_families&=~1U;
|
||||
if(action&MDNS_EVENT_DISABLE_IP6) requested_families&=~2U;
|
||||
return ESP_OK;
|
||||
}
|
||||
static char announced_hostname[60];
|
||||
esp_err_t mdns_init(void) { return ESP_OK; }
|
||||
esp_err_t mdns_hostname_set(const char *hostname) {
|
||||
@@ -70,6 +113,13 @@ esp_err_t mdns_hostname_set(const char *hostname) {
|
||||
}
|
||||
esp_err_t mdns_instance_name_set(const char *name) { assert(name); return ESP_OK; }
|
||||
void mdns_free(void) {}
|
||||
esp_err_t mdns_service_add(const char *name,const char *type,const char *proto,uint16_t port,const void *txt,size_t count) {
|
||||
assert(!mdns_mutex && !name && type && !strcmp(proto,"_tcp") && (port==443 || port==22) && !txt && !count);
|
||||
return ESP_OK;
|
||||
}
|
||||
esp_err_t mdns_service_remove(const char *type,const char *proto) {
|
||||
assert(!mdns_mutex && type && !strcmp(proto,"_tcp")); return ESP_OK;
|
||||
}
|
||||
#include "manager_production.h"
|
||||
|
||||
static uint32_t generation(void) { return s_shared.snapshot.config_generation; }
|
||||
@@ -85,6 +135,20 @@ int main(void) {
|
||||
snapshot_contention=false;
|
||||
assert(wifi_manager_get_settings(&projection)==ESP_OK && projection.ap_password_configured);
|
||||
assert(!projection.profiles[0].password_configured && projection.runtime.active_profile==-1);
|
||||
for(unsigned flags=0;flags<8;++flags) {
|
||||
s_shared.snapshot.state=WIFI_MANAGER_STATE_ONLINE;
|
||||
s_shared.snapshot.ip=(flags&4) ? UINT32_C(0x080200c0) : 0;
|
||||
s_shared.snapshot.ipv6_linklocal=(flags&1)!=0;
|
||||
s_shared.snapshot.ipv6_routable=(flags&2)!=0;
|
||||
assert(wifi_manager_get_settings(&projection)==ESP_OK);
|
||||
assert(projection.runtime.state==WIFI_MANAGER_STATE_ONLINE);
|
||||
assert(projection.runtime.ip==s_shared.snapshot.ip);
|
||||
assert(projection.runtime.ipv6_linklocal==s_shared.snapshot.ipv6_linklocal);
|
||||
assert(projection.runtime.ipv6_routable==s_shared.snapshot.ipv6_routable);
|
||||
}
|
||||
s_shared.snapshot.state=WIFI_MANAGER_STATE_STOPPED; s_shared.snapshot.ip=0;
|
||||
s_shared.snapshot.ipv6_linklocal=false; s_shared.snapshot.ipv6_routable=false;
|
||||
puts("PASS real manager projection: IPv6-only/dual-stack preserve independent availability flags and zero IPv4 online");
|
||||
wifi_manager_patch_t p={.profile=0,.fields=WIFI_PATCH_SSID,.ssid_len=32};
|
||||
for(unsigned i=0;i<32;++i) p.ssid[i]=(uint8_t)(i*8);
|
||||
assert(patch(&p)==ESP_OK && queued==0 && !s_shared.config.profiles[0].enabled);
|
||||
@@ -188,12 +252,19 @@ int main(void) {
|
||||
assert(mdns_service_update_current(1,MDNS_SETTINGS_SET,&config,&stored)==ESP_OK);
|
||||
assert(mdns_service_update_current(1,MDNS_SETTINGS_SAVE,NULL,&stored)==ESP_ERR_NOT_FOUND);
|
||||
assert(mdns_service_start()==ESP_OK && !strcmp(announced_hostname,"sak-first"));
|
||||
assert(requested_families==3);
|
||||
netif_families=2;
|
||||
assert(mdns_service_reconcile()==ESP_OK && requested_families==2);
|
||||
netif_families=3; clock_us+=30000000;
|
||||
assert(mdns_service_reconcile()==ESP_OK && requested_families==3);
|
||||
mdns_service_stop();
|
||||
assert(requested_families==0);
|
||||
assert(mdns_service_get_settings(&m)==ESP_OK && !m.announced);
|
||||
memset(config.suffix,0,sizeof(config.suffix)); strcpy(config.suffix,"offline"); config.suffix_len=7;
|
||||
assert(mdns_service_update_current(m.config_generation,MDNS_SETTINGS_SET,&config,&stored)==ESP_OK);
|
||||
unsigned calls=hostname_calls;
|
||||
assert(mdns_service_start()==ESP_OK && hostname_calls==calls+1 && !strcmp(announced_hostname,"sak-offline"));
|
||||
assert(requested_families==3 && !critical && !tcpip);
|
||||
assert(mdns_service_get_settings(&m)==ESP_OK);
|
||||
assert(mdns_service_update_current(m.config_generation,MDNS_SETTINGS_SAVE,NULL,&stored)==ESP_OK);
|
||||
strcpy(config.suffix,"another"); assert(mdns_service_set_config(&config)==ESP_OK);
|
||||
|
||||
Reference in New Issue
Block a user