Always-on coastal conditions dashboard. Tide predictions, swell and wind from a nearby buoy, moon phase, a 7-day beach planning view, and local eBird sightings — refreshed every 10 minutes, readable from across the kitchen without touching a phone.
DynamicJsonDocument)HTTPClient / WiFiClientSecureconfigTime / NTP synctime.h for all time mathg_page int
spanProgress() / arcPointForProgress()
fetchAllData() runs at startup and every 10 minutes, calling each source sequentially. JSON is parsed with DynamicJsonDocument allocated on the heap, sized to ~1.5× the observed maximum response length, and freed when each fetch function returns. Seven sequential HTTPS calls on a 512KB target with no async — every sizing and timeout decision compounds.
NDBC buoy data arrives as raw whitespace-delimited text, not JSON — parsed manually with strtok to extract wave height, period, swell direction, wind speed, gust, pressure, and water temperature from column offsets. The two eBird calls use a two-source merge: Andree Clark Bird Refuge at 1km radius fills available slots first, the broader SB coast at 5km fills any remainder, with species deduplicated by common name across both.
On a 512KB target, undersizing a DynamicJsonDocument silently returns zero results — the parse fails with no visible error. Each document size was tuned by logging raw response body lengths via serial, observing silent failures, and sizing to ~1.5× the observed peak. Documents are freed when each fetch function returns to prevent heap fragmentation across seven sequential calls.
Arduino's HTTPClient is synchronous. With seven data sources and 12–15 second per-call timeouts, a single unreachable endpoint during startup can make the device unresponsive for over a minute. Each call has a tuned connect timeout with fast-fail on error, and fetchAllData() continues to the next source regardless of individual failures — partial data is better than a frozen screen.
The geo/recent endpoint returns the most recent sighting per species within a radius. To surface Andree Clark Bird Refuge specifically — a 1km target that could be drowned out by the broader waterfront — the fetch runs two sequential calls: Andree Clark at 1km radius fills available display slots first, the SB coast at 5km fills any remainder. Species are deduplicated by common name before insertion. The same dedup applies to the notable/rare endpoint.
Sunrise, sunset, moon phase, and illumination are derived from local algorithms (USNO solar position, Julian date math) rather than API calls. This eliminates a seventh synchronous HTTPS dependency, works without a network on those values, and keeps the sky clock and moon page renderable even when an API fetch fails.
The EPD bitmap font set includes sizes 8, 12, 16, and 20. Size 8 is approximately 6px character width. At 2.13 inches, arm's length, e-ink contrast and dot pitch make size-8 text effectively invisible in practice. Every information element the user needs to read runs at size 12 minimum; size 8 is reserved for decorative labels only.
Buttons registered nothing. Multiple pin assignments were tried. Debounce values were retuned. Watchdog resets appeared and disappeared without explanation. The root cause was in a file called spi.h inside an earlier project version — a pinout table showing that GPIOs 9–14 are the exclusive property of the e-ink SPI bus (BUSY, RES, MOSI, SCK, CS, DC). Two of the original button assignments landed on SCK and CS. Setting those to INPUT_PULLUP during button init left the EPD panel unable to signal ready, trapping the busy-wait loop forever. Reading the hardware header file once resolved a multi-week mystery in under five minutes.
The EPD library uses a single global ImageBW pixel buffer that accumulates all draws until explicitly cleared with memset(ImageBW, 0x00, ALLSCREEN_BYTES). Early versions ghosted content from previous screens because the buffer clear ran in the wrong order — after the page header was drawn rather than before it. The fix is now canonical: beginPage() always zeroes the buffer first, before any draw call.
The Beach Week screen needs seven days of data — morning low time, low height, afternoon high time, high height, and daily temperature — in 250 horizontal pixels. The original design added a weather symbol column at size 8. It was invisible on hardware. That column was cut and replaced with the high-tide time column, which required a second-pass parse over the NOAA prediction array to extract flood events after each morning low. The final layout achieves six columns at size 12 with manually mapped x-coordinates, leaving no pixel to waste.
The Sky Clock page positions sun and moon symbols along an arc representing the sky dome based on their current progress through their rise-to-set window. This required spanProgress() (fractional position in a time span, 0.0–1.0), arcPointForProgress() (mapping that fraction to an x/y coordinate on a parabola defined by three anchor points), and drawArcMarker() (rendering the appropriate symbol at the computed location). The moon symbol changes shape dynamically based on phase and waxing/waning direction.