Files
aes67-ESP32-P4/components/aes67_net/aes67_net.c
T
bsncubed c2621f192a Step 2.2: config registry and NVS store, GET/POST /api/config
- aes67_web/cfg.c: cfg_register(group, defaults, validate, apply),
  cfg_override_defaults() for project defaults, cfg_get(). Stored values
  are loaded on register and merged over defaults (known keys, matching
  types). NVS keeps only keys that differ from defaults, one JSON string
  per group, so new firmware defaults still reach untouched settings.
- POST /api/config: type check against defaults, per-group validation,
  nothing stored unless every group passes; 400 "group.key: reason".
  Unknown keys/groups are ignored. Returns the new config.
- Core groups registered by their components with the UI's ranges:
  ptp (aes67_ptp), aes67 (aes67_tx, incl. multicast range and mono_sum
  => 1 channel), net/inet (aes67_net), log (aes67_syslog).
- Project: PROJECT.def overrides (hostname p4-aes67, name P4 AES67) and
  the "source" group in main/project_cfg.c.
- Config is stored only; nothing is applied live yet.
- Verified on board: defaults, valid save, 8 rejected values (nothing
  stored), persistence across reboot, restore to defaults.

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

149 lines
5.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"
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));
}
// 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);
}
static void net_status(cJSON *st)
{
char ip[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));
}
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, "mac", mac_s);
cJSON_AddStringToObject(st, "link", s_link);
}
esp_err_t aes67_net_init(esp_eth_handle_t eth)
{
// Config is stored only for now; applying it (hostname, static IP, VLAN) comes later.
ESP_ERROR_CHECK(cfg_register("net", NET_DEFAULTS, net_validate, NULL));
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));
status_register(net_status);
return esp_eth_start(eth);
}