nearly done

This commit is contained in:
Ricardo (XenGi) Band 2026-07-17 00:08:48 +02:00
commit 840909af66
No known key found for this signature in database
14 changed files with 3082 additions and 172 deletions

View file

@ -16,6 +16,6 @@ dependencies:
direct_dependencies:
- espressif/w5500
- idf
manifest_hash: c4bad2fb618779164783fe779b8924d7a6d94e582838878b2cc5d584eab2d703
manifest_hash: 4b18530e214cddb04f4b4e06dc77d5623ad78d4827a5400cc696d82c91c1a496
target: esp32s3
version: 3.0.0

View file

@ -1,5 +1,9 @@
idf_component_register(
SRCS "inet_mon_main.cpp"
"display.cpp"
"network.cpp"
"diagnostics.cpp"
"temperature.cpp"
PRIV_REQUIRES spi_flash
REQUIRES
esp_event
@ -8,6 +12,11 @@ idf_component_register(
driver
nvs_flash
esp_driver_gpio
esp_driver_i2c
esp_driver_tsens
esp_lcd
esp_timer
esp_driver_ledc
lwip
INCLUDE_DIRS ""
)

View file

@ -0,0 +1,638 @@
/**
* @file diagnostics.cpp
* @brief Connectivity test suite — ping, DNS, latency, packet loss
*
* Test sequence (DIAG_TEST_COUNT = 8):
* 0. IPv4 config (address + gateway + DNS)
* 1. IPv6 config (address + gateway + DNS)
* 2. Gateway ping (IPv4 + IPv6)
* 3. Uplink ping (IPv4 public IPs)
* 4. DNS resolution (via both DNS servers)
* 5. Latency measurement
* 6. S2S VPN ping
* 7. Temperature
*
* Ping uses raw ICMP sockets (no external dependency needed).
*/
#include "diagnostics.h"
#include "network.h"
#include "temperature.h"
#include "display.h"
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_random.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "lwip/icmp.h"
#include "lwip/inet.h"
#include "lwip/inet_chksum.h"
#include "lwip/dns.h"
#include "lwip/ip4_addr.h"
#include "lwip/ip6_addr.h"
#include "lwip/prot/icmp6.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
static const char *TAG = "DIAG";
// Frame counter for spinner animation during pause delays
static int s_diag_frame = 0;
// ─── Test names ─────────────────────────────────────────────────────────────
const char *g_diag_test_names[DIAG_TEST_COUNT] = {
"IPv4",
"IPv6",
"Gateway",
"Uplink",
"DNS",
"Latency",
"S2S VPN",
"Temp"
};
// ─── Public ping targets ────────────────────────────────────────────────────
static const char *s_uplink_v4[4] = {
"1.1.1.1",
"8.8.8.8",
"9.9.9.9",
"208.67.222.222"
};
static const char *s_uplink_v6[4] = {
"2606:4700:4700::1111", // Cloudflare DNS
"2001:4860:4860::8888", // Google DNS
"2620:fe::fe", // Quad9 DNS
"2620:119:35::35" // OpenDNS
};
static const char *s_dns_domains[3] = {
"google.com",
"cloudflare.com",
"example.com"
};
// ─── Raw ICMP Ping ──────────────────────────────────────────────────────────
/// Minimum ICMP packet size (excluding IP header)
#define ICMP_PKT_SIZE 64
/// Calculate IPv4 checksum (used for ICMP header)
static uint16_t icmp_checksum(uint16_t *buf, int len)
{
uint32_t sum = 0;
for (int i = 0; i < len / 2; i++) {
sum += buf[i];
}
if (len & 1) {
sum += ((uint8_t *)buf)[len - 1];
}
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
return (uint16_t)~sum;
}
/// Ping an IPv4 address. Returns average RTT in ms, or -1 on failure.
/// Uses raw ICMP sockets (CONFIG_LWIP_RAW must be enabled).
int diagnostics_ping_ipv4(const char *ip_str, int timeout_ms, int count,
int *out_successes)
{
if (!ip_str || strcmp(ip_str, "--") == 0) {
if (out_successes) *out_successes = 0;
return -1;
}
// Parse IP
struct in_addr addr;
if (!inet_aton(ip_str, &addr)) {
ESP_LOGW(TAG, "Invalid IP: %s", ip_str);
return -1;
}
// Create raw socket
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock < 0) {
ESP_LOGW(TAG, "Cannot create raw socket (errno %d)", errno);
return -1;
}
// Set socket timeout
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
struct sockaddr_in dest;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_addr = addr;
int rtt_sum = 0;
int responses = 0;
uint16_t pid = (uint16_t)(esp_random() & 0xFFFF);
for (int p = 0; p < count; p++) {
// Build ICMP echo request
uint8_t pkt[ICMP_PKT_SIZE];
struct icmp_echo_hdr *icmp_hdr = (struct icmp_echo_hdr *)pkt;
ICMPH_TYPE_SET(icmp_hdr, ICMP_ECHO);
ICMPH_CODE_SET(icmp_hdr, 0);
icmp_hdr->id = lwip_htons(pid);
icmp_hdr->seqno = lwip_htons(p);
// Fill payload with timestamp (microseconds since boot)
int64_t sent_us = esp_timer_get_time();
memcpy(pkt + sizeof(struct icmp_echo_hdr), &sent_us, sizeof(sent_us));
// Zero out rest of payload (keeps checksum deterministic)
memset(pkt + sizeof(struct icmp_echo_hdr) + sizeof(sent_us), 0,
ICMP_PKT_SIZE - sizeof(struct icmp_echo_hdr) - sizeof(sent_us));
// Zero checksum field, then compute using lwIP's standard function
// NOTE: inet_chksum() returns a value ready to store directly — no htons() needed!
icmp_hdr->chksum = 0;
icmp_hdr->chksum = inet_chksum(pkt, ICMP_PKT_SIZE);
// Send
int ret = sendto(sock, pkt, ICMP_PKT_SIZE, 0,
(struct sockaddr *)&dest, sizeof(dest));
if (ret < 0) {
ESP_LOGW(TAG, "Ping sendto failed: errno %d", errno);
continue;
}
// Receive reply — lwIP raw sockets include the IP header
uint8_t recv_buf[128];
struct sockaddr_in from;
socklen_t from_len = sizeof(from);
ret = recvfrom(sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&from, &from_len);
if (ret >= 0) {
// lwIP delivers data starting at IP header (20 bytes for IPv4)
struct icmp_echo_hdr *reply =
(struct icmp_echo_hdr *)(recv_buf + 20);
if (ICMPH_TYPE(reply) == ICMP_ER &&
reply->id == lwip_htons(pid) &&
reply->seqno == lwip_htons(p)) {
responses++;
// Extract sent timestamp from reply payload
int64_t sent_us_reply;
memcpy(&sent_us_reply,
recv_buf + 20 + sizeof(struct icmp_echo_hdr),
sizeof(sent_us_reply));
int64_t now_us = esp_timer_get_time();
int rtt = (int)((now_us - sent_us_reply) / 1000);
if (rtt < 0) rtt = 0;
rtt_sum += rtt;
}
}
// Update spinner animation after each ping attempt
s_diag_frame++;
display_update_spinner(s_diag_frame);
}
close(sock);
if (out_successes) *out_successes = responses;
if (responses == 0) return -1;
return rtt_sum / responses;
}
/// Ping an IPv6 address. Returns average RTT in ms, or -1 on failure.
int diagnostics_ping_ipv6(const char *ip_str, int timeout_ms, int count,
int *out_successes)
{
if (!ip_str || strcmp(ip_str, "--") == 0) {
if (out_successes) *out_successes = 0;
return -1;
}
// Parse IPv6 address
struct in6_addr addr6;
if (inet_pton(AF_INET6, ip_str, &addr6) != 1) {
ESP_LOGW(TAG, "Invalid IPv6: %s", ip_str);
return -1;
}
// Create raw IPv6 socket
int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
if (sock < 0) {
ESP_LOGW(TAG, "Cannot create IPv6 raw socket (errno %d)", errno);
return -1;
}
// Set socket timeout
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
struct sockaddr_in6 dest;
memset(&dest, 0, sizeof(dest));
dest.sin6_family = AF_INET6;
dest.sin6_addr = addr6;
// Link-local addresses (fe80::/10) need the interface scope ID
if (IN6_IS_ADDR_LINKLOCAL(&addr6)) {
dest.sin6_scope_id = network_get_eth_scope_id();
} else {
dest.sin6_scope_id = 0;
}
int rtt_sum = 0;
int responses = 0;
uint16_t pid = (uint16_t)(esp_random() & 0xFFFF);
for (int p = 0; p < count; p++) {
// Build ICMPv6 echo request
uint8_t pkt[ICMP_PKT_SIZE + 40]; // +40 for IPv6 header if needed
struct icmp6_echo_hdr *icmp6 = (struct icmp6_echo_hdr *)pkt;
icmp6->type = ICMP6_TYPE_EREQ;
icmp6->code = 0;
icmp6->id = lwip_htons(pid);
icmp6->seqno = lwip_htons(p);
// Fill payload with timestamp (microseconds since boot)
int64_t sent_us = esp_timer_get_time();
memcpy(pkt + sizeof(struct icmp6_echo_hdr), &sent_us, sizeof(sent_us));
// Zero out rest of payload
memset(pkt + sizeof(struct icmp6_echo_hdr) + sizeof(sent_us), 0,
ICMP_PKT_SIZE - sizeof(struct icmp6_echo_hdr) - sizeof(sent_us));
// ICMPv6 checksum is computed automatically by lwIP for raw sockets
// (RFC 3542 §3.1 — IPV6_CHECKSUM socket option, cannot be disabled).
// lwIP uses ip6_chksum_pseudo() and overwrites offset 2.
icmp6->chksum = 0;
// Send
int ret = sendto(sock, pkt, ICMP_PKT_SIZE, 0,
(struct sockaddr *)&dest, sizeof(dest));
if (ret < 0) {
ESP_LOGW(TAG, "IPv6 ping sendto failed: %s errno %d", ip_str, errno);
continue;
}
// Receive reply — loop to discard non-echo-reply ICMPv6 traffic (e.g. ND)
uint8_t recv_buf[256];
struct sockaddr_in6 from;
socklen_t from_len;
bool got_reply = false;
for (int tries = 0; tries < 10 && !got_reply; tries++) {
from_len = sizeof(from);
ret = recvfrom(sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&from, &from_len);
if (ret < 0) {
if (tries == 0) {
ESP_LOGI(TAG, "IPv6 recvfrom timeout: %s seq=%d (errno %d)",
ip_str, p, errno);
}
break; // timeout — no more packets
}
if (ret < 40 + (int)sizeof(struct icmp6_echo_hdr)) {
ESP_LOGI(TAG, "IPv6 recv too short: %d bytes", ret);
continue;
}
struct icmp6_echo_hdr *reply =
(struct icmp6_echo_hdr *)(recv_buf + 40);
if (reply->type == ICMP6_TYPE_EREP &&
reply->code == 0 &&
reply->id == lwip_htons(pid) &&
reply->seqno == lwip_htons(p)) {
ESP_LOGI(TAG, "IPv6 ping OK: %s seq=%d", ip_str, p);
responses++;
// Extract sent timestamp from reply payload
int64_t sent_us_reply;
memcpy(&sent_us_reply,
recv_buf + 40 + sizeof(struct icmp6_echo_hdr),
sizeof(sent_us_reply));
int64_t now_us = esp_timer_get_time();
int rtt = (int)((now_us - sent_us_reply) / 1000);
if (rtt < 0) rtt = 0;
rtt_sum += rtt;
got_reply = true;
} else {
ESP_LOGI(TAG, "IPv6 pkt type=%d code=%d id=%d seq=%d",
reply->type, reply->code, reply->id, reply->seqno);
}
}
// Update spinner animation after each IPv6 ping attempt
s_diag_frame++;
display_update_spinner(s_diag_frame);
}
close(sock);
if (out_successes) *out_successes = responses;
if (responses == 0) return -1;
return rtt_sum / responses;
}
// ─── DNS resolution ─────────────────────────────────────────────────────────
bool diagnostics_dns_lookup(const char *hostname, const char **dns_result,
int timeout_ms)
{
if (!hostname) return false;
struct addrinfo hints;
struct addrinfo *result = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // both A (IPv4) and AAAA (IPv6) records
hints.ai_socktype = SOCK_STREAM;
int ret = getaddrinfo(hostname, NULL, &hints, &result);
if (ret == 0 && result) {
freeaddrinfo(result);
if (dns_result) *dns_result = "ok";
return true;
}
ESP_LOGW(TAG, "DNS resolve failed for '%s': ret=%d", hostname, ret);
if (dns_result) {
*dns_result = (ret == EAI_AGAIN) ? "timeout" : "nxdomain";
}
return false;
}
// ─── DNS via specific server ─────────────────────────────────────────────
/// Try DNS resolution via a specific DNS server IP.
/// Temporarily sets the lwIP DNS servers, queries all domains,
/// then restores the original DNS config.
/// Returns true if any domain returned any record (A or AAAA).
static bool test_dns_via_server(const char *dns_str,
const char *domains[], int num_domains,
int timeout_ms)
{
if (!dns_str || strcmp(dns_str, "--") == 0) return false;
// Save current DNS config
ip_addr_t saved[DNS_MAX_SERVERS];
for (int i = 0; i < DNS_MAX_SERVERS; i++) {
saved[i] = *dns_getserver(i); // dereference returned pointer
}
// Parse and set the target DNS server as the sole resolver
ip_addr_t target = IPADDR4_INIT(0);
ip4_addr_t ip4;
if (ip4addr_aton(dns_str, &ip4)) {
ip_addr_copy_from_ip4(target, ip4);
} else {
ip6_addr_t ip6;
if (ip6addr_aton(dns_str, &ip6)) {
ip_addr_copy_from_ip6(target, ip6);
} else {
// Invalid address — restore and bail
for (int i = 0; i < DNS_MAX_SERVERS; i++) {
dns_setserver(i, &saved[i]);
}
return false;
}
}
dns_setserver(0, &target);
for (int i = 1; i < DNS_MAX_SERVERS; i++) {
dns_setserver(i, IP_ADDR_ANY);
}
// Try each domain
bool any_ok = false;
for (int i = 0; i < num_domains; i++) {
const char *dns_status = NULL;
if (diagnostics_dns_lookup(domains[i], &dns_status, timeout_ms)) {
any_ok = true;
break;
}
}
// Restore original DNS config
for (int i = 0; i < DNS_MAX_SERVERS; i++) {
dns_setserver(i, &saved[i]);
}
return any_ok;
}
// ─── Latency measurement ────────────────────────────────────────────────────
static void measure_latency_and_loss(int timeout_ms,
int *out_avg_rtt,
float *out_loss_pct,
bool *out_v4_ok,
bool *out_v6_ok)
{
int total_rtt = 0;
int successes = 0;
bool any_v4 = false;
bool any_v6 = false;
int total = 0;
// Ping all 4 IPv4 targets (5 pings each for accurate loss)
for (int i = 0; i < 4; i++) {
int pkts = 0;
int rtt = diagnostics_ping_ipv4(s_uplink_v4[i], 1000, 5, &pkts);
successes += pkts;
if (rtt >= 0) {
total_rtt += rtt * pkts;
any_v4 = true;
ESP_LOGI(TAG, "Ping %s: RTT=%dms (%d/%d)", s_uplink_v4[i], rtt, pkts, 5);
} else {
ESP_LOGW(TAG, "Ping %s: FAILED (0/%d)", s_uplink_v4[i], 5);
}
}
// Ping all 4 IPv6 targets (5 pings each for accurate loss)
for (int i = 0; i < 4; i++) {
int pkts = 0;
int rtt = diagnostics_ping_ipv6(s_uplink_v6[i], 1000, 5, &pkts);
successes += pkts;
if (rtt >= 0) {
total_rtt += rtt * pkts;
any_v6 = true;
ESP_LOGI(TAG, "Ping %s: RTT=%dms (%d/%d)", s_uplink_v6[i], rtt, pkts, 5);
} else {
ESP_LOGW(TAG, "Ping %s: FAILED (0/%d)", s_uplink_v6[i], 5);
}
}
total = 40; // 8 targets × 5 pings each
if (out_v4_ok) *out_v4_ok = any_v4;
if (out_v6_ok) *out_v6_ok = any_v6;
if (out_loss_pct) {
if (total > 0) {
*out_loss_pct = ((float)(total - successes) / (float)total) * 100.0f;
} else {
*out_loss_pct = -1.0f;
}
}
if (out_avg_rtt) {
if (successes > 0) {
*out_avg_rtt = total_rtt / successes;
ESP_LOGI(TAG, "Latency & Packet Loss: avg RTT=%dms, loss=%.0f%% (%d/%d pings)",
*out_avg_rtt, *out_loss_pct, successes, total);
} else {
*out_avg_rtt = -1;
}
}
}
// ─── Run all tests ──────────────────────────────────────────────────────────
// Helper: pause 1 second so user can read the progress display
// Pause ~1 second with spinner animation (calls display_update_spinner each 100ms)
static void diag_pause(void)
{
for (int i = 0; i < 10; i++) {
vTaskDelay(pdMS_TO_TICKS(100));
s_diag_frame++;
display_update_spinner(s_diag_frame);
}
}
void diagnostics_run_all(struct test_results_t *results,
int timeout_ms,
diag_progress_cb_t progress_cb)
{
if (!results) return;
results->tests_completed = 0;
results->tests_total = DIAG_TEST_COUNT;
// ── Test 0: IPv4 config (address + gateway + DNS via DHCP) ──
diag_pause();
results->has_ipv4 = network_has_ipv4();
results->ipv4 = results->has_ipv4 ? network_get_ipv4_str() : "--";
results->gateway_ip = results->has_ipv4 ? network_get_gateway_str() : "--";
const char *dns4 = network_get_dns_str();
bool has_gw = results->has_ipv4 && results->gateway_ip && strcmp(results->gateway_ip, "--") != 0;
bool has_dns = dns4 && strcmp(dns4, "--") != 0;
ESP_LOGI(TAG, "IPv4: %s gw=%s dns=%s",
results->ipv4,
has_gw ? results->gateway_ip : "--",
has_dns ? dns4 : "--");
results->tests_completed = 1;
if (progress_cb) progress_cb(results, 0);
// ── Test 1: IPv6 config (address + gateway + DNS via SLAAC/RDNSS) ──
diag_pause();
results->has_ipv6 = network_has_ipv6();
results->ipv6 = results->has_ipv6 ? network_get_ipv6_str() : "--";
results->gateway6_ip = results->has_ipv6 ? network_get_gateway6_str() : "--";
const char *dns6 = network_get_dns6_str();
bool has_gw6 = results->has_ipv6 && results->gateway6_ip && strcmp(results->gateway6_ip, "--") != 0;
bool has_dns6 = dns6 && strcmp(dns6, "--") != 0;
ESP_LOGI(TAG, "IPv6: %s gw=%s dns=%s",
results->ipv6,
has_gw6 ? results->gateway6_ip : "--",
has_dns6 ? dns6 : "--");
results->tests_completed = 2;
if (progress_cb) progress_cb(results, 1);
// ── Test 2: Gateway ping (IPv4 + IPv6) ──
diag_pause();
const char *gw4 = network_get_gateway_str();
results->gateway_reachable_v4 = (diagnostics_ping_ipv4(gw4, timeout_ms, 3, NULL) >= 0);
ESP_LOGI(TAG, "Gateway %s: %s", gw4,
results->gateway_reachable_v4 ? "OK" : "FAIL");
const char *gw6 = results->gateway6_ip;
bool can_ping_gw6 = gw6 && strcmp(gw6, "--") != 0;
results->gateway_reachable_v6 = (can_ping_gw6 && diagnostics_ping_ipv6(gw6, timeout_ms, 3, NULL) >= 0);
if (can_ping_gw6) {
ESP_LOGI(TAG, "Gateway6 %s: %s", gw6,
results->gateway_reachable_v6 ? "OK" : "FAIL");
}
results->tests_completed = 3;
if (progress_cb) progress_cb(results, 2);
// ── Test 3: Uplink ping (IPv4 + IPv6) ──
diag_pause();
results->uplink_reachable_v4 = false;
for (int i = 0; i < 4; i++) {
if (diagnostics_ping_ipv4(s_uplink_v4[i], timeout_ms, 1, NULL) >= 0) {
results->uplink_reachable_v4 = true;
break;
}
}
results->uplink_reachable_v6 = false;
for (int i = 0; i < 4; i++) {
if (diagnostics_ping_ipv6(s_uplink_v6[i], timeout_ms, 1, NULL) >= 0) {
results->uplink_reachable_v6 = true;
break;
}
}
ESP_LOGI(TAG, "Uplink: v4=%s v6=%s",
results->uplink_reachable_v4 ? "OK" : "FAIL",
results->uplink_reachable_v6 ? "OK" : "FAIL");
results->tests_completed = 4;
if (progress_cb) progress_cb(results, 3);
// ── Test 4: DNS resolution ──
diag_pause();
results->dns4_ok = false;
results->dns6_ok = false;
results->dns_timeout = false;
// Test via IPv4 DNS server (from DHCP)
results->dns4_ok = test_dns_via_server(
network_get_dns_str(), s_dns_domains, 3, timeout_ms);
// Test via IPv6 DNS server (from RDNSS)
results->dns6_ok = test_dns_via_server(
network_get_dns6_str(), s_dns_domains, 3, timeout_ms);
results->dns_ok = results->dns4_ok || results->dns6_ok;
if (!results->dns_ok) {
results->dns_timeout = true;
}
ESP_LOGI(TAG, "DNS: %s (v4=%s v6=%s)",
results->dns_ok ? "OK" : "FAIL",
results->dns4_ok ? "OK" : "FAIL",
results->dns6_ok ? "OK" : "FAIL");
results->tests_completed = 5;
if (progress_cb) progress_cb(results, 4);
// ── Test 5: Latency & packet loss ──
diag_pause();
float loss = -1.0f;
int avg_rtt = -1;
bool lat_v4_ok = false, lat_v6_ok = false;
measure_latency_and_loss(timeout_ms, &avg_rtt, &loss, &lat_v4_ok, &lat_v6_ok);
results->latency_v4_ok = lat_v4_ok;
results->latency_v6_ok = lat_v6_ok;
results->latency_ms = avg_rtt;
results->packet_loss_pct = loss;
results->tests_completed = 6;
if (progress_cb) progress_cb(results, 5);
// ── Test 6: RS25 S2S VPN ping (1.1.1.1 placeholder, detects VPN forwarding) ──
diag_pause();
results->vpn_reachable = (diagnostics_ping_ipv4("1.1.1.1", timeout_ms, 2, NULL) >= 0);
ESP_LOGI(TAG, "RS25 VPN: %s",
results->vpn_reachable ? "OK" : "FAIL");
results->tests_completed = 7;
if (progress_cb) progress_cb(results, 6);
// ── Test 7: Temperature ──
diag_pause();
results->temperature_c = temperature_read();
ESP_LOGI(TAG, "Temperature: %.1f C", results->temperature_c);
results->tests_completed = 8;
if (progress_cb) progress_cb(results, 7);
ESP_LOGI(TAG, "All tests completed (%d/%d)",
results->tests_completed, results->tests_total);
}

