From 840909af66412a54eaa7e7c068b97751a452c785 Mon Sep 17 00:00:00 2001 From: "Ricardo (XenGi) Band" Date: Fri, 17 Jul 2026 00:08:48 +0200 Subject: [PATCH] nearly done --- firmware/dependencies.lock | 2 +- firmware/main/CMakeLists.txt | 11 +- firmware/main/diagnostics.cpp | 638 ++++++++++++++ firmware/main/diagnostics.h | 57 ++ firmware/main/display.cpp | 1414 +++++++++++++++++++++++++++++++ firmware/main/display.h | 117 +++ firmware/main/idf_component.yml | 2 +- firmware/main/inet_mon_main.cpp | 394 +++++---- firmware/main/network.cpp | 388 +++++++++ firmware/main/network.h | 93 ++ firmware/main/temperature.cpp | 65 ++ firmware/main/temperature.h | 24 + firmware/sdkconfig | 49 +- firmware/sdkconfig.defaults | 2 + 14 files changed, 3083 insertions(+), 173 deletions(-) create mode 100644 firmware/main/diagnostics.cpp create mode 100644 firmware/main/diagnostics.h create mode 100644 firmware/main/display.cpp create mode 100644 firmware/main/display.h create mode 100644 firmware/main/network.cpp create mode 100644 firmware/main/network.h create mode 100644 firmware/main/temperature.cpp create mode 100644 firmware/main/temperature.h create mode 100644 firmware/sdkconfig.defaults diff --git a/firmware/dependencies.lock b/firmware/dependencies.lock index 6150696..c72ea38 100644 --- a/firmware/dependencies.lock +++ b/firmware/dependencies.lock @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/w5500 - idf -manifest_hash: c4bad2fb618779164783fe779b8924d7a6d94e582838878b2cc5d584eab2d703 +manifest_hash: 4b18530e214cddb04f4b4e06dc77d5623ad78d4827a5400cc696d82c91c1a496 target: esp32s3 version: 3.0.0 diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index 24e9f5d..38d5867 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -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 "" ) - diff --git a/firmware/main/diagnostics.cpp b/firmware/main/diagnostics.cpp new file mode 100644 index 0000000..19a3f98 --- /dev/null +++ b/firmware/main/diagnostics.cpp @@ -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 +#include +#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); +} diff --git a/firmware/main/diagnostics.h b/firmware/main/diagnostics.h new file mode 100644 index 0000000..14f1134 --- /dev/null +++ b/firmware/main/diagnostics.h @@ -0,0 +1,57 @@ +#ifndef DIAGNOSTICS_H +#define DIAGNOSTICS_H + +#include +#include +#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 diff --git a/firmware/main/display.cpp b/firmware/main/display.cpp new file mode 100644 index 0000000..105b11f --- /dev/null +++ b/firmware/main/display.cpp @@ -0,0 +1,1414 @@ +/** + * @file display.cpp + * @brief ST7789VW 240x240 LCD display driver using ESP-IDF esp_lcd API + * + * This driver uses the official ESP-IDF esp_lcd_panel_st7789 API on SPI3_HOST. + * The LCD is connected to dedicated SPI pins (GPIO40,41) separate from the + * W5500 Ethernet which uses SPI2_HOST (GPIO11,12,13). + * + * Pin mapping (verified from Waveshare ESP32-S3-LCD-1.3 schematics and demo): + * MOSI = GPIO41 (LCD data) + * SCLK = GPIO40 (LCD clock) + * CS = GPIO39 (chip select) + * DC = GPIO38 (data/command) + * RST = GPIO42 (reset) + * BL_EN = GPIO4 (backlight enable, active HIGH) + * BL_PWM = GPIO20 (backlight PWM brightness control) + * + * @note Backlight uses two pins: GPIO4 as digital enable, GPIO20 for PWM dimming. + */ + +#include "display.h" +#include "diagnostics.h" + +#include +#include +#include +#include "esp_log.h" +#include "driver/spi_master.h" +#include "driver/gpio.h" +#include "driver/ledc.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" + +static const char *TAG = "DISPLAY"; + +// ─── Correct Pin Mapping (Waveshare ESP32-S3-LCD-1.3 Arduino User_Setup.h) ── + +#define PIN_LCD_MOSI ((gpio_num_t)41) +#define PIN_LCD_CLK ((gpio_num_t)40) +#define PIN_LCD_CS ((gpio_num_t)39) +#define PIN_LCD_DC ((gpio_num_t)38) +#define PIN_LCD_RST ((gpio_num_t)42) +#define PIN_LCD_BLK ((gpio_num_t)4) // Backlight enable, active HIGH +#define PIN_LCD_BL_PWM ((gpio_num_t)20) // Backlight PWM brightness +// Test each to find the actual backlight pin +#define PIN_LCD_BLK_CANDIDATES {4, 20, 22} + +// The LCD uses a dedicated SPI bus (SPI3_HOST) separate from the W5500 Ethernet +// which uses SPI2_HOST. This avoids pin conflicts since the LCD uses GPIO40/41 +// while the W5500 uses GPIO11/12/13. +#define LCD_SPI_HOST SPI3_HOST + +// ─── Panel handles ────────────────────────────────────────────────────────── + +static esp_lcd_panel_io_handle_t s_io_handle = NULL; +static esp_lcd_panel_handle_t s_panel_handle = NULL; + +// ─── 8×8 Monospace Font (ASCII 32-126) ────────────────────────────────────── + +static const uint8_t FONT8x8[96][8] = { + // 0x20 ' ' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + // 0x21 '!' + { 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00 }, + // 0x22 '"' + { 0x6C, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00 }, + // 0x23 '#' + { 0x28, 0x28, 0xFE, 0x28, 0xFE, 0x28, 0x28, 0x00 }, + // 0x24 '$' + { 0x10, 0x3C, 0x50, 0x38, 0x14, 0x78, 0x10, 0x00 }, + // 0x25 '%' + { 0x62, 0x64, 0x08, 0x10, 0x26, 0x46, 0x00, 0x00 }, + // 0x26 '&' + { 0x30, 0x48, 0x30, 0x54, 0x88, 0x74, 0x00, 0x00 }, + // 0x27 ''' + { 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00 }, + // 0x28 '(' + { 0x04, 0x08, 0x10, 0x10, 0x10, 0x08, 0x04, 0x00 }, + // 0x29 ')' + { 0x20, 0x10, 0x08, 0x08, 0x08, 0x10, 0x20, 0x00 }, + // 0x2A '*' + { 0x00, 0x24, 0x18, 0x7E, 0x18, 0x24, 0x00, 0x00 }, + // 0x2B '+' — repurposed as checkmark ✓ + { 0x00, 0x01, 0x02, 0x04, 0x88, 0x50, 0x20, 0x00 }, + // 0x2C ',' + { 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x08, 0x10 }, + // 0x2D '-' + { 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00 }, + // 0x2E '.' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00 }, + // 0x2F '/' + { 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x00 }, + // 0x30 '0' + { 0x3C, 0x42, 0x46, 0x5A, 0x62, 0x42, 0x3C, 0x00 }, + // 0x31 '1' + { 0x10, 0x30, 0x50, 0x10, 0x10, 0x10, 0x7C, 0x00 }, + // 0x32 '2' + { 0x3C, 0x42, 0x02, 0x0C, 0x30, 0x40, 0x7E, 0x00 }, + // 0x33 '3' + { 0x3C, 0x42, 0x02, 0x1C, 0x02, 0x42, 0x3C, 0x00 }, + // 0x34 '4' + { 0x08, 0x18, 0x28, 0x48, 0xFE, 0x08, 0x08, 0x00 }, + // 0x35 '5' + { 0x7E, 0x40, 0x7C, 0x02, 0x02, 0x42, 0x3C, 0x00 }, + // 0x36 '6' + { 0x1C, 0x20, 0x40, 0x7C, 0x42, 0x42, 0x3C, 0x00 }, + // 0x37 '7' + { 0x7E, 0x02, 0x04, 0x08, 0x10, 0x10, 0x10, 0x00 }, + // 0x38 '8' + { 0x3C, 0x42, 0x42, 0x3C, 0x42, 0x42, 0x3C, 0x00 }, + // 0x39 '9' + { 0x3C, 0x42, 0x42, 0x3E, 0x02, 0x04, 0x38, 0x00 }, + // 0x3A ':' + { 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00 }, + // 0x3B ';' + { 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x08, 0x10 }, + // 0x3C '<' + { 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00 }, + // 0x3D '=' + { 0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00 }, + // 0x3E '>' + { 0x20, 0x10, 0x08, 0x04, 0x08, 0x10, 0x20, 0x00 }, + // 0x3F '?' + { 0x3C, 0x42, 0x02, 0x0C, 0x10, 0x00, 0x10, 0x00 }, + // 0x40 '@' + { 0x3C, 0x42, 0x4E, 0x52, 0x4E, 0x40, 0x3C, 0x00 }, + // 0x41 'A' + { 0x18, 0x24, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x00 }, + // 0x42 'B' + { 0x7C, 0x42, 0x42, 0x7C, 0x42, 0x42, 0x7C, 0x00 }, + // 0x43 'C' + { 0x3C, 0x42, 0x40, 0x40, 0x40, 0x42, 0x3C, 0x00 }, + // 0x44 'D' + { 0x78, 0x44, 0x42, 0x42, 0x42, 0x44, 0x78, 0x00 }, + // 0x45 'E' + { 0x7E, 0x40, 0x40, 0x7C, 0x40, 0x40, 0x7E, 0x00 }, + // 0x46 'F' + { 0x7E, 0x40, 0x40, 0x7C, 0x40, 0x40, 0x40, 0x00 }, + // 0x47 'G' + { 0x3C, 0x42, 0x40, 0x4E, 0x42, 0x42, 0x3C, 0x00 }, + // 0x48 'H' + { 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x00 }, + // 0x49 'I' + { 0x7C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x7C, 0x00 }, + // 0x4A 'J' + { 0x1E, 0x04, 0x04, 0x04, 0x04, 0x44, 0x38, 0x00 }, + // 0x4B 'K' + { 0x42, 0x44, 0x48, 0x70, 0x48, 0x44, 0x42, 0x00 }, + // 0x4C 'L' + { 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7E, 0x00 }, + // 0x4D 'M' + { 0x42, 0x66, 0x5A, 0x5A, 0x42, 0x42, 0x42, 0x00 }, + // 0x4E 'N' + { 0x42, 0x62, 0x52, 0x4A, 0x46, 0x42, 0x42, 0x00 }, + // 0x4F 'O' + { 0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00 }, + // 0x50 'P' + { 0x7C, 0x42, 0x42, 0x7C, 0x40, 0x40, 0x40, 0x00 }, + // 0x51 'Q' + { 0x3C, 0x42, 0x42, 0x42, 0x4A, 0x44, 0x3A, 0x00 }, + // 0x52 'R' + { 0x7C, 0x42, 0x42, 0x7C, 0x48, 0x44, 0x42, 0x00 }, + // 0x53 'S' + { 0x3C, 0x42, 0x40, 0x3C, 0x02, 0x42, 0x3C, 0x00 }, + // 0x54 'T' + { 0xFE, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00 }, + // 0x55 'U' + { 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00 }, + // 0x56 'V' + { 0x42, 0x42, 0x42, 0x42, 0x24, 0x18, 0x18, 0x00 }, + // 0x57 'W' + { 0x42, 0x42, 0x42, 0x5A, 0x5A, 0x66, 0x42, 0x00 }, + // 0x58 'X' + { 0x42, 0x42, 0x24, 0x18, 0x24, 0x42, 0x42, 0x00 }, + // 0x59 'Y' + { 0x44, 0x44, 0x28, 0x10, 0x10, 0x10, 0x10, 0x00 }, + // 0x5A 'Z' + { 0x7E, 0x04, 0x08, 0x10, 0x20, 0x40, 0x7E, 0x00 }, + // 0x5B '[' + { 0x3C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3C, 0x00 }, + // 0x5C '\' + { 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x00, 0x00 }, + // 0x5D ']' + { 0x3C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x3C, 0x00 }, + // 0x5E '^' + { 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00 }, + // 0x5F '_' + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF }, + // 0x60 '`' + { 0x10, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 }, + // 0x61 'a' + { 0x00, 0x00, 0x3C, 0x02, 0x3E, 0x42, 0x3E, 0x00 }, + // 0x62 'b' + { 0x40, 0x40, 0x5C, 0x62, 0x42, 0x62, 0x5C, 0x00 }, + // 0x63 'c' + { 0x00, 0x00, 0x3C, 0x42, 0x40, 0x42, 0x3C, 0x00 }, + // 0x64 'd' + { 0x02, 0x02, 0x3A, 0x46, 0x42, 0x46, 0x3A, 0x00 }, + // 0x65 'e' + { 0x00, 0x00, 0x3C, 0x42, 0x7E, 0x40, 0x3C, 0x00 }, + // 0x66 'f' + { 0x1C, 0x22, 0x20, 0x7C, 0x20, 0x20, 0x20, 0x00 }, + // 0x67 'g' + { 0x00, 0x00, 0x3A, 0x46, 0x42, 0x46, 0x3A, 0x02 }, + // 0x68 'h' + { 0x40, 0x40, 0x5C, 0x62, 0x42, 0x42, 0x42, 0x00 }, + // 0x69 'i' + { 0x18, 0x00, 0x38, 0x08, 0x08, 0x08, 0x3E, 0x00 }, + // 0x6A 'j' + { 0x0C, 0x00, 0x1C, 0x04, 0x04, 0x04, 0x44, 0x38 }, + // 0x6B 'k' + { 0x40, 0x40, 0x44, 0x48, 0x70, 0x48, 0x44, 0x00 }, + // 0x6C 'l' + { 0x38, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00 }, + // 0x6D 'm' + { 0x00, 0x00, 0xFC, 0x92, 0x92, 0x92, 0x92, 0x00 }, + // 0x6E 'n' + { 0x00, 0x00, 0x5C, 0x62, 0x42, 0x42, 0x42, 0x00 }, + // 0x6F 'o' + { 0x00, 0x00, 0x3C, 0x42, 0x42, 0x42, 0x3C, 0x00 }, + // 0x70 'p' + { 0x00, 0x00, 0x5C, 0x62, 0x42, 0x62, 0x5C, 0x40 }, + // 0x71 'q' + { 0x00, 0x00, 0x3A, 0x46, 0x42, 0x46, 0x3A, 0x02 }, + // 0x72 'r' + { 0x00, 0x00, 0x5C, 0x62, 0x40, 0x40, 0x40, 0x00 }, + // 0x73 's' + { 0x00, 0x00, 0x3E, 0x40, 0x3C, 0x02, 0x7C, 0x00 }, + // 0x74 't' + { 0x20, 0x20, 0x7C, 0x20, 0x20, 0x22, 0x1C, 0x00 }, + // 0x75 'u' + { 0x00, 0x00, 0x42, 0x42, 0x42, 0x46, 0x3A, 0x00 }, + // 0x76 'v' + { 0x00, 0x00, 0x44, 0x44, 0x28, 0x28, 0x10, 0x00 }, + // 0x77 'w' + { 0x00, 0x00, 0x82, 0x92, 0x92, 0x92, 0x6C, 0x00 }, + // 0x78 'x' + { 0x00, 0x00, 0x42, 0x24, 0x18, 0x24, 0x42, 0x00 }, + // 0x79 'y' + { 0x00, 0x00, 0x42, 0x42, 0x42, 0x46, 0x3A, 0x02 }, + // 0x7A 'z' + { 0x00, 0x00, 0x7E, 0x04, 0x18, 0x20, 0x7E, 0x00 }, + // 0x7B '{' + { 0x0C, 0x10, 0x10, 0x60, 0x10, 0x10, 0x0C, 0x00 }, + // 0x7C '|' + { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00 }, + // 0x7D '}' + { 0x30, 0x08, 0x08, 0x06, 0x08, 0x08, 0x30, 0x00 }, + // 0x7E '~' + { 0x00, 0x70, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00 }, + // 0xB0 '°' (degree symbol) + { 0x3C, 0x42, 0x42, 0x3C, 0x00, 0x00, 0x00, 0x00 } +}; + +// ─── Ethernet RJ45 Plug Logo (64 × 64 RGB565 color, from eth_logo.png) ──── +// +// Converted from original PNG composited on black background. +// Data is RGB565 big-endian (high byte first), 8192 bytes total. + +#define LOGO_W 64 +#define LOGO_H 64 + +static const uint8_t LOGO_ETH_COLOR[LOGO_W * LOGO_H * 2] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x5B, 0x2D, 0x9D, 0x56, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0x9D, 0x56, 0x5B, 0x2D, 0x08, 0x41, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x21, 0x95, 0x15, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x94, 0xF5, 0x00, 0x21, 0x00, 0x00, + 0x00, 0x00, 0x5B, 0x2D, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x5B, 0x2D, 0x00, 0x00, + 0x00, 0x00, 0x9D, 0x56, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x9D, 0x36, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x3B, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xBE, 0x3B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x8C, 0xB5, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x8C, 0xB5, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x5A, 0x8B, 0xDD, 0x49, 0xBC, 0x8A, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xBC, 0x8A, 0xDD, 0x49, 0x5A, 0x8B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x83, 0x4B, 0xE5, 0x49, 0xA3, 0xEA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x39, 0xEB, 0xD5, 0x09, + 0xD5, 0x09, 0x39, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xA3, 0xEA, 0xE5, 0x49, 0x83, 0x4B, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x5A, 0x8B, 0xDD, 0x49, 0xBC, 0x8A, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xBC, 0x8A, 0xDD, 0x49, 0x5A, 0x8B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xAC, 0x0A, 0xED, 0x89, 0xC4, 0xAA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x4B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0xAA, 0xED, 0x89, 0xAC, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xAC, 0x0A, 0xED, 0x89, 0xC4, 0xAA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x4B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0xAA, 0xED, 0x89, 0xAC, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xAC, 0x0A, 0xED, 0x89, 0xC4, 0xAA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x4B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0xAA, 0xED, 0x89, 0xAC, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xAC, 0x0A, 0xED, 0x89, 0xC4, 0xAA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x4B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0xAA, 0xED, 0x89, 0xAC, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xAC, 0x0A, 0xED, 0x89, 0xC4, 0xAA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x4B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0xAA, 0xED, 0x89, 0xAC, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xAC, 0x0A, 0xED, 0x89, 0xC4, 0xAA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x4B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0xAA, 0xED, 0x89, 0xAC, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xAC, 0x0A, 0xED, 0x89, 0xC4, 0xAA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x4B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0xAA, 0xED, 0x89, 0xAC, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xAC, 0x0A, 0xED, 0x89, 0xC4, 0xAA, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x4B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0xAA, 0xED, 0x89, 0xAC, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x4B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x4B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x2B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x2B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xA4, 0x0A, 0xED, 0x89, 0xC4, 0x8A, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x4A, 0x2B, 0xED, 0x89, + 0xED, 0x89, 0x4A, 0x2B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xC4, 0x8A, 0xED, 0x89, 0xA4, 0x0A, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x83, 0x2B, 0xED, 0x89, 0xDD, 0x29, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0xDD, 0x29, 0xED, 0x89, 0x83, 0x2B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x42, 0x2B, 0xB4, 0x6A, 0x93, 0xAA, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x93, 0xAA, 0xB4, 0x6A, 0x42, 0x2B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x62, 0x8B, 0xBC, 0x8A, 0x7B, 0x2B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0xAC, 0x0A, + 0xAC, 0x0A, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x7B, 0x2B, 0xBC, 0x8A, 0x62, 0x8B, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x42, 0x2B, 0xB4, 0x6A, 0x93, 0xAA, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x93, 0xAA, 0xB4, 0x6A, 0x42, 0x2B, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x74, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x74, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x84, 0x94, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x84, 0x94, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xB6, 0x1A, 0x94, 0xF5, + 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, 0x8C, 0xD5, + 0x94, 0xF5, 0xB6, 0x1A, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0x7C, 0x33, 0x5B, 0x0F, 0x5B, 0x0F, 0x5B, 0x0F, 0x42, 0x4C, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x42, 0x4C, 0x5B, 0x0F, 0x5B, 0x0F, 0x5B, 0x0F, 0x7C, 0x33, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x6B, 0xB1, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x6B, 0xB1, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x6B, 0xB1, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x6B, 0xB1, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x6B, 0xB1, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x6B, 0xB1, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x6B, 0xB1, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x6B, 0xB1, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x6B, 0xB1, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x6B, 0xB1, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x7C, 0x13, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, + 0x31, 0xEB, 0x31, 0xEB, 0x31, 0xEB, 0x7C, 0x13, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xB6, 0x3A, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, + 0xA5, 0x98, 0xA5, 0x98, 0xA5, 0x98, 0xB6, 0x3A, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0xAD, 0xD9, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xAD, 0xD9, 0x00, 0x00, + 0x00, 0x00, 0x9D, 0x56, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x9D, 0x36, 0x00, 0x00, + 0x00, 0x00, 0x5B, 0x2D, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x5B, 0x2D, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x21, 0x94, 0xF5, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, + 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0xBE, 0x5B, 0x94, 0xF5, 0x00, 0x21, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x5B, 0x2D, 0x9D, 0x36, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, + 0xAD, 0xD9, 0xAD, 0xD9, 0xAD, 0xD9, 0x9D, 0x36, 0x5B, 0x2D, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +// Total: 8192 bytes + + +// ─── Byte swapping ────────────────────────────────────────────────────────── +// The ST7789 RAMCTRL is configured for big-endian pixel data (high byte first). +// On the little-endian ESP32-S3, a uint16_t value like 0x07E0 (GREEN) is stored +// as bytes [0xE0, 0x07] in memory. When passed to esp_lcd_panel_draw_bitmap(), +// these bytes are sent directly over SPI, but the display in big-endian mode +// interprets the first byte as high byte, giving 0xE007 = dark red instead of +// green. The PIX() macro pre-swaps bytes so the display receives correct values. +#define PIX(v) __builtin_bswap16(v) + +// ─── Low-level helpers ────────────────────────────────────────────────────── + +/// Fill a rectangular region with a single 16-bit color, one row at a time. +static void disp_fill_region(int x0, int y0, int x1, int y1, uint16_t color) +{ + int width = x1 - x0 + 1; + uint16_t row_buf[240]; + uint16_t swapped = PIX(color); + for (int i = 0; i < width; i++) { + row_buf[i] = swapped; + } + for (int y = y0; y <= y1; y++) { + esp_lcd_panel_draw_bitmap(s_panel_handle, x0, y, x1 + 1, y + 1, row_buf); + } +} + +/// Draw a single horizontal line from (x0, y) to (x1, y). +static void disp_draw_hspan(int x0, int y, int x1, uint16_t color) +{ + int width = x1 - x0 + 1; + uint16_t row[240]; + uint16_t swapped = PIX(color); + for (int i = 0; i < width; i++) { + row[i] = swapped; + } + esp_lcd_panel_draw_bitmap(s_panel_handle, x0, y, x0 + width, y + 1, row); +} + +// ─── Public API ────────────────────────────────────────────────────────────── + +void display_init(void) +{ + ESP_LOGI(TAG, "Display init: MOSI=41 CLK=40 CS=39 DC=38 RST=42 BLK_EN=4 BL_PWM=20 on SPI3_HOST"); + + // --- Backlight enable (GPIO4, active HIGH, with pull-up) --- + gpio_config_t gpio_conf = {}; + gpio_conf.intr_type = GPIO_INTR_DISABLE; + gpio_conf.mode = GPIO_MODE_OUTPUT; + gpio_conf.pin_bit_mask = ((uint64_t)0X01 << PIN_LCD_RST) | ((uint64_t)0X01 << PIN_LCD_BLK); + gpio_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_conf.pull_up_en = GPIO_PULLUP_ENABLE; + ESP_ERROR_CHECK_WITHOUT_ABORT(gpio_config(&gpio_conf)); + gpio_set_level(PIN_LCD_BLK, 1); + + // --- Backlight PWM (GPIO20, 30 kHz, 8-bit, ~50% duty) --- + ledc_timer_config_t timer_conf = {}; + timer_conf.speed_mode = LEDC_LOW_SPEED_MODE; + timer_conf.duty_resolution = LEDC_TIMER_8_BIT; + timer_conf.timer_num = LEDC_TIMER_3; + timer_conf.freq_hz = 30 * 1000; + timer_conf.clk_cfg = LEDC_USE_RC_FAST_CLK; + ESP_ERROR_CHECK_WITHOUT_ABORT(ledc_timer_config(&timer_conf)); + + ledc_channel_config_t ledc_conf = {}; + ledc_conf.gpio_num = PIN_LCD_BL_PWM; + ledc_conf.speed_mode = LEDC_LOW_SPEED_MODE; + ledc_conf.channel = LEDC_CHANNEL_1; + ledc_conf.intr_type = LEDC_INTR_DISABLE; + ledc_conf.timer_sel = LEDC_TIMER_3; + ledc_conf.duty = 128; + ledc_conf.hpoint = 0; + ledc_conf.flags.output_invert = 0; + ESP_ERROR_CHECK_WITHOUT_ABORT(ledc_channel_config(&ledc_conf)); + ESP_LOGI(TAG, "Backlight PWM on GPIO20: duty=%d/255 (%d%%)", ledc_conf.duty, + (ledc_conf.duty * 100) / 255); + + // --- Initialize the LCD SPI bus (SPI3_HOST, dedicated to LCD) --- + // This is a separate bus from the W5500 Ethernet (which uses SPI2_HOST). + // Only MOSI and CLK are needed (display write-only). + spi_bus_config_t buscfg; + memset(&buscfg, 0, sizeof(buscfg)); + buscfg.mosi_io_num = PIN_LCD_MOSI; + buscfg.miso_io_num = -1; // No MISO (display write-only) + buscfg.sclk_io_num = PIN_LCD_CLK; + buscfg.quadwp_io_num = -1; + buscfg.quadhd_io_num = -1; + buscfg.max_transfer_sz = 240 * 2; // 1 row of RGB565 pixels + + ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO)); + + // --- Create SPI panel IO --- + esp_lcd_panel_io_spi_config_t io_config; + memset(&io_config, 0, sizeof(io_config)); + io_config.cs_gpio_num = PIN_LCD_CS; + io_config.dc_gpio_num = PIN_LCD_DC; + io_config.spi_mode = 0; + io_config.pclk_hz = 40 * 1000 * 1000; // 40 MHz + io_config.trans_queue_depth = 4; + io_config.lcd_cmd_bits = 8; + io_config.lcd_param_bits = 8; + + ESP_ERROR_CHECK( + esp_lcd_new_panel_io_spi(LCD_SPI_HOST, &io_config, &s_io_handle) + ); + + // --- Create ST7789 panel --- + esp_lcd_panel_dev_config_t panel_config; + memset(&panel_config, 0, sizeof(panel_config)); + panel_config.reset_gpio_num = PIN_LCD_RST; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG; + + ESP_ERROR_CHECK( + esp_lcd_new_panel_st7789(s_io_handle, &panel_config, &s_panel_handle) + ); + + // --- Reset & init --- + ESP_ERROR_CHECK(esp_lcd_panel_reset(s_panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_init(s_panel_handle)); + + // Turn display on + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(s_panel_handle, true)); + + // Enable INVON (color inversion). The Waveshare panel has a reversed + // polarizer that requires data inversion. INVON + the panel's physical + // inversion cancel out so RGB565 data displays with correct colors. + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(s_panel_handle, true)); + + // Fill with black + display_fill(COLOR_BLACK); + + // Quick self-test: flash red, green, then black + ESP_LOGI(TAG, "Self-test: RED"); + display_fill(COLOR_RED); + vTaskDelay(pdMS_TO_TICKS(300)); + + ESP_LOGI(TAG, "Self-test: GREEN"); + display_fill(COLOR_GREEN); + vTaskDelay(pdMS_TO_TICKS(300)); + + ESP_LOGI(TAG, "Self-test: BLACK"); + display_fill(COLOR_BLACK); + + ESP_LOGI(TAG, "Display initialized successfully"); +} + +void display_fill(uint16_t color) +{ + disp_fill_region(0, 0, DISP_WIDTH - 1, DISP_HEIGHT - 1, color); +} + +void display_pixel(int x, int y, uint16_t color) +{ + if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT) { + return; + } + uint16_t pixel = PIX(color); + esp_lcd_panel_draw_bitmap(s_panel_handle, x, y, x + 1, y + 1, &pixel); +} + +void display_text(int x, int y, const char *text, uint16_t fg, uint16_t bg, int scale) +{ + if (!text || scale < 1) return; + + while (*text) { + char c = *text; + int idx; + if (c >= 32 && c <= 126) { + idx = c - 32; + } else if ((unsigned char)c == 176) { + idx = 95; // degree symbol + } else { + idx = 0; + } + + const uint8_t *glyph = FONT8x8[idx]; + int char_w = 8 * scale; + + // Skip characters entirely left of the display + int char_end = x + char_w; + if (char_end < 0) { + x += char_w; + text++; + continue; + } + if (x >= DISP_WIDTH) { + // Past right edge, nothing more will fit + break; + } + + for (int row = 0; row < 8; row++) { + uint8_t bits = glyph[row]; + // Repeat each source row 'scale' times vertically + for (int rep = 0; rep < scale; rep++) { + int py = y + row * scale + rep; + if (py < 0 || py >= DISP_HEIGHT) continue; + + // Build one output row: each source bit becomes 'scale' pixels wide + uint16_t pixel_row[8 * 4]; // max scale=4, width=32 + for (int col = 0; col < 8; col++) { + uint16_t color = (bits & (1 << (7 - col))) ? fg : bg; + uint16_t swapped = PIX(color); + for (int s = 0; s < scale; s++) { + pixel_row[col * scale + s] = swapped; + } + } + // Clip the row to display width + int draw_w = char_w; + if (x + draw_w > DISP_WIDTH) { + draw_w = DISP_WIDTH - x; + } + esp_lcd_panel_draw_bitmap(s_panel_handle, x, py, x + draw_w, py + 1, pixel_row); + } + } + + x += char_w; + text++; + } +} + +void display_rect(int x0, int y0, int x1, int y1, uint16_t color) +{ + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 >= DISP_WIDTH) x1 = DISP_WIDTH - 1; + if (y1 >= DISP_HEIGHT) y1 = DISP_HEIGHT - 1; + if (x0 > x1 || y0 > y1) return; + + disp_fill_region(x0, y0, x1, y1, color); +} + +void display_dot_online(int x, int y, bool online) +{ + uint16_t color = online ? COLOR_GREEN : COLOR_RED; + + int cx = x + 8; + int cy = y + 8; + int r = 7; + + for (int dy = -r; dy <= r; dy++) { + float dx_f = sqrtf((float)(r * r - dy * dy)); + int dx = (int)(0.5f + dx_f); + int x1 = cx - dx; + int x2 = cx + dx; + if (x1 < 0) x1 = 0; + if (x2 >= DISP_WIDTH) x2 = DISP_WIDTH - 1; + int py = cy + dy; + if (py < 0 || py >= DISP_HEIGHT) continue; + disp_draw_hspan(x1, py, x2, color); + } +} + +void display_splash(const char *title) +{ + display_fill(COLOR_BLACK); + + // Color Ethernet RJ45 plug logo (64x64 RGB565), drawn row by row + int logo_x = (DISP_WIDTH - LOGO_W) / 2; + const int row_bytes = LOGO_W * 2; // 128 bytes per row + for (int row = 0; row < LOGO_H; row++) { + const uint8_t *row_data = &LOGO_ETH_COLOR[row * row_bytes]; + // Data is already in big-endian RGB565 format + esp_lcd_panel_draw_bitmap(s_panel_handle, + logo_x, 8 + row, + logo_x + LOGO_W, 8 + row + 1, + (const void *)row_data); + } + + // Horizontal line below logo + display_rect(30, 82, DISP_WIDTH - 30, 83, COLOR_GREEN); + + // Title centered below line + int tx = (DISP_WIDTH - (int)strlen(title) * 16) / 2; + display_text(tx, 100, title, COLOR_CYAN, COLOR_BLACK, 2); +} + +void display_booting(bool link_done, bool dhcp_done, bool slaac_done) +{ + display_fill(COLOR_BLACK); + + // ── Header: centered "BOOTING" ── + { + char hdr[] = "BOOTING"; + int hdr_w = strlen(hdr) * 16; + int hdr_x = (DISP_WIDTH - hdr_w) / 2; + if (hdr_x < 0) hdr_x = 0; + display_text(hdr_x, 4, hdr, COLOR_CYAN, COLOR_BLACK, 2); + } + + // ── Green underline ── + display_rect(40, 28, DISP_WIDTH - 40, 29, COLOR_GREEN); + + // ── [✓] Link ── + { + char line[16]; + snprintf(line, sizeof(line), "[%c] Link", link_done ? '+' : ' '); + uint16_t c = link_done ? COLOR_GREEN : COLOR_YELLOW; + display_text(20, 48, line, c, COLOR_BLACK, 2); + } + + // ── [✓] DHCP ── + { + char line[16]; + snprintf(line, sizeof(line), "[%c] DHCP", dhcp_done ? '+' : ' '); + uint16_t c = dhcp_done ? COLOR_GREEN : COLOR_YELLOW; + display_text(20, 72, line, c, COLOR_BLACK, 2); + } + + // ── [✓] SLAAC ── + { + char line[16]; + snprintf(line, sizeof(line), "[%c] SLAAC", slaac_done ? '+' : ' '); + uint16_t c = slaac_done ? COLOR_GREEN : COLOR_YELLOW; + display_text(20, 96, line, c, COLOR_BLACK, 2); + } +} + +void display_status(const struct test_results_t *results) +{ + display_fill(COLOR_BLACK); + + // Scale=2: 16x16px per char, 15 chars per line, line height=20px + // Scale=1: 8x8px per char, 30 chars per line, line height=12px + // We use scale=2 for headers/stats and scale=1 for IPv6 data (long addresses) + + // Track current Y position + int y = 0; + + // ── Line 0: Dot + centered ONLINE/OFFLINE ── + { + const char *status_label = results->link_up ? "ONLINE" : "OFFLINE"; + uint16_t status_color = results->link_up ? COLOR_GREEN : COLOR_RED; + int label_w = strlen(status_label) * 16; + int combined_w = 16 + 4 + label_w; // dot(16) + gap(4) + text + int start_x = (DISP_WIDTH - combined_w) / 2; + if (start_x < 0) start_x = 0; + display_dot_online(start_x, y, results->link_up); + display_text(start_x + 20, y, status_label, status_color, COLOR_BLACK, 2); + // Thin cyan separator line under the status label + y += 20; + display_rect(40, y - 2, DISP_WIDTH - 40, y - 1, COLOR_CYAN); + y += 3; // 3px margin below the line + } + + // ── Line 1: Latency + Loss: "Lat 50 Los 0" (15 chars = 240px) ── + { + char buf[24]; + int lat = results->latency_ms; + float loss = results->packet_loss_pct; + + if (lat >= 0) { + // Clamp to 9999 + if (lat > 9999) lat = 9999; + if (loss >= 0.0f && loss <= 99.0f) { + snprintf(buf, sizeof(buf), "Lat %4d Los %2.0f", lat, (double)loss); + } else if (loss >= 0.0f) { + snprintf(buf, sizeof(buf), "Lat %4d Los --", lat); + } else { + snprintf(buf, sizeof(buf), "Lat %4d Los --", lat); + } + } else { + if (loss >= 0.0f && loss <= 99.0f) { + snprintf(buf, sizeof(buf), "Lat -- Los %2.0f", (double)loss); + } else { + snprintf(buf, sizeof(buf), "Lat -- Los --"); + } + } + // 15 chars max: "Lat 50 Los 0" = 15 + uint16_t c; + if (results->latency_v4_ok && results->latency_v6_ok) { + c = COLOR_GREEN; + } else if (results->latency_v4_ok || results->latency_v6_ok) { + c = COLOR_YELLOW; + } else { + c = COLOR_RED; + } + display_text(0, y, buf, c, COLOR_BLACK, 2); + y += 20; + } + + // ── Line 2: Temperature ── + { + char buf[24]; + if (results->temperature_c > -273.0f) { + float t = results->temperature_c; + if (t < -99.0f) t = -99.0f; + if (t > 999.0f) t = 999.0f; + snprintf(buf, sizeof(buf), "Temp %3.0f \260C", (double)t); + } else { + snprintf(buf, sizeof(buf), "Temp --"); + } + display_text(0, y, buf, COLOR_WHITE, COLOR_BLACK, 2); + y += 20; + } + + // ── Line 3: == IPv4 == section header ── + { + char hdr[] = "== IPv4 =="; + int hdr_w = strlen(hdr) * 16; + int hdr_x = (DISP_WIDTH - hdr_w) / 2; + if (hdr_x < 0) hdr_x = 0; + display_text(hdr_x, y, hdr, COLOR_CYAN, COLOR_BLACK, 2); + y += 20; + } + + // ── Line 4: IPv4 address ── + { + const char *ip = results->ipv4 ? results->ipv4 : "--"; + display_text(0, y, ip, results->has_ipv4 ? COLOR_WHITE : COLOR_YELLOW, COLOR_BLACK, 2); + y += 20; + } + + // ── Line 5: IPv4 gateway ── + { + const char *gw = results->gateway_ip ? results->gateway_ip : "--"; + display_text(0, y, gw, results->has_ipv4 ? COLOR_WHITE : COLOR_YELLOW, COLOR_BLACK, 2); + y += 20; + } + + // ── Line 6: == IPv6 == section header ── + { + char hdr[] = "== IPv6 =="; + int hdr_w = strlen(hdr) * 16; + int hdr_x = (DISP_WIDTH - hdr_w) / 2; + if (hdr_x < 0) hdr_x = 0; + display_text(hdr_x, y, hdr, COLOR_CYAN, COLOR_BLACK, 2); + y += 20; + } + + // ── Line 7: IPv6 link-local address (fe80::..., scale=1) ── + { + const char *ll = results->ipv6_ll ? results->ipv6_ll : "--"; + bool has_v6 = (results->has_ipv6 && results->ipv6_ll && results->ipv6_ll[0] != '-'); + display_text(0, y, ll, has_v6 ? COLOR_WHITE : COLOR_YELLOW, COLOR_BLACK, 1); + y += 12; + } + + // ── Line 8: IPv6 global address (2xxx::..., scale=1, split if needed) ── + { + const char *gua = results->ipv6_gua ? results->ipv6_gua : "--"; + bool has_gua = (results->has_ipv6 && results->ipv6_gua && results->ipv6_gua[0] != '-'); + + // Determine how many segments fit on one line (max 30 chars at scale=1) + // Each segment: up to 4 hex chars + 1 colon separator + // Split only if the address overflows the display width + int gua_len = (int)strlen(gua); + const int max_line = 30; // max chars at scale=1 (240px / 8) + + if (has_gua && gua_len > max_line) { + // Find break point: place as many complete segments as fit + int last_colon = 0; + for (int i = 0; gua[i]; i++) { + if (gua[i] == ':') { + if (i + 1 <= max_line) { + last_colon = i; // this colon fits + } else { + break; // i is past max_line, use previous colon + } + } + } + + if (last_colon > 0) { + // Print first part up to and including last_colon + char line1[32]; + int len1 = last_colon; // includes the trailing colon + if (len1 > (int)sizeof(line1) - 1) len1 = sizeof(line1) - 1; + memcpy(line1, gua, len1); + line1[len1] = '\0'; + display_text(0, y, line1, COLOR_WHITE, COLOR_BLACK, 1); + y += 12; + // Second part: everything after the colon + const char *part2 = gua + last_colon + 1; + display_text(0, y, part2, COLOR_WHITE, COLOR_BLACK, 1); + } else { + display_text(0, y, gua, COLOR_WHITE, COLOR_BLACK, 1); + } + } else { + display_text(0, y, gua, has_gua ? COLOR_WHITE : COLOR_YELLOW, COLOR_BLACK, 1); + } + y += 12; + } + + // ── Line 9: IPv6 gateway (router's link-local, scale=1) ── + { + const char *gw6 = results->gateway6_ip ? results->gateway6_ip : "--"; + bool has_gw6 = (results->has_ipv6 && results->gateway6_ip && results->gateway6_ip[0] != '-'); + display_text(0, y, gw6, has_gw6 ? COLOR_WHITE : COLOR_YELLOW, COLOR_BLACK, 1); + y += 12; + } + + // ── Line 10: DNS status (scale=2) ── + { + const char *status; + uint16_t c; + if (results->dns4_ok && results->dns6_ok) { + // Both IPv4 and IPv6 DNS servers returned records + status = "DNS OK"; + c = COLOR_GREEN; + } else if (results->dns4_ok && !results->dns6_ok) { + // Only IPv4 DNS worked + status = "DNS v4 OK"; + c = COLOR_YELLOW; + } else if (!results->dns4_ok && results->dns6_ok) { + // Only IPv6 DNS worked + status = "DNS v6 OK"; + c = COLOR_YELLOW; + } else if (results->dns_timeout) { + status = "DNS FAIL"; + c = COLOR_RED; + } else { + status = "DNS --"; + c = COLOR_YELLOW; + } + display_text(0, y, status, c, COLOR_BLACK, 2); + y += 20; + } + + // ── Line 11: RS25 site-to-site VPN status (scale=2) ── + { + const char *txt; + uint16_t c; + if (results->vpn_reachable) { + txt = "RS25 S2S OK"; + c = COLOR_GREEN; + } else { + txt = "RS25 S2S --"; + c = COLOR_RED; + } + display_text(0, y, txt, c, COLOR_BLACK, 2); + } +} + +// ─── Progress screen ──────────────────────────────────────────────────────── + +// ─── Progress display state (for spinner-only updates) ────────────────────── + +static int s_spinner_x = 0; +static int s_spinner_y = 0; + +// ─── Test progress screen ─────────────────────────────────────────────────── + +void display_progress(const struct test_results_t *results, + int test_index, int frame) +{ + display_fill(COLOR_BLACK); + + // Top line: spinner + "Test: TestName" + const char *spinner = "|/-\\"; + char spin_char[2] = { spinner[frame & 3], '\0' }; + + display_text(0, 0, spin_char, COLOR_GREEN, COLOR_BLACK, 2); + + char running[32]; + // test_index is the test just completed; show the next one as running + int next_idx = test_index + 1; + if (next_idx < 0) next_idx = 0; + if (next_idx >= DIAG_TEST_COUNT) { + // All tests done — keep showing the last one + next_idx = DIAG_TEST_COUNT - 1; + } + const char *test_name = g_diag_test_names[next_idx]; + snprintf(running, sizeof(running), " Test: %s", test_name); + display_text(16, 0, running, COLOR_WHITE, COLOR_BLACK, 2); + + // Completed tests with checkmark prefix + int y = 28; + for (int i = 0; i < results->tests_completed && i < DIAG_TEST_COUNT; i++) { + char line[34]; + snprintf(line, sizeof(line), "+ %s", g_diag_test_names[i]); + display_text(0, y, line, COLOR_GREEN, COLOR_BLACK, 2); + y += 20; + } + + // Save spinner position for partial updates + s_spinner_x = 0; + s_spinner_y = 0; +} + +void display_update_spinner(int frame) +{ + const char *spinner = "|/-\\"; + char spin_char[2] = { spinner[frame & 3], '\0' }; + // Clear the 16x16 area where the spinner sits, then redraw + display_rect(s_spinner_x, s_spinner_y, s_spinner_x + 15, s_spinner_y + 15, COLOR_BLACK); + display_text(s_spinner_x, s_spinner_y, spin_char, COLOR_GREEN, COLOR_BLACK, 2); +} + +// ─── Failure screen ───────────────────────────────────────────────────────── + +void display_failure(const struct test_results_t *results) +{ + display_fill(COLOR_BLACK); + + // Big red FAIL header + display_text(0, 0, "TEST FAILED", COLOR_RED, COLOR_BLACK, 2); + + // Draw a red underline + display_rect(0, 20, DISP_WIDTH - 1, 21, COLOR_RED); + + int y = 32; + + // Show failed tests + if (!results->link_up) { + display_text(0, y, "Link: DOWN", COLOR_RED, COLOR_BLACK, 2); + y += 24; + } + if (!results->has_ipv4) { + display_text(0, y, "IPv4: none", COLOR_RED, COLOR_BLACK, 2); + y += 24; + } + if (!results->gateway_reachable_v4) { + display_text(0, y, "Gateway: unreachable", COLOR_RED, COLOR_BLACK, 2); + y += 24; + } + if (!results->uplink_reachable_v4) { + display_text(0, y, "Uplink IPv4: unreachable", COLOR_RED, COLOR_BLACK, 2); + y += 24; + } + if (!results->uplink_reachable_v6) { + display_text(0, y, "Uplink IPv6: unreachable", COLOR_RED, COLOR_BLACK, 2); + y += 24; + } + if (!results->dns_ok) { + display_text(0, y, "DNS: failed", COLOR_RED, COLOR_BLACK, 2); + y += 24; + } + + if (y < DISP_HEIGHT - 48) { + display_text(0, DISP_HEIGHT - 40, "Retrying in 30s...", COLOR_YELLOW, COLOR_BLACK, 2); + } +} diff --git a/firmware/main/display.h b/firmware/main/display.h new file mode 100644 index 0000000..cce8e51 --- /dev/null +++ b/firmware/main/display.h @@ -0,0 +1,117 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#include +#include + +#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 diff --git a/firmware/main/idf_component.yml b/firmware/main/idf_component.yml index f4ee2b3..0dc0ad9 100644 --- a/firmware/main/idf_component.yml +++ b/firmware/main/idf_component.yml @@ -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" diff --git a/firmware/main/inet_mon_main.cpp b/firmware/main/inet_mon_main.cpp index 00df4f4..aff2c83 100644 --- a/firmware/main/inet_mon_main.cpp +++ b/firmware/main/inet_mon_main.cpp @@ -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 #include #include @@ -7,225 +21,267 @@ #include "esp_chip_info.h" #include "esp_flash.h" #include "esp_system.h" - #include "esp_log.h" -#include "esp_event.h" -#include "esp_netif.h" #include "nvs_flash.h" - -#include "driver/spi_master.h" #include "driver/gpio.h" -#include "esp_eth.h" -#include "esp_eth_mac.h" -#include "esp_eth_phy.h" -#include "esp_eth_com.h" - -#include "esp_eth_mac_w5500.h" -#include "esp_eth_phy_w5500.h" +#include "display.h" +#include "network.h" +#include "diagnostics.h" +#include "temperature.h" static const char *TAG = "INETMON"; -// -------------------- PIN CONFIG -------------------- -#define PIN_SCK 12 -#define PIN_MOSI 11 -#define PIN_MISO 13 -#define PIN_CS 10 -#define PIN_RST 9 -#define PIN_INT 14 +// ─── Animation ────────────────────────────────────────────────────────────── -static esp_eth_handle_t eth_handle = NULL; +static int s_anim_frame = 0; +// ─── Helper: wait with display update ─────────────────────────────────────── -// -------------------- EVENT HANDLER -------------------- -static void eth_event_handler(void *arg, esp_event_base_t event_base, - int32_t event_id, void *event_data) +// ─── Boot wait helpers ───────────────────────────────────────────────────── + +/// Wait up to `timeout_sec` seconds for Ethernet link, showing booting screen. +/// Returns true once link is up. +static bool wait_for_link(int timeout_sec) { - switch (event_id) { - - case ETHERNET_EVENT_CONNECTED: - ESP_LOGI(TAG, "Ethernet LINK UP"); - break; - - case ETHERNET_EVENT_DISCONNECTED: - ESP_LOGI(TAG, "Ethernet LINK DOWN"); - break; - - case ETHERNET_EVENT_START: - ESP_LOGI(TAG, "Ethernet START"); - break; - - case ETHERNET_EVENT_STOP: - ESP_LOGI(TAG, "Ethernet STOP"); - break; - - default: - break; + display_booting(false, false, false); // initial draw + for (int i = 0; i < timeout_sec; i++) { + if (network_is_link_up()) return true; + vTaskDelay(pdMS_TO_TICKS(1000)); } + return network_is_link_up(); } -static void got_ip_event_handler(void *arg, esp_event_base_t event_base, - int32_t event_id, void *event_data) +/// Wait up to `timeout_sec` seconds for DHCP IP, showing booting screen. +/// link_done must be true (link must already be up). +/// Returns true once DHCP gives us an IPv4 address. +static bool wait_for_dhcp(int timeout_sec) { - ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; - - ESP_LOGI( - TAG, - "IP: " IPSTR " GW: " IPSTR " MASK: " IPSTR, - IP2STR(&event->ip_info.ip), - IP2STR(&event->ip_info.gw), - IP2STR(&event->ip_info.netmask) - ); + display_booting(true, false, false); // initial draw + for (int i = 0; i < timeout_sec; i++) { + if (network_has_ipv4()) return true; + vTaskDelay(pdMS_TO_TICKS(1000)); + } + return network_has_ipv4(); } +/// Wait up to `timeout_sec` seconds for a global IPv6 address (GUA). +/// Shows the booting screen with [X] link, [X] dhcp, and [ ]/[X] slaac. +/// Returns true once a GUA is assigned, false on timeout. +static bool wait_for_slaac(int timeout_sec) +{ + // If already assigned, skip the boot screen entirely + if (network_has_ipv6_gua()) return true; + + display_booting(true, true, false); // initial draw ([ ] SLAAC) + int timeout_ms = timeout_sec * 1000; + int elapsed = 0; + while (elapsed < timeout_ms) { + if (network_has_ipv6_gua()) { + display_booting(true, true, true); + vTaskDelay(pdMS_TO_TICKS(250)); // let user see the green [X] + return true; + } + vTaskDelay(pdMS_TO_TICKS(100)); + elapsed += 100; + } + bool got = network_has_ipv6_gua(); + display_booting(true, true, got); + return got; +} + +// ─── app_main ─────────────────────────────────────────────────────────────── + extern "C" void app_main(void) { - printf("Hello world!\n"); + printf("Hello world! Internet Monitor v0.1\n"); - /* Print chip information */ + // ── 1. Print chip info ── esp_chip_info_t chip_info; uint32_t flash_size; esp_chip_info(&chip_info); printf("This is %s chip with %d CPU core(s), %s%s%s%s, ", - CONFIG_IDF_TARGET, - chip_info.cores, + CONFIG_IDF_TARGET, chip_info.cores, (chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "", (chip_info.features & CHIP_FEATURE_BT) ? "BT" : "", (chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "", - (chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "" - ); + (chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : ""); unsigned major_rev = chip_info.revision / 100; unsigned minor_rev = chip_info.revision % 100; printf("silicon revision v%d.%d, ", major_rev, minor_rev); - if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) { + + if (esp_flash_get_size(NULL, &flash_size) != ESP_OK) { printf("Get flash size failed"); return; } - printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024), (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external"); - printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size()); - fflush(stdout); - ESP_LOGI(TAG, "Starting W5500 Ethernet..."); - - // NVS + event loop + // ── 2. Initialize system services ── ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); - - esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ð_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"); - // printf("Restarting now.\n"); - // esp_restart(); + 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 + } + } + + // 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..."); + } } diff --git a/firmware/main/network.cpp b/firmware/main/network.cpp new file mode 100644 index 0000000..df64067 --- /dev/null +++ b/firmware/main/network.cpp @@ -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 +#include +#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); +} diff --git a/firmware/main/network.h b/firmware/main/network.h new file mode 100644 index 0000000..9a663e0 --- /dev/null +++ b/firmware/main/network.h @@ -0,0 +1,93 @@ +#ifndef NETWORK_H +#define NETWORK_H + +#include +#include +#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 diff --git a/firmware/main/temperature.cpp b/firmware/main/temperature.cpp new file mode 100644 index 0000000..213dd99 --- /dev/null +++ b/firmware/main/temperature.cpp @@ -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 +#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; +} diff --git a/firmware/main/temperature.h b/firmware/main/temperature.h new file mode 100644 index 0000000..02e24ef --- /dev/null +++ b/firmware/main/temperature.h @@ -0,0 +1,24 @@ +#ifndef TEMPERATURE_H +#define TEMPERATURE_H + +#include +#include + +#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 diff --git a/firmware/sdkconfig b/firmware/sdkconfig index ce50b84..e3e62d2 100644 --- a/firmware/sdkconfig +++ b/firmware/sdkconfig @@ -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: diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults new file mode 100644 index 0000000..70a41da --- /dev/null +++ b/firmware/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_LWIP_RAW=y +CONFIG_LWIP_IPV6_RDNSS_MAX_DNS_SERVERS=2