nearly done
This commit is contained in:
parent
6ea40b3134
commit
840909af66
14 changed files with 3082 additions and 172 deletions
|
|
@ -16,6 +16,6 @@ dependencies:
|
|||
direct_dependencies:
|
||||
- espressif/w5500
|
||||
- idf
|
||||
manifest_hash: c4bad2fb618779164783fe779b8924d7a6d94e582838878b2cc5d584eab2d703
|
||||
manifest_hash: 4b18530e214cddb04f4b4e06dc77d5623ad78d4827a5400cc696d82c91c1a496
|
||||
target: esp32s3
|
||||
version: 3.0.0
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
)
|
||||
|
||||
|
|
|
|||
638
firmware/main/diagnostics.cpp
Normal file
638
firmware/main/diagnostics.cpp
Normal 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);
|
||||
}
|
||||
57
firmware/main/diagnostics.h
Normal file
57
firmware/main/diagnostics.h
Normal 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
1414
firmware/main/display.cpp
Normal file
File diff suppressed because it is too large
Load diff
117
firmware/main/display.h
Normal file
117
firmware/main/display.h
Normal 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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, ð_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(ð_config, ð_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
388
firmware/main/network.cpp
Normal 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(ð_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,
|
||||
ð_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
93
firmware/main/network.h
Normal 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
|
||||
65
firmware/main/temperature.cpp
Normal file
65
firmware/main/temperature.cpp
Normal 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;
|
||||
}
|
||||
24
firmware/main/temperature.h
Normal file
24
firmware/main/temperature.h
Normal 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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
2
firmware/sdkconfig.defaults
Normal file
2
firmware/sdkconfig.defaults
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
CONFIG_LWIP_RAW=y
|
||||
CONFIG_LWIP_IPV6_RDNSS_MAX_DNS_SERVERS=2
|
||||
Loading…
Reference in a new issue