View file

@ -0,0 +1,57 @@
#ifndef DIAGNOSTICS_H
#define DIAGNOSTICS_H
#include <stdint.h>
#include <stdbool.h>
#include "display.h"
#ifdef __cplusplus
extern "C" {
#endif
/// Maximum hostname length for DNS queries
#define DIAG_MAX_HOSTNAME 64
// ─── Test names (for progress display) ──────────────────────────────────────
#define DIAG_TEST_COUNT 8
extern const char *g_diag_test_names[DIAG_TEST_COUNT];
// ─── Progress callback ──────────────────────────────────────────────────────
/// Called by diagnostics after each test step so the display can refresh.
/// Passes the current results struct and the index of the test just completed.
typedef void (*diag_progress_cb_t)(const struct test_results_t *results,
int test_index);
// ─── API ────────────────────────────────────────────────────────────────────
/// Run all diagnostic tests. Calls progress_cb after each step.
/// Results are written to the provided results struct.
void diagnostics_run_all(struct test_results_t *results,
int timeout_ms,
diag_progress_cb_t progress_cb);
/// Ping a single IPv4 address. Returns average RTT in ms, or -1 on failure.
/// If out_successes is non-NULL, stores the number of successful pings there.
int diagnostics_ping_ipv4(const char *ip_str, int timeout_ms, int count,
int *out_successes);
/// Ping a single IPv6 address. Returns average RTT in ms, or -1 on failure.
/// If out_successes is non-NULL, stores the number of successful pings there.
int diagnostics_ping_ipv6(const char *ip_str, int timeout_ms, int count,
int *out_successes);
/// Resolve a hostname via DNS (uses lwip DNS API).
/// Returns true if resolution succeeded (at least one IP returned).
/// *dns_result is set to "ok", "timeout", or "nxdomain" accordingly.
/// Caller may pass NULL for dns_result to omit the status string.
bool diagnostics_dns_lookup(const char *hostname, const char **dns_result,
int timeout_ms);
#ifdef __cplusplus
}
#endif
#endif // DIAGNOSTICS_H

