Files
aes67-ESP32-P4/components/aes67_net/aes67_net.c
T
bsncubed c4468c4af0 Network: status reports mask/gw/dns; UI shows the DHCP lease in the static fields
With DHCP on, the greyed-out IP/netmask/gateway/DNS fields show the
addresses in use. Unticking DHCP keeps them as the starting point for a
static setup. (Static addressing itself is not applied yet.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:42:35 +10:00

203 lines
7.2 KiB
C

#include "aes67_net.h"
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include "aes67_cfg.h"
#include "aes67_web.h"
#include "esp_eth.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "lldp.h"
#include "mdns.h"
static const char *TAG = "net";
// AES67 / untagged interface.
static const char NET_DEFAULTS[] =
"{\"hostname\":\"aes67\",\"vlan\":false,\"web_on\":\"both\",\"dhcp\":true,"
"\"ip\":\"\",\"mask\":\"\",\"gw\":\"\",\"dns\":\"\"}";
// Internet / tagged interface, only used with net.vlan.
static const char INET_DEFAULTS[] =
"{\"vlan_id\":10,\"pcp\":0,\"dhcp\":true,\"ip\":\"\",\"mask\":\"\",\"gw\":\"\",\"dns\":\"\"}";
static esp_netif_t *s_netif;
static char s_link[24] = "down";
static void eth_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
esp_eth_handle_t eth = *(esp_eth_handle_t *)data;
switch (id) {
case ETHERNET_EVENT_CONNECTED: {
uint8_t mac[6];
eth_speed_t speed;
eth_duplex_t duplex;
esp_eth_ioctl(eth, ETH_CMD_G_MAC_ADDR, mac);
esp_eth_ioctl(eth, ETH_CMD_G_SPEED, &speed);
esp_eth_ioctl(eth, ETH_CMD_G_DUPLEX_MODE, &duplex);
ESP_LOGI(TAG, "link up: %s Mbps %s duplex, MAC %02x:%02x:%02x:%02x:%02x:%02x",
speed == ETH_SPEED_100M ? "100" : "10", duplex == ETH_DUPLEX_FULL ? "full" : "half",
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");
break;
}
case ETHERNET_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "link down");
snprintf(s_link, sizeof(s_link), "down");
break;
default:
break;
}
}
static void ip_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
const ip_event_got_ip_t *ev = data;
ESP_LOGI(TAG, "got IP " IPSTR " mask " IPSTR " gw " IPSTR,
IP2STR(&ev->ip_info.ip), IP2STR(&ev->ip_info.netmask), IP2STR(&ev->ip_info.gw));
lldp_changed();
}
// Static addressing needs ip + mask; gw/dns are optional (none on the AES67 side with VLAN split).
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);
}
// RFC 1123 label: letters, digits, hyphen; not starting or ending with a hyphen.
static bool check_hostname(const cJSON *g, char *err, size_t n)
{
if (!cfg_check_str(g, "hostname", 1, 63, err, n)) {
return false;
}
const char *h = cJSON_GetObjectItemCaseSensitive(g, "hostname")->valuestring;
size_t len = strlen(h);
for (size_t i = 0; i < len; i++) {
if (!isalnum((unsigned char)h[i]) && h[i] != '-') {
snprintf(err, n, "hostname: only letters, digits and '-'");
return false;
}
}
if (h[0] == '-' || h[len - 1] == '-') {
snprintf(err, n, "hostname: must not start or end with '-'");
return false;
}
return true;
}
static bool net_validate(const cJSON *g, char *err, size_t n)
{
static const char *const web_on[] = { "both", "inet", "aes67", NULL };
return check_hostname(g, err, n) &&
cfg_check_enum(g, "web_on", web_on, err, n) &&
check_addr(g, err, n);
}
static bool inet_validate(const cJSON *g, char *err, size_t n)
{
return cfg_check_int(g, "vlan_id", 1, 4094, err, n) &&
cfg_check_int(g, "pcp", 0, 7, err, n) &&
check_addr(g, err, n);
}
// mDNS: <hostname>.local plus the web UI as _http._tcp.
static void mdns_setup(void)
{
cJSON *net = cfg_get("net");
const char *host = cJSON_GetObjectItemCaseSensitive(net, "hostname")->valuestring;
esp_err_t err = mdns_init();
if (err == ESP_OK) {
mdns_hostname_set(host);
mdns_instance_name_set(host);
mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
ESP_LOGI(TAG, "mDNS: %s.local, _http._tcp port 80", host);
} else {
ESP_LOGE(TAG, "mDNS init failed: %s", esp_err_to_name(err));
}
cJSON_Delete(net);
}
static void net_apply(const cJSON *g)
{
const char *host = cJSON_GetObjectItemCaseSensitive(g, "hostname")->valuestring;
// DHCP server sees the new name at the next lease renewal.
esp_netif_set_hostname(s_netif, host);
if (mdns_hostname_set(host) == ESP_OK && mdns_instance_name_set(host) == ESP_OK) {
ESP_LOGI(TAG, "mDNS: now %s.local", host);
}
lldp_changed();
}
static void net_status(cJSON *st)
{
char ip[16] = "", mask[16] = "", gw[16] = "", dns[16] = "", mac_s[18] = "";
esp_netif_ip_info_t info;
if (esp_netif_get_ip_info(s_netif, &info) == ESP_OK && info.ip.addr) {
snprintf(ip, sizeof(ip), IPSTR, IP2STR(&info.ip));
snprintf(mask, sizeof(mask), IPSTR, IP2STR(&info.netmask));
if (info.gw.addr) {
snprintf(gw, sizeof(gw), IPSTR, IP2STR(&info.gw));
}
}
esp_netif_dns_info_t d;
if (esp_netif_get_dns_info(s_netif, ESP_NETIF_DNS_MAIN, &d) == ESP_OK && d.ip.type == ESP_IPADDR_TYPE_V4 &&
d.ip.u_addr.ip4.addr) {
snprintf(dns, sizeof(dns), IPSTR, IP2STR(&d.ip.u_addr.ip4));
}
uint8_t mac[6];
if (esp_netif_get_mac(s_netif, mac) == ESP_OK) {
snprintf(mac_s, sizeof(mac_s), "%02x:%02x:%02x:%02x:%02x:%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
// Single untagged interface until the VLAN split: AES67 and web share it.
cJSON_AddStringToObject(st, "ip", ip);
cJSON_AddStringToObject(st, "aes67_ip", ip);
cJSON_AddStringToObject(st, "mask", mask); // in use (from DHCP or static)
cJSON_AddStringToObject(st, "gw", gw);
cJSON_AddStringToObject(st, "dns", dns);
cJSON_AddStringToObject(st, "mac", mac_s);
cJSON_AddStringToObject(st, "link", s_link);
}
esp_err_t aes67_net_init(esp_eth_handle_t eth)
{
// Applied so far: hostname (DHCP, mDNS, LLDP). Static IP and VLAN come later.
ESP_ERROR_CHECK(cfg_register("net", NET_DEFAULTS, net_validate, net_apply));
ESP_ERROR_CHECK(cfg_register("inet", INET_DEFAULTS, inet_validate, NULL));
ESP_ERROR_CHECK(esp_netif_init());
esp_err_t err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
return err;
}
esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH();
s_netif = esp_netif_new(&netif_cfg);
ESP_ERROR_CHECK(esp_netif_attach(s_netif, esp_eth_new_netif_glue(eth)));
ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, eth_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ip_event, NULL));
// 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));
cJSON_Delete(net);
mdns_setup();
ESP_ERROR_CHECK(lldp_start(eth, s_netif));
status_register(net_status);
return esp_eth_start(eth);
}
esp_netif_t *aes67_net_netif(void)
{
return s_netif;
}