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:
@@ -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] 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] 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.
|
- [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.
|
- [ ] 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.
|
- [ ] 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.
|
- [ ] 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.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
idf_component_register(SRCS "aes67_net.c"
|
idf_component_register(SRCS "aes67_net.c" "lldp.c"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
REQUIRES esp_eth esp_netif esp_event
|
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)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
|
#include "lldp.h"
|
||||||
#include "mdns.h"
|
#include "mdns.h"
|
||||||
|
|
||||||
static const char *TAG = "net";
|
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;
|
const ip_event_got_ip_t *ev = data;
|
||||||
ESP_LOGI(TAG, "got IP " IPSTR " mask " IPSTR " gw " IPSTR,
|
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));
|
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 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) {
|
if (mdns_hostname_set(host) == ESP_OK && mdns_instance_name_set(host) == ESP_OK) {
|
||||||
ESP_LOGI(TAG, "mDNS: now %s.local", host);
|
ESP_LOGI(TAG, "mDNS: now %s.local", host);
|
||||||
}
|
}
|
||||||
|
lldp_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void net_status(cJSON *st)
|
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));
|
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ip_event, NULL));
|
||||||
|
|
||||||
mdns_setup();
|
mdns_setup();
|
||||||
|
ESP_ERROR_CHECK(lldp_start(eth, s_netif));
|
||||||
status_register(net_status);
|
status_register(net_status);
|
||||||
return esp_eth_start(eth);
|
return esp_eth_start(eth);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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.
|
- 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`.
|
- 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).
|
- 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.
|
- Switch port: AES67 as native/untagged VLAN, internet tagged, PoE on. IGMP snooping + querier on the AES67 VLAN.
|
||||||
|
|
||||||
## Syslog
|
## Syslog
|
||||||
|
|||||||
Reference in New Issue
Block a user