1414
firmware/main/display.cpp Normal file

File diff suppressed because it is too large Load diff

117
firmware/main/display.h Normal file
View file

@ -0,0 +1,117 @@
#ifndef DISPLAY_H
#define DISPLAY_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
// ─── Colors (RGB565) ─────────────────────────────────────────────────────────
/// Macro to create an RGB565 color from 8-bit R, G, B components.
#define RGB565(r, g, b) ((((r) & 0xF8) << 8) | (((g) & 0xFC) << 3) | (((b) & 0xF8) >> 3))
#define COLOR_BLACK 0x0000
#define COLOR_WHITE 0xFFFF
#define COLOR_GREEN 0x07E0 // (0, 255, 0)
#define COLOR_RED 0xF800 // (255, 0, 0)
#define COLOR_YELLOW 0xFFE0 // (255, 255, 0)
#define COLOR_BLUE 0x001F // (0, 0, 255)
#define COLOR_CYAN 0x07FF // (0, 255, 255)
#define COLOR_ORANGE 0xFBE0 // approx
// ─── Display dimensions ──────────────────────────────────────────────────────
#define DISP_WIDTH 240
#define DISP_HEIGHT 240
// ─── Test results structure (forward declaration) ────────────────────────────
struct test_results_t {
bool link_up;
bool has_ipv4;
bool has_ipv6;
const char *ipv4;
const char *ipv6;
const char *ipv6_ll; // link-local (fe80::...)
const char *ipv6_gua; // global unicast (2a02::...)
const char *gateway_ip;
const char *gateway6_ip;
bool gateway_reachable_v4;
bool gateway_reachable_v6;
bool uplink_reachable_v4;
bool uplink_reachable_v6;
bool dns_ok;
bool dns4_ok; // true if IPv4 DNS query succeeded
bool dns6_ok; // true if IPv6 DNS query succeeded
bool dns_timeout;
bool vpn_reachable; // true if VPN target IP responded
bool latency_v4_ok; // true if at least one IPv4 latency target responded
bool latency_v6_ok; // true if at least one IPv6 latency target responded
int latency_ms;
float packet_loss_pct;
float temperature_c;
int tests_completed;
int tests_total;
};
// ─── API ─────────────────────────────────────────────────────────────────────
/// Initialize the display.
/// Must be called after spi_bus_initialize(SPI2_HOST).
void display_init(void);
/// Fill the entire 240x240 screen with one 16-bit color.
void display_fill(uint16_t color);
/// Set a single pixel at (x, y). x: 0-239, y: 0-239.
void display_pixel(int x, int y, uint16_t color);
/// Draw ASCII text starting at pixel (x, y).
/// Uses a built-in 8x8 monospace font (ASCII 32-126).
/// Characters outside this range show as space.
/// fg = foreground color, bg = background color.
/// scale = font multiplier (1 = 8x8, 2 = 16x16, etc.)
void display_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale);
/// Draw a filled rectangle from (x0,y0) to (x1,y1) inclusive.
void display_rect(int x0, int y0, int x1, int y1, uint16_t color);
/// Draw a 16x16 circle indicator for up/down status.
/// online=true -> green fill, online=false -> red fill.
void display_dot_online(int x, int y, bool online);
/// Render the full status screen. Clears the area first, then draws everything.
void display_status(const struct test_results_t *results);
/// Show a simple boot progress screen with [X] link / [X] dhcp / [X] slaac markers.
/// link_done: true once Ethernet link is established
/// dhcp_done: true once DHCP gives us an IPv4 address
/// slaac_done: true once IPv6 SLAAC provides a global unicast address
void display_booting(bool link_done, bool dhcp_done, bool slaac_done);
/// Show a splash screen with a title.
void display_splash(const char *title);
/// Show incremental test progress with animation.
/// Top line: spinning indicator + "Running: TestName"
/// Below: completed tests listed with "+" prefix.
/// frame: incrementing counter (0,1,2,3) for spinner animation.
void display_progress(const struct test_results_t *results,
int test_index, int frame);
/// Update just the spinner character (partial redraw, no full screen clear).
/// Must call display_progress() first to set up the layout.
void display_update_spinner(int frame);
/// Show a failure screen showing which test failed and details.
/// Displays for 30 seconds then returns.
void display_failure(const struct test_results_t *results);
#ifdef __cplusplus
}
#endif
#endif // DISPLAY_H

