638 lines
22 KiB
C++
638 lines
22 KiB
C++
/**
|
||
* @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);
|
||
}
|