diff --git a/CLAUDE.md b/CLAUDE.md index ec697d5..bf4dcee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4 - [x] 0. Check chip revision and flash size; create IDF project and empty component stubs (layout in aes67-core-base.md). Set up the OTA partition table (two app slots, no factory) and PROJECT_VER from git now, so the layout never changes later. - [x] 1. Ethernet: IP101 up, DHCP, IP logged. Ping works. - [x] 2. Web server + config store (cJSON in NVS) + embedded index.html; /api/config, /api/status (stub values), /api/reboot. -- [ ] 2a. Finding the device: mDNS (hostname.local + _http._tcp), then LLDP (switch shows name + IP). +- [x] 2a. Finding the device: mDNS (hostname.local + _http._tcp), then LLDP (switch shows name + IP). - [ ] 2b. Firmware update: /api/ota upload + rollback self-test. Test: update to a new build, then deliberately flash a build that fails its self-test and confirm it rolls back. After this, flash over the network; keep USB for recovery. - [ ] 3. PTP TimeReceiver: lock to an existing GM (Riedel), fill `status.ptp`. Confirm EMAC hardware timestamps work. First check whether the installed ESP-IDF has a PTP example/component for the P4 before writing one. - [ ] 4. AES67 TX with a 1 kHz test tone, PTP-paced; /stream.sdp. Verify: import SDP on a Riedel Artist 4-wire AES67 port, and check packets/timestamps in Wireshark. diff --git a/components/aes67_net/CMakeLists.txt b/components/aes67_net/CMakeLists.txt index 9f17119..29477c4 100644 --- a/components/aes67_net/CMakeLists.txt +++ b/components/aes67_net/CMakeLists.txt @@ -1,4 +1,4 @@ -idf_component_register(SRCS "aes67_net.c" +idf_component_register(SRCS "aes67_net.c" "lldp.c" INCLUDE_DIRS "include" REQUIRES esp_eth esp_netif esp_event - PRIV_REQUIRES aes67_web lwip espressif__mdns) + PRIV_REQUIRES aes67_web lwip espressif__mdns esp_app_format esp_timer) diff --git a/components/aes67_net/aes67_net.c b/components/aes67_net/aes67_net.c index 97023c9..b2c5250 100644 --- a/components/aes67_net/aes67_net.c +++ b/components/aes67_net/aes67_net.c @@ -10,6 +10,7 @@ #include "esp_event.h" #include "esp_log.h" #include "esp_netif.h" +#include "lldp.h" #include "mdns.h" static const char *TAG = "net"; @@ -58,6 +59,7 @@ 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). @@ -129,6 +131,7 @@ static void net_apply(const cJSON *g) 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) @@ -170,6 +173,7 @@ esp_err_t aes67_net_init(esp_eth_handle_t eth) ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ip_event, NULL)); mdns_setup(); + ESP_ERROR_CHECK(lldp_start(eth, s_netif)); status_register(net_status); return esp_eth_start(eth); } diff --git a/components/aes67_net/lldp.c b/components/aes67_net/lldp.c new file mode 100644 index 0000000..200fe1c --- /dev/null +++ b/components/aes67_net/lldp.c @@ -0,0 +1,125 @@ +// LLDP (IEEE 802.1AB) transmit only: tells the switch our name and management IP. +#include "lldp.h" + +#include + +#include "aes67_cfg.h" +#include "esp_app_desc.h" +#include "esp_eth.h" +#include "esp_log.h" +#include "esp_timer.h" + +#define LLDP_INTERVAL_S 30 +#define LLDP_TTL_S (4 * LLDP_INTERVAL_S) +#define FRAME_MAX 512 +#define FRAME_MIN 60 // without FCS + +enum { TLV_END, TLV_CHASSIS_ID, TLV_PORT_ID, TLV_TTL, TLV_PORT_DESC, TLV_SYS_NAME, TLV_SYS_DESC, + TLV_SYS_CAP, TLV_MGMT_ADDR }; + +static const char *TAG = "lldp"; +static const uint8_t LLDP_MCAST[6] = { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e }; + +static esp_eth_handle_t s_eth; +static esp_netif_t *s_netif; +static esp_timer_handle_t s_timer; +static bool s_logged; + +static size_t tlv(uint8_t *p, int type, const void *val, size_t len) +{ + p[0] = (type << 1) | ((len >> 8) & 1); + p[1] = len & 0xff; + memcpy(p + 2, val, len); + return 2 + len; +} + +static size_t tlv_str(uint8_t *p, int type, const char *s) +{ + size_t len = strlen(s); + return tlv(p, type, s, len > 255 ? 255 : len); +} + +void lldp_send(void) +{ + if (!s_eth) { + return; + } + uint8_t mac[6]; + if (esp_eth_ioctl(s_eth, ETH_CMD_G_MAC_ADDR, mac) != ESP_OK) { + return; + } + cJSON *net = cfg_get("net"); + const char *host = cJSON_GetObjectItemCaseSensitive(net, "hostname")->valuestring; + const esp_app_desc_t *app = esp_app_get_description(); + + uint8_t f[FRAME_MAX] = { 0 }; + size_t n = 0; + memcpy(f, LLDP_MCAST, 6); + memcpy(f + 6, mac, 6); + f[12] = 0x88; + f[13] = 0xcc; + n = 14; + + uint8_t id[7] = { 4 }; // chassis ID subtype 4: MAC address + memcpy(id + 1, mac, 6); + n += tlv(f + n, TLV_CHASSIS_ID, id, sizeof(id)); + id[0] = 3; // port ID subtype 3: MAC address + n += tlv(f + n, TLV_PORT_ID, id, sizeof(id)); + uint8_t ttl[2] = { LLDP_TTL_S >> 8, LLDP_TTL_S & 0xff }; + n += tlv(f + n, TLV_TTL, ttl, sizeof(ttl)); + n += tlv_str(f + n, TLV_PORT_DESC, "eth0"); + n += tlv_str(f + n, TLV_SYS_NAME, host); + char desc[128]; + snprintf(desc, sizeof(desc), "AES67 device, %s %s (ESP-IDF %s)", app->project_name, app->version, app->idf_ver); + n += tlv_str(f + n, TLV_SYS_DESC, desc); + uint8_t cap[4] = { 0x00, 0x80, 0x00, 0x80 }; // capabilities: station only (supported, enabled) + n += tlv(f + n, TLV_SYS_CAP, cap, sizeof(cap)); + + esp_netif_ip_info_t ip = { 0 }; + bool have_ip = esp_netif_get_ip_info(s_netif, &ip) == ESP_OK && ip.ip.addr; + if (have_ip) { + // addr len 5, subtype IPv4, address, interface subtype ifIndex, ifIndex 1, OID len 0 + uint8_t m[12] = { 5, 1 }; + memcpy(m + 2, &ip.ip.addr, 4); // already network byte order + m[6] = 2; + m[10] = 1; + n += tlv(f + n, TLV_MGMT_ADDR, m, sizeof(m)); + } + n += tlv(f + n, TLV_END, NULL, 0); + if (n < FRAME_MIN) { + n = FRAME_MIN; + } + + esp_err_t err = esp_eth_transmit(s_eth, f, n); + if (err != ESP_OK) { + ESP_LOGW(TAG, "transmit failed: %s", esp_err_to_name(err)); + } else if (!s_logged || !have_ip) { + // Log once per name/IP change, not every 30 s. + ESP_LOGI(TAG, "sent: name %s, mgmt " IPSTR ", %u bytes", host, IP2STR(&ip.ip), (unsigned)n); + s_logged = have_ip; + } + cJSON_Delete(net); +} + +void lldp_changed(void) +{ + s_logged = false; + lldp_send(); +} + +static void timer_cb(void *arg) +{ + lldp_send(); +} + +esp_err_t lldp_start(esp_eth_handle_t eth, esp_netif_t *netif) +{ + s_eth = eth; + s_netif = netif; + const esp_timer_create_args_t args = { .callback = timer_cb, .name = "lldp" }; + esp_err_t err = esp_timer_create(&args, &s_timer); + if (err == ESP_OK) { + err = esp_timer_start_periodic(s_timer, LLDP_INTERVAL_S * 1000000ULL); + } + return err; +} diff --git a/components/aes67_net/lldp.h b/components/aes67_net/lldp.h new file mode 100644 index 0000000..758fd0b --- /dev/null +++ b/components/aes67_net/lldp.h @@ -0,0 +1,10 @@ +#pragma once + +#include "esp_eth_driver.h" +#include "esp_netif.h" + +// Send an LLDPDU every 30 s (TTL 120 s). +esp_err_t lldp_start(esp_eth_handle_t eth, esp_netif_t *netif); +// Send now (e.g. after an IP or hostname change) and log the next one. +void lldp_changed(void); +void lldp_send(void); diff --git a/docs/aes67-core-base.md b/docs/aes67-core-base.md index b47aa34..d6f00f6 100644 --- a/docs/aes67-core-base.md +++ b/docs/aes67-core-base.md @@ -92,6 +92,7 @@ When this device is the GM, offset/frequency/delay show "–". - Default: one untagged interface. VLAN split option: AES67 untagged + internet tagged (inet.vlan_id/pcp) using the ESP-IDF vlan_support approach (examples/network/vlan_support, ESP32-P4 supported). AES67 side has no gateway; default route and DNS are on inet. - PTP, RTP, SAP and IGMP bind to the AES67 netif. HTTP server filters on `web_on`. - mDNS (espressif/mdns): `.local` plus the web UI as `_http._tcp` port 80. A hostname change applies live (the new name answers after ~1 s of RFC 6762 probing). +- LLDP (IEEE 802.1AB), transmit only, own code (not in IDF/lwIP): every 30 s, TTL 120 s, plus immediately on a new IP or hostname. TLVs: chassis ID and port ID = MAC, port description "eth0", system name = net.hostname, system description = project + firmware version, capabilities station-only, management address = IPv4. With the VLAN split, send it on the AES67 (untagged) side; the 802.1 port VLAN ID TLV can be added then. - Switch port: AES67 as native/untagged VLAN, internet tagged, PoE on. IGMP snooping + querier on the AES67 VLAN. ## Syslog