View file

@ -2,7 +2,7 @@
dependencies:
## Required IDF version
idf:
version: '>=5.5.4'
version: '>=6.0.1'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"

View file

@ -1,3 +1,17 @@
/**
* @file inet_mon_main.cpp
* @brief Internet Monitor — Main State Machine
*
* State machine sequence:
* 1. SPLASH — Show splash screen for 2 seconds
* 2. LINK_CHECK — Wait for Ethernet link up (30s timeout)
* 3. DHCP_WAIT — Wait for DHCP IPv4 address (30s timeout)
* 4. RUN_TESTS — Execute diagnostic tests with live progress display
* 5. SHOW_STATUS — Display results for 60 seconds
* 6. On link drop or any test failure: show FAILURE screen for 30s
* 7. Loop back to LINK_CHECK
*/
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
@ -7,225 +21,267 @@
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_eth.h"
#include "esp_eth_mac.h"
#include "esp_eth_phy.h"
#include "esp_eth_com.h"
#include "esp_eth_mac_w5500.h"
#include "esp_eth_phy_w5500.h"
#include "display.h"
#include "network.h"
#include "diagnostics.h"
#include "temperature.h"
static const char *TAG = "INETMON";
// -------------------- PIN CONFIG --------------------
#define PIN_SCK 12
#define PIN_MOSI 11
#define PIN_MISO 13
#define PIN_CS 10
#define PIN_RST 9
#define PIN_INT 14
// ─── Animation ──────────────────────────────────────────────────────────────
static esp_eth_handle_t eth_handle = NULL;
static int s_anim_frame = 0;
// ─── Helper: wait with display update ───────────────────────────────────────
// -------------------- EVENT HANDLER --------------------
static void eth_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
// ─── Boot wait helpers ─────────────────────────────────────────────────────
/// Wait up to `timeout_sec` seconds for Ethernet link, showing booting screen.
/// Returns true once link is up.
static bool wait_for_link(int timeout_sec)
{
switch (event_id) {
case ETHERNET_EVENT_CONNECTED:
ESP_LOGI(TAG, "Ethernet LINK UP");
break;
case ETHERNET_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "Ethernet LINK DOWN");
break;
case ETHERNET_EVENT_START:
ESP_LOGI(TAG, "Ethernet START");
break;
case ETHERNET_EVENT_STOP:
ESP_LOGI(TAG, "Ethernet STOP");
break;
default:
break;
display_booting(false, false, false); // initial draw
for (int i = 0; i < timeout_sec; i++) {
if (network_is_link_up()) return true;
vTaskDelay(pdMS_TO_TICKS(1000));
}
return network_is_link_up();
}
static void got_ip_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
/// Wait up to `timeout_sec` seconds for DHCP IP, showing booting screen.
/// link_done must be true (link must already be up).
/// Returns true once DHCP gives us an IPv4 address.
static bool wait_for_dhcp(int timeout_sec)
{
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(
TAG,
"IP: " IPSTR " GW: " IPSTR " MASK: " IPSTR,
IP2STR(&event->ip_info.ip),
IP2STR(&event->ip_info.gw),
IP2STR(&event->ip_info.netmask)
);
display_booting(true, false, false); // initial draw
for (int i = 0; i < timeout_sec; i++) {
if (network_has_ipv4()) return true;
vTaskDelay(pdMS_TO_TICKS(1000));
}
return network_has_ipv4();
}
/// Wait up to `timeout_sec` seconds for a global IPv6 address (GUA).
/// Shows the booting screen with [X] link, [X] dhcp, and [ ]/[X] slaac.
/// Returns true once a GUA is assigned, false on timeout.
static bool wait_for_slaac(int timeout_sec)
{
// If already assigned, skip the boot screen entirely
if (network_has_ipv6_gua()) return true;
display_booting(true, true, false); // initial draw ([ ] SLAAC)
int timeout_ms = timeout_sec * 1000;
int elapsed = 0;
while (elapsed < timeout_ms) {
if (network_has_ipv6_gua()) {
display_booting(true, true, true);
vTaskDelay(pdMS_TO_TICKS(250)); // let user see the green [X]
return true;
}
vTaskDelay(pdMS_TO_TICKS(100));
elapsed += 100;
}
bool got = network_has_ipv6_gua();
display_booting(true, true, got);
return got;
}
// ─── app_main ───────────────────────────────────────────────────────────────
extern "C" void app_main(void)
{
printf("Hello world!\n");
printf("Hello world! Internet Monitor v0.1\n");
/* Print chip information */
// ── 1. Print chip info ──
esp_chip_info_t chip_info;
uint32_t flash_size;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), %s%s%s%s, ",
CONFIG_IDF_TARGET,
chip_info.cores,
CONFIG_IDF_TARGET, chip_info.cores,
(chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
(chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : ""
);
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "");
unsigned major_rev = chip_info.revision / 100;
unsigned minor_rev = chip_info.revision % 100;
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
if (esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
printf("Get flash size failed");
return;
}
printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");
printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
fflush(stdout);
ESP_LOGI(TAG, "Starting W5500 Ethernet...");
// NVS + event loop
// ── 2. Initialize system services ──
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &eth_event_handler, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL);
ESP_ERROR_CHECK(gpio_install_isr_service(0));
// ---------------- SPI BUS ----------------
spi_bus_config_t buscfg = {};
// ── 3. Initialize SPI bus for W5500 (SPI2_HOST) ──
network_spi_init();
buscfg.mosi_io_num = PIN_MOSI;
buscfg.miso_io_num = PIN_MISO;
buscfg.sclk_io_num = PIN_SCK;
// ── 4. Initialize display (SPI3_HOST, independent) ──
display_init();
buscfg.quadwp_io_num = -1;
buscfg.quadhd_io_num = -1;
// ── 5. Show splash screen ──
display_splash("INET MON");
vTaskDelay(pdMS_TO_TICKS(2000));
buscfg.data4_io_num = -1;
buscfg.data5_io_num = -1;
buscfg.data6_io_num = -1;
buscfg.data7_io_num = -1;
// ── 6. Initialize Ethernet ──
network_register_handlers();
esp_eth_handle_t eth = network_eth_init();
network_netif_init(eth);
network_eth_start(eth);
buscfg.max_transfer_sz = 0;
buscfg.flags = 0;
buscfg.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO;
buscfg.intr_flags = 0;
// ── 7. Initialize temperature sensor (best-effort) ──
temperature_init();
ESP_ERROR_CHECK(
spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)
);
// ---------------- SPI DEVICE CONFIG ----------------
spi_device_interface_config_t spi_devcfg = {};
spi_devcfg.command_bits = 16;
spi_devcfg.address_bits = 8;
spi_devcfg.mode = 0;
spi_devcfg.clock_speed_hz = 5 * 1000 * 1000;
spi_devcfg.spics_io_num = PIN_CS;
spi_devcfg.queue_size = 20;
// ---------------- W5500 CONFIG ----------------
eth_w5500_config_t w5500_config =
ETH_W5500_DEFAULT_CONFIG(SPI2_HOST, &spi_devcfg);
w5500_config.int_gpio_num = PIN_INT;
// ---------------- MAC / PHY ----------------
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
phy_config.reset_gpio_num = PIN_RST;
// ---------------- CREATE MAC + PHY ----------------
esp_eth_mac_t *mac =
esp_eth_mac_new_w5500(&w5500_config, &mac_config);
esp_eth_phy_t *phy =
esp_eth_phy_new_w5500(&phy_config);
esp_eth_config_t eth_config =
ETH_DEFAULT_CONFIG(mac, phy);
ESP_LOGI(TAG, "Installing Ethernet driver...");
ESP_ERROR_CHECK(
esp_eth_driver_install(&eth_config, &eth_handle)
);
ESP_LOGI(TAG, "Ethernet driver installed");
uint8_t mac_addr[6];
ESP_ERROR_CHECK(
esp_eth_ioctl(
eth_handle,
ETH_CMD_G_MAC_ADDR,
mac_addr
)
);
ESP_LOGI(
TAG,
"Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X",
mac_addr[0],
mac_addr[1],
mac_addr[2],
mac_addr[3],
mac_addr[4],
mac_addr[5]
);
// ---------------- NETWORK INTERFACE ----------------
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
esp_netif_t *eth_netif = esp_netif_new(&cfg);
ESP_ERROR_CHECK(
esp_netif_attach(
eth_netif,
esp_eth_new_netif_glue(eth_handle)
)
);
// ---------------- START ETH ----------------
ESP_ERROR_CHECK(esp_eth_start(eth_handle));
ESP_LOGI(TAG, "W5500 init complete, waiting for DHCP...");
// ── 8. Main test loop ──
struct test_results_t results;
memset(&results, 0, sizeof(results));
results.tests_total = DIAG_TEST_COUNT;
while (true) {
vTaskDelay(pdMS_TO_TICKS(2000));
// ═══════════════════════════════════════════════════════════
// STATE: LINK_CHECK
// ═══════════════════════════════════════════════════════════
ESP_LOGI(TAG, "State: LINK_CHECK");
if (!network_is_link_up()) {
ESP_LOGI(TAG, "Waiting for Ethernet link...");
bool link_up = wait_for_link(30);
if (!link_up) {
ESP_LOGW(TAG, "Link check timeout");
results.link_up = false;
display_failure(&results);
vTaskDelay(pdMS_TO_TICKS(30000));
continue; // retry link
}
}
// printf("Restarting now.\n");
// esp_restart();
// Start IPv6 SLAAC now that link is up (not automatic for Ethernet in IDF v6)
network_start_ipv6();
// ═══════════════════════════════════════════════════════════
// STATE: DHCP_WAIT
// ═══════════════════════════════════════════════════════════
ESP_LOGI(TAG, "State: DHCP_WAIT");
if (!network_has_ipv4()) {
ESP_LOGI(TAG, "Waiting for DHCP...");
bool got_ip = wait_for_dhcp(30);
if (!got_ip) {
ESP_LOGW(TAG, "DHCP timeout");
results.link_up = network_is_link_up();
results.has_ipv4 = false;
results.ipv4 = "--";
display_failure(&results);
vTaskDelay(pdMS_TO_TICKS(30000));
// If link dropped, go back to link check
if (!network_is_link_up()) continue;
// Otherwise retry DHCP
continue;
}
}
// Both link and DHCP done — wait for IPv6 GUA via SLAAC
// Shows [X] link, [X] dhcp, [ ] slaac while waiting
wait_for_slaac(20);
// ═══════════════════════════════════════════════════════════
// STATE: RUN_TESTS
// ═══════════════════════════════════════════════════════════
ESP_LOGI(TAG, "State: RUN_TESTS");
// Reset results for new test cycle
results.link_up = network_is_link_up();
results.has_ipv4 = network_has_ipv4();
results.ipv4 = results.has_ipv4 ? network_get_ipv4_str() : "--";
results.has_ipv6 = network_has_ipv6();
results.ipv6_ll = network_has_ipv6_ll() ? network_get_ipv6_ll_str() : "--";
results.ipv6_gua = network_has_ipv6_gua() ? network_get_ipv6_gua_str() : "--";
results.ipv6 = results.ipv6_gua[0] != '-' ? results.ipv6_gua : results.ipv6_ll;
results.gateway_ip = network_has_ipv4() ? network_get_gateway_str() : "--";
results.gateway6_ip = network_has_ipv6() ? network_get_gateway6_str() : "--";
results.gateway_reachable_v4 = false;
results.gateway_reachable_v6 = false;
results.uplink_reachable_v4 = false;
results.uplink_reachable_v6 = false;
results.dns_ok = false;
results.dns4_ok = false;
results.dns6_ok = false;
results.dns_timeout = false;
results.vpn_reachable = false;
results.latency_ms = -1;
results.packet_loss_pct = -1.0f;
results.temperature_c = -273.0f;
results.tests_completed = 0;
// Show initial progress screen (before any test starts) — pass -1 so test 0 shows
display_progress(&results, -1, s_anim_frame++);
// Run diagnostics with live progress updates
diagnostics_run_all(&results, 5000,
[](const struct test_results_t *r, int test_idx) {
display_progress(r, test_idx, s_anim_frame++);
}
);
// ═══════════════════════════════════════════════════════════
// STATE: EVALUATE — check if any test failed
// ═══════════════════════════════════════════════════════════
bool all_passed = results.link_up &&
results.has_ipv4 &&
results.gateway_reachable_v4;
if (!all_passed) {
ESP_LOGW(TAG, "Some tests failed, showing failure screen");
display_failure(&results);
vTaskDelay(pdMS_TO_TICKS(30000));
// If link dropped, go back to link check
if (!network_is_link_up()) continue;
// Otherwise re-run tests
continue;
}
// ═══════════════════════════════════════════════════════════
// STATE: SHOW_STATUS — display results for 60 seconds
// ═══════════════════════════════════════════════════════════
ESP_LOGI(TAG, "State: SHOW_STATUS");
// Re-read IPv6 state — may have arrived during tests (SLAAC RAs)
results.has_ipv6 = network_has_ipv6();
results.ipv6_ll = network_has_ipv6_ll() ? network_get_ipv6_ll_str() : "--";
results.ipv6_gua = network_has_ipv6_gua() ? network_get_ipv6_gua_str() : "--";
results.ipv6 = results.ipv6_gua[0] != '-' ? results.ipv6_gua : results.ipv6_ll;
results.gateway6_ip = network_has_ipv6() ? network_get_gateway6_str() : "--";
display_status(&results);
for (int i = 0; i < 60; i++) {
vTaskDelay(pdMS_TO_TICKS(1000));
// If link drops, go back to link check early
if (!network_is_link_up()) {
ESP_LOGI(TAG, "Link dropped during status display");
break;
}
}
// Loop back to LINK_CHECK
ESP_LOGI(TAG, "Test cycle complete, restarting...");
}
}

