diff --git a/components/aes67_net/aes67_net.c b/components/aes67_net/aes67_net.c index ba95825..695a7ae 100644 --- a/components/aes67_net/aes67_net.c +++ b/components/aes67_net/aes67_net.c @@ -11,10 +11,13 @@ #include "esp_log.h" #include "esp_netif.h" #include "lldp.h" +#include "lwip/inet.h" #include "mdns.h" static const char *TAG = "net"; +static const char *const ADDR_KEYS[] = { "dhcp", "ip", "mask", "gw", "dns" }; + // AES67 / untagged interface. static const char NET_DEFAULTS[] = "{\"hostname\":\"aes67\",\"vlan\":false,\"web_on\":\"both\",\"dhcp\":true," @@ -25,6 +28,48 @@ static const char INET_DEFAULTS[] = static esp_netif_t *s_netif; static char s_link[24] = "down"; +static cJSON *s_addr; // addressing in use since boot (ADDR_KEYS of "net") + +// The keys that define the IP setup. With DHCP the static fields don't matter (the UI fills them +// with the lease), so they don't count as a change. +static cJSON *addressing(const cJSON *g) +{ + cJSON *a = cJSON_CreateObject(); + int n = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(g, "dhcp")) ? 1 : sizeof(ADDR_KEYS) / sizeof(ADDR_KEYS[0]); + for (int i = 0; i < n; i++) { + cJSON_AddItemToObject(a, ADDR_KEYS[i], cJSON_Duplicate(cJSON_GetObjectItemCaseSensitive(g, ADDR_KEYS[i]), true)); + } + return a; +} + +static uint32_t addr_of(const cJSON *g, const char *key) +{ + const cJSON *v = cJSON_GetObjectItemCaseSensitive(g, key); + return cJSON_IsString(v) && v->valuestring[0] ? inet_addr(v->valuestring) : 0; +} + +// Static addressing: stop the DHCP client and set ours. On link up, like IDF's static IP example. +static void set_static(void) +{ + if (cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(s_addr, "dhcp"))) { + return; + } + esp_err_t err = esp_netif_dhcpc_stop(s_netif); + if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { + ESP_LOGE(TAG, "DHCP client stop: %s", esp_err_to_name(err)); + } + esp_netif_ip_info_t ip = { + .ip.addr = addr_of(s_addr, "ip"), .netmask.addr = addr_of(s_addr, "mask"), .gw.addr = addr_of(s_addr, "gw"), + }; + err = esp_netif_set_ip_info(s_netif, &ip); + if (err != ESP_OK) { + ESP_LOGE(TAG, "static IP: %s", esp_err_to_name(err)); + } + esp_netif_dns_info_t dns = { .ip.type = ESP_IPADDR_TYPE_V4, .ip.u_addr.ip4.addr = addr_of(s_addr, "dns") }; + if (dns.ip.u_addr.ip4.addr) { + esp_netif_set_dns_info(s_netif, ESP_NETIF_DNS_MAIN, &dns); + } +} static void eth_event(void *arg, esp_event_base_t base, int32_t id, void *data) { @@ -43,6 +88,7 @@ static void eth_event(void *arg, esp_event_base_t base, int32_t id, void *data) mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); snprintf(s_link, sizeof(s_link), "up, %s Mbps %s", speed == ETH_SPEED_100M ? "100" : "10", duplex == ETH_DUPLEX_FULL ? "full" : "half"); + set_static(); break; } case ETHERNET_EVENT_DISCONNECTED: @@ -66,10 +112,27 @@ static void ip_event(void *arg, esp_event_base_t base, int32_t id, void *data) static bool check_addr(const cJSON *g, char *err, size_t n) { bool dhcp = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(g, "dhcp")); - return cfg_check_ipv4(g, "ip", dhcp, err, n) && - cfg_check_ipv4(g, "mask", dhcp, err, n) && - cfg_check_ipv4(g, "gw", true, err, n) && - cfg_check_ipv4(g, "dns", true, err, n); + if (!cfg_check_ipv4(g, "ip", dhcp, err, n) || !cfg_check_ipv4(g, "mask", dhcp, err, n) || + !cfg_check_ipv4(g, "gw", true, err, n) || !cfg_check_ipv4(g, "dns", true, err, n)) { + return false; + } + if (dhcp) { + return true; + } + uint32_t ip = ntohl(addr_of(g, "ip")), mask = ntohl(addr_of(g, "mask")), gw = ntohl(addr_of(g, "gw")); + if (mask == 0 || (~mask & (~mask + 1)) != 0 || mask == 0xffffffff) { + snprintf(err, n, "mask: not a valid netmask"); + return false; + } + if ((ip & ~mask) == 0 || (ip & ~mask) == ~mask) { + snprintf(err, n, "ip: network or broadcast address of the subnet"); + return false; + } + if (gw && ((gw & mask) != (ip & mask) || gw == ip)) { + snprintf(err, n, "gw: must be another address in the IP's subnet"); + return false; + } + return true; } // RFC 1123 label: letters, digits, hyphen; not starting or ending with a hyphen. @@ -134,6 +197,13 @@ static void net_apply(const cJSON *g) ESP_LOGI(TAG, "mDNS: now %s.local", host); } lldp_changed(); + // New addressing: PTP/TX/SAP sockets are bound to the address, so it takes a reboot. + cJSON *a = addressing(g); + if (!cJSON_Compare(a, s_addr, true)) { + ESP_LOGW(TAG, "IP setup changed: rebooting to apply it"); + web_reboot_later(1500); + } + cJSON_Delete(a); } static void net_status(cJSON *st) @@ -169,7 +239,7 @@ static void net_status(cJSON *st) esp_err_t aes67_net_init(esp_eth_handle_t eth) { - // Applied so far: hostname (DHCP, mDNS, LLDP). Static IP and VLAN come later. + // Applied: hostname (DHCP, mDNS, LLDP) live; DHCP/static addressing at boot. VLAN comes later. ESP_ERROR_CHECK(cfg_register("net", NET_DEFAULTS, net_validate, net_apply)); ESP_ERROR_CHECK(cfg_register("inet", INET_DEFAULTS, inet_validate, NULL)); @@ -189,6 +259,13 @@ esp_err_t aes67_net_init(esp_eth_handle_t eth) // Hostname in DHCP requests (option 12), so the router's lease list shows it. cJSON *net = cfg_get("net"); ESP_ERROR_CHECK(esp_netif_set_hostname(s_netif, cJSON_GetObjectItemCaseSensitive(net, "hostname")->valuestring)); + s_addr = addressing(net); + if (!cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(net, "dhcp"))) { + ESP_LOGI(TAG, "static IP %s/%s gw %s dns %s", cJSON_GetObjectItemCaseSensitive(net, "ip")->valuestring, + cJSON_GetObjectItemCaseSensitive(net, "mask")->valuestring, + cJSON_GetObjectItemCaseSensitive(net, "gw")->valuestring, + cJSON_GetObjectItemCaseSensitive(net, "dns")->valuestring); + } cJSON_Delete(net); mdns_setup(); ESP_ERROR_CHECK(lldp_start(eth, s_netif)); diff --git a/web/index.html b/web/index.html index 25546ad..64017ff 100644 --- a/web/index.html +++ b/web/index.html @@ -417,7 +417,15 @@ async function save(e) { const c = read(); c.aes67.session_ver = (Number(cfg.aes67?.session_ver) || 0) + 1; // SDP version bump per RFC 4566 await api('POST', '/api/config', c); + // A changed IP setup reboots the device: follow it to the new address. + const addr = n => n.dhcp ? 'dhcp' : [n.ip, n.mask, n.gw, n.dns].join('/'); + const moved = addr(cfg.net || {}) !== addr(c.net); cfg = c; fill(); msg.textContent = 'Saved.'; + if (moved) { + const to = c.net.dhcp ? location.host : c.net.ip; + msg.textContent = `Rebooting with the new IP setup. Opening http://${to}/ in 20 s.`; + setTimeout(() => location.href = `http://${to}/`, 20000); + } } // PTP display follows the Riedel (Bolero/Artist) status layout, IEEE 1588-2019 terms.