Step 2a (2/2): LLDP transmit, switch shows name and IP

- aes67_net/lldp.c: IEEE 802.1AB LLDPDU sent as a raw frame with
  esp_eth_transmit every 30 s (TTL 120 s) and immediately on a new IP
  or hostname change. TLVs: chassis/port ID (MAC), port description,
  system name (net.hostname), system description (project + version),
  capabilities station-only, IPv4 management address.
- Docs: LLDP in the core Network section; step 2a ticked.
- Verified on an HP 2530-8G-PoE+ (show lldp info remote-device 2): all
  fields shown, management address 192.168.192.244.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 23:14:09 +10:00
parent 979420a6bb
commit 30c32e702b
6 changed files with 143 additions and 3 deletions
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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)
+4
View File
@@ -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);
}
+125
View File
@@ -0,0 +1,125 @@
// LLDP (IEEE 802.1AB) transmit only: tells the switch our name and management IP.
#include "lldp.h"
#include <string.h>
#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;
}
+10
View File
@@ -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);
+1
View File
@@ -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): `<net.hostname>.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