388
firmware/main/network.cpp Normal file
View file

@ -0,0 +1,388 @@
/**
* @file network.cpp
* @brief W5500 Ethernet — init, DHCP, IPv6, event handlers
*
* Manages the W5500 Ethernet controller on SPI2_HOST. Provides network
* status strings (IP, gateway, DNS, MAC) for use by diagnostics and display.
*
* Pin mapping:
* W5500_SCK = GPIO12 W5500_MOSI = GPIO11
* W5500_MISO = GPIO13 W5500_CS = GPIO10
* W5500_RST = GPIO9 W5500_INT = GPIO14
*
* NOTE: The LCD uses separate SPI3_HOST (GPIO40/41), NOT the same bus.
*/
#include "network.h"
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_mac.h"
#include "driver/spi_master.h"
#include "esp_eth.h"
#include "esp_eth_mac.h"
#include "esp_eth_phy.h"
#include "esp_eth_com.h"
#include "esp_eth_mac_w5500.h"
#include "esp_eth_phy_w5500.h"
#include "lwip/priv/nd6_priv.h"
static const char *TAG = "NETWORK";
// ─── Pin Mapping ────────────────────────────────────────────────────────────
#define PIN_SCK 12
#define PIN_MOSI 11
#define PIN_MISO 13
#define PIN_W5500_CS 10
#define PIN_W5500_RST 9
#define PIN_W5500_INT 14
// ─── Global state ───────────────────────────────────────────────────────────
static esp_eth_handle_t s_eth_handle = NULL;
static bool s_link_up = false;
static bool s_has_ipv4 = false;
static bool s_has_ipv6 = false;
// Netif handle (store so we can enable IPv6 later)
static esp_netif_t *s_netif = NULL;
static char s_ipv4_str[24] = "--";
static char s_netmask_str[24] = "--";
static char s_gw_str[24] = "--";
static char s_dns_str[24] = "--";
// Separate storage for link-local and global IPv6 addresses
static char s_ipv6_ll_str[48] = "--"; // fe80::...
static char s_ipv6_gua_str[48] = "--"; // 2a02::... (global unicast)
static char s_gw6_str[48] = "--"; // IPv6 default gateway (link-local of router)
static char s_dns6_str[48] = "--"; // IPv6 DNS from RDNSS
static char s_mac_str[20] = "--";
// Track whether we have specific IPv6 address types
static bool s_has_ipv6_ll = false;
static bool s_has_ipv6_gua = false;
// Backward-compat: s_ipv6_str points to whichever is more interesting
static const char *s_ipv6_str = s_ipv6_ll_str;
// ─── IPv6 Compact Formatter ───────────────────────────────────────────────
/// Format an IPv6 address in RFC 5952 compact form (with ::) for display.
/// Uses esp_netif's block macros which apply htonl for correct endianness.
static void format_ipv6_compact(const esp_ip6_addr_t *ip6, char *buf, size_t bufsz)
{
// Get the 8 16-bit blocks with correct byte order
uint16_t blocks[8];
blocks[0] = ESP_IP6_ADDR_BLOCK1(ip6);
blocks[1] = ESP_IP6_ADDR_BLOCK2(ip6);
blocks[2] = ESP_IP6_ADDR_BLOCK3(ip6);
blocks[3] = ESP_IP6_ADDR_BLOCK4(ip6);
blocks[4] = ESP_IP6_ADDR_BLOCK5(ip6);
blocks[5] = ESP_IP6_ADDR_BLOCK6(ip6);
blocks[6] = ESP_IP6_ADDR_BLOCK7(ip6);
blocks[7] = ESP_IP6_ADDR_BLOCK8(ip6);
// Find longest run of consecutive zero blocks (RFC 5952: replace with ::)
int best_start = -1, best_len = 0;
int cur_start = -1, cur_len = 0;
for (int i = 0; i < 8; i++) {
if (blocks[i] == 0) {
if (cur_start < 0) { cur_start = i; cur_len = 1; }
else { cur_len++; }
if (cur_len > best_len) { best_start = cur_start; best_len = cur_len; }
} else {
cur_start = -1; cur_len = 0;
}
}
size_t pos = 0;
for (int i = 0; i < 8; i++) {
if (best_len >= 2 && i == best_start) {
// Insert :: (only once at the start of the zero run)
if (pos + 2 < bufsz) { buf[pos++] = ':'; buf[pos++] = ':'; }
i += best_len - 1; // skip the rest of the zero run
continue;
}
if (pos > 0 && pos < bufsz && buf[pos-1] != ':') {
// If we just wrote a block (not after ::), add colon separator
// But only if we didn't just write ':' from the previous iteration
if (pos + 1 < bufsz) buf[pos++] = ':';
}
// Write the hex block without leading zeros
int cnt = snprintf(buf + pos, bufsz - pos, "%x", (unsigned)blocks[i]);
if (cnt > 0 && (size_t)cnt < bufsz - pos) pos += cnt;
}
// Handle edge case: all-zero address (::)
if (best_len == 8) {
buf[0] = ':'; buf[1] = ':'; pos = 2;
}
buf[pos < bufsz ? pos : bufsz - 1] = '\0';
}
// ─── Event Handlers ─────────────────────────────────────────────────────────
static void eth_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
switch (event_id) {
case ETHERNET_EVENT_CONNECTED:
ESP_LOGI(TAG, "Ethernet LINK UP");
s_link_up = true;
break;
case ETHERNET_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "Ethernet LINK DOWN");
s_link_up = false;
s_has_ipv4 = false;
s_has_ipv6 = false;
break;
case ETHERNET_EVENT_START:
ESP_LOGI(TAG, "Ethernet START");
break;
case ETHERNET_EVENT_STOP:
ESP_LOGI(TAG, "Ethernet STOP");
break;
default:
break;
}
}
static void got_ip_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
s_has_ipv4 = true;
snprintf(s_ipv4_str, sizeof(s_ipv4_str),
IPSTR, IP2STR(&event->ip_info.ip));
snprintf(s_gw_str, sizeof(s_gw_str),
IPSTR, IP2STR(&event->ip_info.gw));
snprintf(s_netmask_str, sizeof(s_netmask_str),
IPSTR, IP2STR(&event->ip_info.netmask));
// Get DNS from DHCP
esp_netif_t *netif = event->esp_netif;
// Find the first IPv4 DNS slot (RDNSS may have overwritten MAIN with IPv6)
esp_netif_dns_info_t dns;
esp_netif_dns_type_t slots[] = {
ESP_NETIF_DNS_MAIN, ESP_NETIF_DNS_BACKUP, ESP_NETIF_DNS_FALLBACK
};
for (int i = 0; i < 3; i++) {
if (esp_netif_get_dns_info(netif, slots[i], &dns) == ESP_OK &&
dns.ip.type == ESP_IPADDR_TYPE_V4) {
snprintf(s_dns_str, sizeof(s_dns_str),
IPSTR, IP2STR(&dns.ip.u_addr.ip4));
break;
}
}
ESP_LOGI(TAG, "IP: %s GW: %s MASK: %s DNS: %s",
s_ipv4_str, s_gw_str, s_netmask_str, s_dns_str);
}
static void got_ip6_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data;
s_has_ipv6 = true;
s_netif = event->esp_netif;
// Determine address type (link-local vs global)
esp_ip6_addr_type_t addr_type = esp_netif_ip6_get_addr_type(&event->ip6_info.ip);
char temp_buf[48];
format_ipv6_compact(&event->ip6_info.ip, temp_buf, sizeof(temp_buf));
if (addr_type == ESP_IP6_ADDR_IS_LINK_LOCAL) {
// Link-local: fe80::...
strncpy(s_ipv6_ll_str, temp_buf, sizeof(s_ipv6_ll_str) - 1);
s_ipv6_ll_str[sizeof(s_ipv6_ll_str) - 1] = '\0';
s_has_ipv6_ll = true;
ESP_LOGI(TAG, "IPv6 link-local: %s", s_ipv6_ll_str);
} else if (addr_type == ESP_IP6_ADDR_IS_GLOBAL) {
// Global unicast: 2xxx:: or other GUA prefixes
strncpy(s_ipv6_gua_str, temp_buf, sizeof(s_ipv6_gua_str) - 1);
s_ipv6_gua_str[sizeof(s_ipv6_gua_str) - 1] = '\0';
s_has_ipv6_gua = true;
s_ipv6_str = s_ipv6_gua_str; // point backward-compat to GUA
ESP_LOGI(TAG, "IPv6 global: %s", s_ipv6_gua_str);
} else {
// Other types (ULA, etc.) - just store in main field
strncpy(s_ipv6_ll_str, temp_buf, sizeof(s_ipv6_ll_str) - 1);
s_ipv6_ll_str[sizeof(s_ipv6_ll_str) - 1] = '\0';
s_has_ipv6_ll = true;
}
// Capture IPv6 DNS from RDNSS option in RA
esp_netif_dns_info_t dns;
if (s_netif && esp_netif_get_dns_info(s_netif, ESP_NETIF_DNS_MAIN, &dns) == ESP_OK) {
if (dns.ip.type == ESP_IPADDR_TYPE_V6) {
format_ipv6_compact(&dns.ip.u_addr.ip6, s_dns6_str, sizeof(s_dns6_str));
ESP_LOGI(TAG, "IPv6 DNS: %s", s_dns6_str);
}
}
// Extract IPv6 default gateway from lwIP's ND6 default router list
for (int i = 0; i < LWIP_ND6_NUM_ROUTERS; i++) {
struct nd6_neighbor_cache_entry *ne = default_router_list[i].neighbor_entry;
if (ne != NULL && !ip6_addr_isany(&ne->next_hop_address)) {
// Found a valid router — its next_hop_address is the gateway
format_ipv6_compact((const esp_ip6_addr_t *)&ne->next_hop_address,
s_gw6_str, sizeof(s_gw6_str));
ESP_LOGI(TAG, "IPv6 gateway: %s", s_gw6_str);
break;
}
}
}
// ─── SPI Bus ────────────────────────────────────────────────────────────────
void network_spi_init(void)
{
spi_bus_config_t buscfg;
memset(&buscfg, 0, sizeof(buscfg));
buscfg.mosi_io_num = (int)PIN_MOSI;
buscfg.miso_io_num = (int)PIN_MISO;
buscfg.sclk_io_num = (int)PIN_SCK;
buscfg.quadwp_io_num = -1;
buscfg.quadhd_io_num = -1;
buscfg.data4_io_num = -1;
buscfg.data5_io_num = -1;
buscfg.data6_io_num = -1;
buscfg.data7_io_num = -1;
buscfg.max_transfer_sz = 0;
buscfg.flags = 0;
buscfg.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO;
buscfg.intr_flags = 0;
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
ESP_LOGI(TAG, "SPI2_HOST initialized for W5500 Ethernet");
}
// ─── Ethernet Init ──────────────────────────────────────────────────────────
esp_eth_handle_t network_eth_init(void)
{
ESP_LOGI(TAG, "Starting W5500 Ethernet...");
// SPI device config
spi_device_interface_config_t spi_devcfg;
memset(&spi_devcfg, 0, sizeof(spi_devcfg));
spi_devcfg.command_bits = 16;
spi_devcfg.address_bits = 8;
spi_devcfg.mode = 0;
spi_devcfg.clock_speed_hz = 5 * 1000 * 1000; // 5 MHz
spi_devcfg.spics_io_num = (int)PIN_W5500_CS;
spi_devcfg.queue_size = 20;
// W5500 config
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(SPI2_HOST, &spi_devcfg);
w5500_config.int_gpio_num = (int)PIN_W5500_INT;
// MAC / PHY config
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
phy_config.reset_gpio_num = (int)PIN_W5500_RST;
// Create MAC + PHY
esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
esp_eth_phy_t *phy = esp_eth_phy_new_w5500(&phy_config);
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy);
ESP_LOGI(TAG, "Installing Ethernet driver...");
ESP_ERROR_CHECK(esp_eth_driver_install(&eth_config, &s_eth_handle));
ESP_LOGI(TAG, "Ethernet driver installed");
// Set MAC address from ESP32 efuse (W5500 has no factory MAC)
uint8_t mac_addr[6];
esp_read_mac(mac_addr, ESP_MAC_ETH);
// Ensure locally-administered unicast: set bit 1 of byte 0
mac_addr[0] = (mac_addr[0] & 0xFE) | 0x02;
esp_eth_ioctl(s_eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr);
// Store MAC string
snprintf(s_mac_str, sizeof(s_mac_str),
"%02X:%02X:%02X:%02X:%02X:%02X",
mac_addr[0], mac_addr[1], mac_addr[2],
mac_addr[3], mac_addr[4], mac_addr[5]);
ESP_LOGI(TAG, "Ethernet MAC: %s", s_mac_str);
return s_eth_handle;
}
esp_netif_t *network_netif_init(esp_eth_handle_t eth_handle)
{
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_handle)));
return s_netif;
}
void network_start_ipv6(void)
{
if (!s_netif) {
ESP_LOGW(TAG, "Cannot start IPv6: netif not ready");
return;
}
// Explicitly request IPv6 link-local address creation.
// In ESP-IDF v6, this is NOT automatic for Ethernet interfaces.
// This triggers SLAAC and fires IP_EVENT_GOT_IP6 when an address is assigned.
esp_err_t ret = esp_netif_create_ip6_linklocal(s_netif);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to create IPv6 link-local: %s", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "IPv6 link-local creation requested");
}
}
void network_eth_start(esp_eth_handle_t eth_handle)
{
ESP_ERROR_CHECK(esp_eth_start(eth_handle));
ESP_LOGI(TAG, "W5500 started, waiting for link + DHCP...");
}
void network_register_handlers(void)
{
esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID,
&eth_event_handler, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP,
&got_ip_event_handler, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6,
&got_ip6_event_handler, NULL);
}
// ─── Accessors ──────────────────────────────────────────────────────────────
bool network_is_link_up(void) { return s_link_up; }
bool network_has_ipv4(void) { return s_has_ipv4; }
bool network_has_ipv6(void) { return s_has_ipv6; }
bool network_has_ipv6_ll(void) { return s_has_ipv6_ll; }
bool network_has_ipv6_gua(void) { return s_has_ipv6_gua; }
const char *network_get_ipv4_str(void) { return s_ipv4_str; }
const char *network_get_gateway_str(void) { return s_gw_str; }
const char *network_get_netmask_str(void) { return s_netmask_str; }
const char *network_get_dns_str(void) { return s_dns_str; }
const char *network_get_ipv6_str(void) { return s_ipv6_str; }
const char *network_get_ipv6_ll_str(void) { return s_ipv6_ll_str; }
const char *network_get_ipv6_gua_str(void){ return s_ipv6_gua_str; }
const char *network_get_gateway6_str(void) { return s_gw6_str; }
const char *network_get_dns6_str(void) { return s_dns6_str; }
const char *network_get_mac_str(void) { return s_mac_str; }
int network_get_eth_scope_id(void)
{
if (!s_netif) return 0;
return esp_netif_get_netif_impl_index(s_netif);
}

