In Part 1, we took an ESP32-C3 SuperMini and an SSD1306 OLED display to create a standalone network gauge that collects Wi-Fi parameters and checks WAN reachability. While having a tiny screen on your desk is convenient, an isolated gadget is only half the story.
The real power of an IoT device unlocks when its telemetry is liberated from the breadboard.
By turning the ESP32-C3 into an ultra-lightweight REST microservice, any browser, terminal, or full-stack dashboard on the local area network can query its status in real time. However, moving from an offline microcontroller to an HTTP server introduces modern networking challenges: defining structured JSON schemas within memory constraints, overcoming browser security barriers like Cross-Origin Resource Sharing (CORS), and navigating the quirks of zero-configuration local service discovery (mDNS).
1. Designing an Embedded REST Endpoint
The goal is simple: whenever an HTTP client sends a GET request to /api/status, the ESP32 should return an instantaneous, machine-readable snapshot of the local network state.
The ESP32 Arduino core includes a lightweight WebServer library that runs comfortably on port 80. Instead of constructing heavy object trees in memory, we construct a compact JSON payload directly from our live diagnostic cache:
#include <WebServer.h> WebServer server(80); void handleGetStatus() { // Inject CORS headers so web clients aren't blocked by the browser server.sendHeader("Access-Control-Allow-Origin", "*"); server.sendHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); server.sendHeader("Access-Control-Allow-Headers", "*"); // Build the JSON telemetry string String json = "{"; json += "\"ssid\":\"" + WiFi.SSID() + "\","; json += "\"ip\":\"" + WiFi.localIP().toString() + "\","; json += "\"mask\":\"" + WiFi.subnetMask().toString() + "\","; json += "\"gateway\":\"" + WiFi.gatewayIP().toString() + "\","; json += "\"dns\":\"" + WiFi.dnsIP().toString() + "\","; json += "\"rssi\":" + String(currentRssi) + ","; json += "\"signal\":\"" + currentSignal + "\","; json += "\"dnsOk\":" + String(dnsStatus ? "true" : "false") + ","; json += "\"internetOk\":" + String(wanStatus ? "true" : "false"); json += "}"; server.send(200, "application/json", json); } void setupServer() { server.on("/api/status", HTTP_GET, handleGetStatus); server.begin(); } The JSON Telemetry Schema
The resulting response is predictable, structured, and easy for downstream consumers to parse:
{ "ssid": "SpectrumSetup-53", "ip": "192.168.1.238", "mask": "255.255.255.0", "gateway": "192.168.1.1", "dns": "192.168.1.1", "rssi": -31, "signal": "Excl", "dnsOk": true, "internetOk": true } Keeping the Server Responsive
Embedded web servers do not run on heavy multi-threaded runtimes like Kestrel or Nginx. They rely on cooperative single-thread polling. For the server to accept connections without dropping packets, server.handleClient() must be invoked at the top of the main loop() on every single iteration:
void loop() { // Service incoming HTTP requests immediately server.handleClient(); // Run diagnostics and button checks on non-blocking timers checkButton(); handleDiagnosticSchedule(); } 2. The Cross-Origin Reality (CORS)
If you test your new API using a command-line tool, it returns data seamlessly. But the moment you attempt to fetch this JSON from a web application running in a browser (like a local development server on http://localhost:5000), the browser abruptly blocks the response with a CORS error.
Modern browsers enforce the Same-Origin Policy. When a script originating on localhost:5000 attempts to query 192.168.1.238, the browser refuses to hand the data to the frontend unless the remote server explicitly permits it.
Because microcontrollers do not have reverse proxies handling security policies for them, the firmware must inject explicit headers:
server.sendHeader("Access-Control-Allow-Origin", "*"); server.sendHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); server.sendHeader("Access-Control-Allow-Headers", "*"); By appending Access-Control-Allow-Origin: *, the ESP32 broadcasts to any requesting browser that its diagnostic status is publicly readable across the local network.
3. Zero-Configuration Local Discovery (mDNS)
Hardcoding raw IP addresses (like 192.168.1.238) is fragile. Routers renew DHCP leases, assign new IPs after power cycles, and break hardcoded configurations.
To make the gauge discoverable without needing a static IP or router reservations, we implement multicast DNS (mDNS) using the ESPmDNS library. This allows any machine on the local subnet to reach the board using a friendly human-readable hostname: [http://netgauge.local](http://netgauge.local).
#include <ESPmDNS.h> void setupMDNS() { if (mdns.begin("netgauge")) { // Advertise the HTTP service to the LAN mdns.addService("http", "tcp", 80); mdns.addServiceTxt("http", "tcp", "app", "netgauge-mini"); Serial.println("mDNS responder started: http://netgauge.local"); } } Once registered, queries to netgauge.local resolve directly to whatever IP address the router's DHCP server handed to the ESP32.
4. Practical Debugging: The Windows PowerShell "curl" Trap
With firmware flashed and mDNS initialized, the natural first verification step on a Windows workstation is opening PowerShell and running a quick curl check:
curl http://netgauge.local/api/status Instead of JSON, you are greeted by an immediate, frustrating failure:
curl : The remote name could not be resolved: 'netgauge.local' + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest] Why Did This Fail?
In Windows PowerShell (5.1 and earlier), curl is not the real curl utility. It is an internal alias for Invoke-WebRequest.
Under the hood, Invoke-WebRequest relies on legacy .NET HttpWebRequest networking components that bypass Windows’ modern mDNS resolver cache, failing to resolve the .local top-level domain even when standard ICMP pings succeed!
The Fix
To bypass the PowerShell alias and hit the native, standard binary, append the .exe extension explicitly:
curl.exe http://netgauge.local/api/status Alternatively, test directly against the raw assigned IP:
curl.exe http://192.168.1.238/api/status The response arrives instantly:
{"ssid":"SpectrumSetup-53","ip":"192.168.1.238","mask":"255.255.255.0","gateway":"192.168.1.1","dns":"192.168.1.1","rssi":-31,"signal":"Excl","dnsOk":true,"internetOk":true} If mDNS ever behaves erratically after frequent re-flashes, a quick cache flush on your PC clears any stale mapping:
ipconfig /flushdns What’s Next?
Our ESP32-C3 is no longer just driving a miniature OLED on a breadboard—it is a live, autonomous REST microservice. It continuously measures the local wireless link, updates its local hardware screen, and responds to incoming HTTP requests across the LAN in milliseconds.
In Part 3, we cross over to modern .NET to build a real-time Blazor Web App dashboard, setting up resilient polling loops, strongly typed JSON models, and an interface that brings this raw hardware telemetry to life on a desktop monitor.