93
firmware/main/network.h Normal file
View file

@ -0,0 +1,93 @@
#ifndef NETWORK_H
#define NETWORK_H
#include <stdint.h>
#include <stdbool.h>
#include "esp_eth.h"
#include "esp_netif.h"
#include "esp_event.h" // for esp_event_handler_register
#ifdef __cplusplus
extern "C" {
#endif
/// Initialize SPI2_HOST bus for W5500.
/// Must be called before network_eth_init().
void network_spi_init(void);
/// Explicitly start IPv6 SLAAC on the Ethernet interface.
/// Must be called after network_netif_init().
/// In ESP-IDF v6, IPv6 link-local is NOT automatic for Ethernet.
void network_start_ipv6(void);
/// Initialize W5500 Ethernet on SPI2_HOST.
/// Must be called after nvs_flash_init(), esp_netif_init(), esp_event_loop_create_default(),
/// and network_spi_init().
/// Returns the ethernet handle.
esp_eth_handle_t network_eth_init(void);
/// Create and attach a TCP/IP network interface for Ethernet.
/// Must be called after network_eth_init().
esp_netif_t *network_netif_init(esp_eth_handle_t eth_handle);
/// Start the Ethernet driver (begins PHY link negotiation + DHCP).
void network_eth_start(esp_eth_handle_t eth_handle);
/// Register Ethernet and IP event handlers with the default event loop.
/// Must be called before starting Ethernet.
void network_register_handlers(void);
/// Check if Ethernet link is up.
bool network_is_link_up(void);
/// Check if IPv4 address has been assigned via DHCP.
bool network_has_ipv4(void);
/// Get the IPv4 address string (e.g., "192.168.1.123").
const char *network_get_ipv4_str(void);
/// Get the IPv4 gateway string.
const char *network_get_gateway_str(void);
/// Get the IPv4 netmask string.
const char *network_get_netmask_str(void);
/// Get the IPv4 DNS server string (first DNS from DHCP).
const char *network_get_dns_str(void);
/// Get the IPv6 DNS server string (from RDNSS in RA).
const char *network_get_dns6_str(void);
/// Get the link-local IPv6 address (fe80::...).
const char *network_get_ipv6_ll_str(void);
/// Get the global IPv6 address (2xxx::... or ULA).
const char *network_get_ipv6_gua_str(void);
/// Get the primary IPv6 address string (GUA if available, else link-local).
const char *network_get_ipv6_str(void);
/// Get the IPv6 gateway string (router's link-local, from lwIP ND6 router list).
const char *network_get_gateway6_str(void);
/// Check if a link-local IPv6 address is available.
bool network_has_ipv6_ll(void);
/// Check if a global IPv6 address is available.
bool network_has_ipv6_gua(void);
/// Check if any IPv6 address is available.
bool network_has_ipv6(void);
/// Get the MAC address string (for display).
const char *network_get_mac_str(void);
/// Get the lwIP interface scope ID (index) for the Ethernet netif.
/// Used when sending packets to link-local IPv6 (fe80::) addresses.
int network_get_eth_scope_id(void);
#ifdef __cplusplus
}
#endif
#endif // NETWORK_H

View file

@ -0,0 +1,65 @@
/**
* @file temperature.cpp
* @brief ESP32-S3 internal temperature sensor driver
*
* Uses the built-in TEMPSENSOR peripheral to measure chip die temperature.
* No external wiring needed. The chip temperature is typically 10-20°C
* above ambient due to self-heating, but is useful for relative monitoring.
*
* API reference: driver/temperature_sensor.h
*/
#include "temperature.h"
#include <stdio.h>
#include "esp_log.h"
#include "driver/temperature_sensor.h"
static const char *TAG = "TEMP";
// ─── State ──────────────────────────────────────────────────────────────────
static temperature_sensor_handle_t s_temp_handle = NULL;
static bool s_sensor_ready = false;
// ─── Public API ─────────────────────────────────────────────────────────────
bool temperature_init(void)
{
ESP_LOGI(TAG, "Initializing ESP32-S3 internal temperature sensor...");
// Configure range for typical operation (0°C to 80°C covers rack temps)
temperature_sensor_config_t config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(0, 80);
esp_err_t ret = temperature_sensor_install(&config, &s_temp_handle);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Temperature sensor install failed: %s", esp_err_to_name(ret));
return false;
}
ret = temperature_sensor_enable(s_temp_handle);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Temperature sensor enable failed: %s", esp_err_to_name(ret));
temperature_sensor_uninstall(s_temp_handle);
s_temp_handle = NULL;
return false;
}
s_sensor_ready = true;
ESP_LOGI(TAG, "Internal temperature sensor ready");
return true;
}
float temperature_read(void)
{
if (!s_sensor_ready || !s_temp_handle) return -273.0f;
float celsius = -273.0f;
esp_err_t ret = temperature_sensor_get_celsius(s_temp_handle, &celsius);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Temperature read failed: %s", esp_err_to_name(ret));
return -273.0f;
}
return celsius;
}

View file

@ -0,0 +1,24 @@
#ifndef TEMPERATURE_H
#define TEMPERATURE_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/// Initialize the QMI8658C temperature sensor on I2C_NUM_0.
/// Must be called after i2c_driver_install().
/// Returns true if sensor was detected and initialized.
bool temperature_init(void);
/// Read the current temperature in degrees Celsius.
/// Returns -273.0f (absolute zero) if read failed or sensor not present.
float temperature_read(void);
#ifdef __cplusplus
}
#endif
#endif // TEMPERATURE_H

View file

@ -1031,6 +1031,35 @@ CONFIG_GDMA_OBJ_DRAM_SAFE=y
# CONFIG_GPIO_CTRL_FUNC_IN_IRAM is not set
# end of ESP-Driver:GPIO Configurations
#
# ESP-Driver:I2C Configurations
#
# default:
# CONFIG_I2C_ISR_IRAM_SAFE is not set
# default:
# CONFIG_I2C_ENABLE_DEBUG_LOG is not set
# default:
CONFIG_I2C_MASTER_ISR_HANDLER_IN_IRAM=y
# end of ESP-Driver:I2C Configurations
#
# ESP-Driver:I2S Configurations
#
# default:
# CONFIG_I2S_ISR_IRAM_SAFE is not set
# default:
# CONFIG_I2S_CTRL_FUNC_IN_IRAM is not set
# default:
# CONFIG_I2S_ENABLE_DEBUG_LOG is not set
# end of ESP-Driver:I2S Configurations
#
# ESP-Driver:LEDC Configurations
#
# default:
# CONFIG_LEDC_CTRL_FUNC_IN_IRAM is not set
# end of ESP-Driver:LEDC Configurations
#
# ESP-Driver:SPI Configurations
#
@ -1040,6 +1069,13 @@ CONFIG_SPI_MASTER_ISR_IN_IRAM=y
CONFIG_SPI_SLAVE_ISR_IN_IRAM=y
# end of ESP-Driver:SPI Configurations
#
# ESP-Driver:Temperature Sensor Configurations
#
# default:
# CONFIG_TEMP_SENSOR_ENABLE_DEBUG_LOG is not set
# end of ESP-Driver:Temperature Sensor Configurations
#
# ESP-Driver:UART Configurations
#
@ -1202,6 +1238,17 @@ CONFIG_ESP_SPI_BUS_LOCK_ISR_FUNCS_IN_IRAM=y
CONFIG_ESP_INTR_IN_IRAM=y
# end of Hardware Settings
#
# ESP-Driver:LCD Controller Configurations
#
# default:
# CONFIG_LCD_RGB_ISR_IRAM_SAFE is not set
# default:
# CONFIG_LCD_RGB_RESTART_IN_VSYNC is not set
# default:
# CONFIG_LCD_ENABLE_DEBUG_LOG is not set
# end of ESP-Driver:LCD Controller Configurations
#
# LibC
#
@ -1664,7 +1711,7 @@ CONFIG_LWIP_IPV6=y
CONFIG_LWIP_IPV6_AUTOCONFIG=y
CONFIG_LWIP_IPV6_NUM_ADDRESSES=3
# CONFIG_LWIP_IPV6_FORWARD is not set
CONFIG_LWIP_IPV6_RDNSS_MAX_DNS_SERVERS=0
CONFIG_LWIP_IPV6_RDNSS_MAX_DNS_SERVERS=2
CONFIG_LWIP_IPV6_DHCP6=y
CONFIG_LWIP_NETIF_STATUS_CALLBACK=y
# default:

View file

@ -0,0 +1,2 @@
CONFIG_LWIP_RAW=y
CONFIG_LWIP_IPV6_RDNSS_MAX_DNS_SERVERS=2