Ground truth:
src/main.cpp(setupRoutes(),setupDemoModeRoutes(),struct ClockSettings). If this document and the source disagree, the source wins and this file is stale. Schema below matchesSETTINGS_VERSION 19(firmware v2.30.x/v2.31.0 era).
Base URL: http://esp32c3-v3-8inch.local/ (or direct IP address)
Example IPs in this document use the documentation range 192.0.2.x; your device will have a real LAN address (check /net or your router).
Description: Serve main web UI page
Response: HTML page with embedded JavaScript and CSS
Features:
- Live SVG clock preview (updates every 90ms)
- Time controls (manual set, browser sync, NTP sync, +/- minute, time zone)
- Display settings (brightness, schedule, themes)
- Ring color pickers (9 elements: outer marker, outer fill, seconds, minutes, hours, middle face, inner face, inner hour, center)
- Petal depth slider (0-100, all three rings)
- Animation controls (second trail, progress ring, chimes, interval animations, Animation Style panel with preview)
- Auto-brightness controls (mode, Darkest/Brightest sliders, live lux display)
- Focus Reminders panel
- Demo mode controls (start/stop, pre-roll, live status)
- Network info panel
Description: Get current clock time and status
Response:
{
"hour": 14,
"minute": 32,
"second": 18,
"ntpSynced": true,
"wifi": true,
"ip": "192.0.2.100"
}Polling: Web UI polls this every 1 second
Description: Get temperature sensor reading (future)
Response (when sensor available):
{
"available": true,
"celsius": 22.5
}Response (when sensor not available):
{
"available": false
}Note: Currently placeholder. BME280/SHT31 sensor not yet implemented.
Description: Get ambient light sensor reading
Response (when VEML7700 available):
{
"available": true,
"lux": 123.4,
"autoBrightness": 150
}Response (when sensor not available):
{
"available": false
}Polling: Web UI polls this every 2 seconds when auto-brightness mode active
Fields:
lux: Current light level (0-120,000 lux range, auto-gain)autoBrightness: Calculated brightness value (0-255) based on logarithmic curve
Description: Diagnostic lux override -- pin the sensor to any value so the brightness response can be swept remotely, or release it back to the real sensor
Parameters (query string, one required):
lux(float, >= 0): hold the sensor at this valueclear(any value): release the override
Response:
{"override":true,"lux":42.0}when set{"override":false}when cleared{"available":false}when no sensor400 {"error":"need ?lux=X or ?clear=1"}when neither parameter given
Description: Get network information
Response:
{
"hostname": "esp32c3-v3-8inch",
"ssid": "YOUR_SSID",
"ip": "192.0.2.100",
"gateway": "192.0.2.1",
"subnet": "255.255.255.0",
"dns": "192.0.2.1",
"rssi": -45,
"status": 3
}Fields:
rssi: WiFi signal strength in dBm (typical range: -30 to -80)status: WiFi connection status code (3 = WL_CONNECTED)
Description: One-call diagnostics: uptime, firmware and settings version, time, NTP state, WiFi state, lux and the full brightness chain, ring levels, power-limiter state, animation state, timezone, save count
Response fields (JSON): uptime_sec, firmware_version, settings_version, time, ntp_synced, ntp_last_delta_sec, wifi_status, wifi_ssid, wifi_rssi, wifi_ip, lux, brightness_target, brightness_ramped, effective_brightness, outer_marker_level, outer_filler_level, hours_level, middle_hour_level, inner_hour_level, center_level, middle_ambient_scale, inner_ambient_scale, button_event_count, free_heap, clock_pixel_count, ring_pixel_offset, default_outer_ring_offset, outer_ring_offset, anim_phase, last_anim_source, last_anim_mode, display_sleep, reel_active, master_fade, est_milliamps, limiter_brightness, max_milliamps, timezone, settings_save_count
effective_brightness is computed through the same autoSpanMap() the renderer uses, so /diag can never disagree with what the LEDs are doing.
Description: Get all current settings
Response (all keys, values are examples):
{
"dayBrightness": 44,
"nightBrightness": 5,
"nightStartHour": 22,
"nightEndHour": 7,
"centerSource": 0,
"secondTrail": 0,
"progressSeconds": 0,
"hourlyChime": 1,
"statusAnimations": 1,
"outerMarkerColor": "#6EB9FF",
"outerMarkerLevel": 225,
"outerFillerColor": "#0008C8",
"outerFillerLevel": 145,
"secondsColor": "#64FFB4",
"secondsLevel": 230,
"minutesColor": "#FF6400",
"minutesLevel": 220,
"hoursColor": "#DC00B4",
"hoursLevel": 255,
"middleFaceColor": "#DC00B4",
"innerFaceColor": "#FF3C00",
"innerHourColor": "#6EB9FF",
"innerHourLevel": 255,
"centerColor": "#FF3C00",
"centerLevel": 180,
"autoBrightnessMode": 1,
"minAutoBrightness": 10,
"maxAutoBrightness": 153,
"quarterAnimation": 3,
"halfHourAnimation": 1,
"hourAnimation": 4,
"intervalAnimationsEnabled": 1,
"focusReminder_enabled": 0,
"focusReminder_startHour": 8,
"focusReminder_endHour": 22,
"focusReminder_intervalMinutes": 60,
"focusReminder_daysMask": 0,
"focusReminder_animation": 0,
"focusReminder_durationSeconds": 60,
"outerRingOffset": 0,
"animationPalette": 7,
"animationSpeed": 3,
"animationBrightness": 157,
"trailLength": 6,
"reminderPalette": 0,
"outerRingBrightness": 77,
"middleFaceScale": 39,
"innerFaceScale": 39,
"darkRoomOff": 0,
"secondTrailLength": 4,
"secondTrailStyle": 2,
"progressLevel": 90,
"progressStyle": 0,
"petalDepth": 45,
"timezone": "MST7MDT,M3.2.0,M11.1.0",
"tzConfigured": true,
"hasUserDefaults": false
}Color format: Hex RGB (#RRGGBB)
Level values: 0-255 (brightness intensity for each element)
Mode enums: See Settings Structure section below
tzConfigured (v2.31.0): read-only. true once a time zone has been saved to NVS; drives the first-run timezone banner in the web UI. POST /settings does not accept it -- strip it before re-POSTing a saved settings backup.
Description: Whether an animation is currently playing, and which phase. Lets an external sequencer (the demo reel designer's Live Run) event-pace triggers instead of guessing wall-clock durations.
Response: {"animating":true,"phase":"HR2"}
CORS: This is the one CORS-open endpoint (Access-Control-Allow-Origin: *) -- read-only status, no side effects, and the designer tool runs from a different origin.
Description: WiFi settings page (HTML). Shows the saved SSID and current connection status; form posts to POST /wifi.
Description: Firmware update page (HTML). Shows the running firmware version and accepts a .bin upload via POST /update.
Description: Demo mode state for the web UI and overlay
Response (running): active, preroll (bool), preroll_ms (remaining pre-roll, when pre-rolling), step, steps, subtitle, elapsed_ms, step_duration_ms
Response (idle): {"active":false}
Description: Full-screen 1920x1080 OBS-ready browser source (HTML) with fade transitions; mirrors /demo/status subtitles. Stays blank during pre-roll so no subtitle is composited onto the black lead-in.
Description: Manually set clock time
Parameters (application/x-www-form-urlencoded):
hour(int, 0-23, required)minute(int, 0-59, required)second(int, 0-59, required)
Example:
POST /set
Content-Type: application/x-www-form-urlencoded
hour=14&minute=30&second=0
Response: 200 OK "ok" or 400 Bad Request "missing hour/minute/second"
Side effects: Triggers STATUS_TIME_SYNC animation (1.2 sec)
Description: Sync clock to browser's local time
Parameters (application/x-www-form-urlencoded):
hour(int, 0-23, required)minute(int, 0-59, required)second(int, 0-59, required)
Example:
const d = new Date();
fetch('/syncBrowser', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `hour=${d.getHours()}&minute=${d.getMinutes()}&second=${d.getSeconds()}`
});Response: 200 OK "ok" or 400 Bad Request "missing hour/minute/second"
Side effects: Triggers STATUS_TIME_SYNC animation (1.5 sec)
Description: Force immediate NTP time sync
Parameters: None
Response: 200 OK "ok" or 503 Service Unavailable "ntp unavailable"
Side effects:
- Triggers STATUS_TIME_SYNC animation (1.5 sec) on success
- Triggers STATUS_WIFI_FAIL animation (1.5 sec) on failure
Failure conditions:
- WiFi not connected
- NTP server unreachable
- Time received is before cutoff (< Jan 2024)
Description: Increment time by 1 minute
Parameters: None
Response: 200 OK "ok"
Side effects: Triggers STATUS_BUTTON animation (0.7 sec)
Description: Decrement time by 1 minute
Parameters: None
Response: 200 OK "ok"
Side effects: Triggers STATUS_BUTTON animation (0.7 sec)
Description: Fire an animation immediately for preview -- does not modify saved settings
Parameters (application/x-www-form-urlencoded):
type(string, required):quarter|halfhour|hour|remindermode(int, required): animation index within the typequarter: 1-3halfhour: 1-3hour: 1-5reminder: 0-11
Optional style overrides (non-persistent, apply to this preview only, auto-cleared when the animation ends):
palette(0-3 moods, 7 = clock colors)speed(1-5)brightness(50-255)trail(2-12)reminderPalette(0-3 moods, 7 = clock colors)
Example:
POST /previewAnimation
Content-Type: application/x-www-form-urlencoded
type=hour&mode=4
Response: 200 OK "ok" | 400 Bad Request "<error>"
Side effects: Triggers the specified animation on the LED rings immediately. No settings are written -- the old flow persisted the style via POST /settings before every preview, which cost an EEPROM flash commit per click.
Description: Update display settings. Only keys present in the request are touched; everything else keeps its saved value.
Parameters (application/x-www-form-urlencoded, all optional):
Brightness:
dayBrightness(int, 0-255)nightBrightness(int, 0-255)nightStartHour(int, 0-23)nightEndHour(int, 0-23)
Display:
centerSource(int, 0-4: 0=status + bloom breathing, 1=bloom only, 2=status only, 3=temperature when a sensor exists, 4=off)secondTrail(int, 0-1)progressSeconds(int, 0-1)hourlyChime(int, 0-1)statusAnimations(int, 0-1)
Ring Colors (hex format #RRGGBB):
outerMarkerColor,outerFillerColor,secondsColor,minutesColor,hoursColor,middleFaceColor,innerFaceColor,innerHourColor,centerColor
Ring Levels (int, 0-255):
outerMarkerLevel,outerFillerLevel,secondsLevel,minutesLevel,hoursLevel,innerHourLevel,centerLevel
Auto-Brightness:
autoBrightnessMode(int, 0-2: 0=manual, 1=auto, 2=scheduled)minAutoBrightness(int, 5-255) -- the Darkest slidermaxAutoBrightness(int, 5-255) -- the Brightest slider
Time-Interval Animations:
quarterAnimation(int, 0-3: 0=off, 1=Slow comet, 2=Dual orbit, 3=Bloom ripple)halfHourAnimation(int, 0-3: 0=off, 1=Unfurl, 2=Three comets, 3=Breathe)hourAnimation(int, 0-5: 0=off, 1=Ceremony, 2=Galaxy spin, 3=Supernova, 4=Comet relay, 5=Deep breath)intervalAnimationsEnabled(int, 0-1)
Animation Style:
animationPalette(int; shared 5-option list, v2.27.0:0=Golden hour (warm),1=Moonlight (cool),2=Dawn (soft-warm),3=Twilight (muted-cool),7=Clock colors -- default. Any other value is coerced to7. Moods live intools/palettes/palettes.jsonmood_palettes;7is ring-mapped from the face colors in firmware)animationSpeed(int, 1-5: 1=slow, 3=normal, 5=fast)animationBrightness(int, 50-255)trailLength(int, 2-12)reminderPalette(int; same 5-option list asanimationPalette:0-3moods,7=Clock colors. Independent ofanimationPaletteso a nudge can read differently from a chime)
Focus Reminders:
focusReminder_enabled(int, 0-1)focusReminder_startHour(int, 0-23)focusReminder_endHour(int, 0-23) -- start == end is treated as an always-on 24h window (v2.31.0)focusReminder_intervalMinutes(int, 1-1440)focusReminder_daysMask(int, 0-127: bitmask Sun=bit0 .. Sat=bit6)focusReminder_animation(int, 0-11: 0-5 delegate to the chime animation slots, 6-11 dedicated nudges)focusReminder_durationSeconds(int, 1-60) -- reserved: accepted and stored, not yet used by the firmware
Face / ring tuning:
outerRingOffset(int, 0-59: clockwise LED rotation applied to all rings at render time)outerRingBrightness(int, 0-100: percent multiplier on outer ring colors)middleFaceScale(int, 0-255: middle ring ambient level)innerFaceScale(int, 0-255: inner ring ambient level)darkRoomOff(int, 0-1: blank all LEDs in a pitch-black room, auto mode only)petalDepth(int, 0-100: how deeply ring fills are shaded between petals; 0 = flat fill). LegacypetalMode(0/1) is still accepted as an alias whenpetalDepthis absent, so custom themes saved before v2.30.0 keep their texture.
Second-hand accessories:
secondTrailLength(int, 2-12)secondTrailStyle(int, 0-2: 0=classic geometric, 1=linear, 2=smooth gamma comet)progressLevel(int, 0-255: tint strength of the progress arc)progressStyle(int, 0-1: 0=uniform arc, 1=comet gradient)
Timezone:
timezone(string, POSIX TZ, e.g.MST7MDT,M3.2.0,M11.1.0). Not aClockSettingsfield -- it lives in NVS. An invalid string returns400and nothing is saved.
Misc:
silent(any value): suppress the settings-saved status animation
Example:
POST /settings
Content-Type: application/x-www-form-urlencoded
dayBrightness=200&nightBrightness=20&autoBrightnessMode=1&secondsColor=#FF0000
Response: 200 OK "ok" | 400 Bad Request (invalid timezone) | 503 Service Unavailable (firmware update in progress -- settings are locked so a flash write cannot collide with the OTA partition write)
Side effects:
- Settings saved to EEPROM
- Triggers STATUS_SETTINGS_SAVED animation (1.3 sec) unless
silent - Clock display updates immediately with new colors/brightness
Description: Non-persistent "Preview on clock": render the posted settings for a short TTL with no EEPROM write, then auto-revert to the saved face. Safe during OTA. Same parameter set as POST /settings.
Extra parameter: ttl (int, ms, optional; default 10000, clamped 1000-30000)
Response: 200 OK "ok"
Description: Cancel an in-flight preview and return to the saved face immediately
Response: 200 OK "ok"
Description: Reset settings to defaults (user-saved defaults if present, else factory)
Response: 200 OK "ok" | 503 during a firmware update
Description: Save the current settings as the user defaults (second EEPROM slot at offset 128, magic 0xD2). POST /settings/reset restores this set from then on.
Response: 200 OK "ok" | 503 during a firmware update
Description: Latch demo-reel Live Run mode: the live clock face is not drawn between previewed animations (black instead) and the device generates gentle cross-dissolves itself off each animation's start/end edges
Parameters: ms (int, optional; default 8000, clamped 2000-20000) -- idle crash-guard: if nothing animates for this long the clock returns to its face on its own, so a closed or crashed browser can never strand it black
Response: 200 OK "ok"
Description: End reel mode: gently dissolve back to the live clock face
Response: 200 OK "ok"
Description: Save WiFi credentials from the /wifi page and reconnect
Parameters: ssid (string, 1-32 chars, required), pass (string)
Response: 200 OK "Saved. Reconnecting to <ssid>..." | 400 on bad SSID length
Description: Upload a firmware .bin (multipart form upload from the /update page). Streams chunks to the OTA partition, verifies, reboots on success.
Response: 200 "Update successful, rebooting..." | 400 "No update in progress" | 413 "File too large" | 500 on begin/write/end failure
Description: Start the demo reel (video recording sequence)
Parameters: delay (int, seconds, optional, clamped 0-60) -- capture pre-roll: holds the clock fully dark before the reel opens, so a camera can be rolling and settled and frame one is a fade-up out of clean black. Handled on-device, so the countdown survives the browser tab closing.
Response: 200 {"status":"started"}
Description: Stop the demo reel immediately
Response: 200 {"status":"stopped"}
Description: Run only the auto-brightness demo cycle (lux override sweep), standalone
Parameters: ms (int, optional; default 14000) -- cycle duration
Response: 200 {"status":"started"}
Regenerated from src/main.cpp (struct ClockSettings, SETTINGS_VERSION = 19). Field order below is the EEPROM layout order.
struct ClockSettings {
uint8_t magic; // 0xC1 (validation byte)
uint8_t version; // 19 (SETTINGS_VERSION)
// Brightness
uint8_t dayBrightness; // 0-255, default 44
uint8_t nightBrightness; // 0-255, default 5
uint8_t nightStartHour; // 0-23, default 22
uint8_t nightEndHour; // 0-23, default 7
// Display
uint8_t centerSource; // 0-4: what the center LED shows when idle
// (0=status+bloom, 1=bloom, 2=status, 3=temperature
// when a sensor exists, 4=off). Reuses the EEPROM
// byte of the retired colorTheme field.
uint8_t secondTrail; // 0-1 (tints over the face since v16)
uint8_t progressSeconds; // 0-1 (tints over the face since v16)
uint8_t hourlyChime; // 0-1, default 1
uint8_t statusAnimations; // 0-1, default 1
// Ring colors. Note middleFace and innerFace are RGB only (their levels are
// the *FaceScale fields further down); the rest are RGB + level.
uint8_t outerMarkerRed, outerMarkerGreen, outerMarkerBlue, outerMarkerLevel;
uint8_t outerFillerRed, outerFillerGreen, outerFillerBlue, outerFillerLevel;
uint8_t secondsRed, secondsGreen, secondsBlue, secondsLevel;
uint8_t minutesRed, minutesGreen, minutesBlue, minutesLevel;
uint8_t hoursRed, hoursGreen, hoursBlue, hoursLevel; // middle 24h hand
uint8_t middleFaceRed, middleFaceGreen, middleFaceBlue; // middle ring ambient
uint8_t innerFaceRed, innerFaceGreen, innerFaceBlue; // inner ring ambient
uint8_t innerHourRed, innerHourGreen, innerHourBlue, innerHourLevel; // inner 12h hand
uint8_t centerRed, centerGreen, centerBlue, centerLevel;
// Auto-brightness
uint8_t autoBrightnessMode; // 0=manual, 1=auto, 2=scheduled; default 1
uint8_t minAutoBrightness; // 5-255 (Darkest), default 10
uint8_t maxAutoBrightness; // 5-255 (Brightest), default 153
// Time-interval animations
uint8_t quarterAnimation; // 0-3, default 3 (Bloom Ripple)
uint8_t halfHourAnimation; // 0-3, default 1 (Unfurl)
uint8_t hourAnimation; // 0-5, default 4 (Comet Relay)
uint8_t intervalAnimationsEnabled; // 0-1, default 1
// Focus Reminders (added v8)
uint8_t focusReminder_enabled; // 0-1, default 0
uint8_t focusReminder_startHour; // 0-23, default 8
uint8_t focusReminder_endHour; // 0-23, default 22
uint16_t focusReminder_intervalMinutes; // 1-1440, default 60
uint8_t focusReminder_daysMask; // bitmask Sun(bit0)..Sat(bit6)
// (factory default becomes all days in v2.31.0)
uint8_t focusReminder_animation; // 0-11: 0-5 reuse the chime anims,
// 6-11 dedicated nudges
uint8_t focusReminder_durationSeconds; // 1-60. RESERVED: accepted via POST
// /settings and stored, but not yet
// used by the firmware.
uint32_t focusReminder_lastFireMs; // INTERNAL: RAM-only bookkeeping,
// ignore it. Never meaningfully
// persisted; not settable.
// Ring rotation (added v10)
uint8_t outerRingOffset; // 0-59: clockwise LED rotation, all rings
// Animation customization (added v11)
uint8_t animationPalette; // 0-3 moods, 7=Clock colors (default); others coerce to 7
uint8_t animationSpeed; // 1-5, default 3
uint8_t animationBrightness; // 50-255, default 157
uint8_t trailLength; // 2-12, default 6
uint8_t reminderPalette; // same 5-option list, independent of animationPalette
// Face tuning (added v12-v14)
uint8_t outerRingBrightness; // 0-100 percent, default 77
uint8_t middleFaceScale; // 0-255, default 39
uint8_t innerFaceScale; // 0-255, default 39
uint8_t darkRoomOff; // 0-1 (added v15), default 0
// Second-hand accessory tuning (added v16)
uint8_t secondTrailLength; // 2-12, default 4
uint8_t secondTrailStyle; // 0-2, default 2 (smooth)
uint8_t progressLevel; // 0-255, default 90
uint8_t progressStyle; // 0-1, default 0 (uniform)
// Petal depth (added v17 as an on/off flag, widened to 0-100 in v19)
uint8_t petalDepth; // 0-100, default 45. 0 = flat fill.
// RETIRED (v19): held autoBrightnessGain (v18). Folded into
// maxAutoBrightness on migration; byte kept zeroed to preserve the layout.
uint8_t reservedGain;
};EEPROM window: 256 bytes total. Main slot at offset 0 (magic 0xC1); user-saved defaults slot at offset 128 (magic 0xD2, written by POST /settings/saveDefault). Compile-time static_asserts guard both boundaries.
Not in the struct: the timezone (NVS, clock/tz) and WiFi credentials (NVS, wifi namespace). Neither is touched by settings migrations or factory reset of the struct.
SettingsStore::defaults() in src/main.cpp is authoritative -- the values below were read from it at settings v19 and are the ChronoBloom theme:
Brightness:
- Day: 44 (~17%), Night: 5 (~2%), night schedule 22:00-07:00
- Auto: mode 1 (sensor), Darkest 10, Brightest 153 (the same daylight peak the pre-v19 defaults produced)
Colors (RGB @ level):
- Outer marker: periwinkle blue (110, 185, 255) @ 225
- Outer filler: deep royal blue (0, 8, 200) @ 145
- Seconds: mint green (100, 255, 180) @ 230
- Minutes: orange (255, 100, 0) @ 220
- Hours (middle hand): hot pink/magenta (220, 0, 180) @ 255
- Middle face: hot pink (220, 0, 180), scale 39
- Inner face: warm orange (255, 60, 0), scale 39
- Inner hour hand: periwinkle (110, 185, 255) @ 255 (v2.30.1 -- was magenta, zero hue contrast against the middle hand)
- Center: warm orange-red (255, 60, 0) @ 180
Animations (factory defaults):
- Quarter-hour: Bloom Ripple (mode 3), Half-hour: Unfurl (mode 1), Top of hour: Comet Relay (mode 4), interval animations enabled
- Style: palette 7 (Clock colors), speed 3, brightness 157, trail 6; reminder palette 0 (Golden hour)
- Petal depth: 45
Causes:
- Missing required parameters
- Invalid parameter format
- Out-of-range values
- Invalid POSIX timezone string on
POST /settings
Example: POST /set without hour parameter returns:
400 Bad Request
missing hour/minute/second
Causes:
- NTP sync requested but WiFi disconnected, NTP unreachable, or time invalid (
POST /syncNtp) - Settings write requested while a firmware update is running (
POST /settings,/settings/reset,/settings/saveDefaultall answerFirmware update in progress -- settings locked; a flash commit mid-OTA can corrupt the half-written image)
CORS: GET /anim/status is CORS-open (Access-Control-Allow-Origin: *; read-only, no side effects). Every other endpoint is same-origin only.
Authentication: None. Anyone on the local network can access all endpoints.
Rate limiting: None. Endpoints can be called unlimited times.
Future considerations:
- Basic authentication for
/settingsPOST - Rate limiting for NTP sync (max 1/minute)
- API key for external integrations
Status: Not implemented
Future: Live time updates could use WebSocket instead of 1-second polling to reduce HTTP overhead.
When MQTT support added:
Note: topic prefix
chronobloom-clockbelow is the planned firmware identifier. Final topic names will be confirmed when MQTT is implemented.
Subscribe (commands from Home Assistant):
chronobloom-clock/command/brightness-- Set brightness (0-255)chronobloom-clock/command/mode-- Set display modechronobloom-clock/command/animation-- Trigger specific animation
Publish (state to Home Assistant):
chronobloom-clock/state-- Current time, brightness, mode (JSON)chronobloom-clock/sensor/lux-- Ambient light levelchronobloom-clock/sensor/temperature-- Temperature (when sensor added)
Fetch time:
const response = await fetch('/time');
const data = await response.json();
console.log(`Current time: ${data.hour}:${data.minute}:${data.second}`);Save settings:
const params = new URLSearchParams();
params.set('dayBrightness', 200);
params.set('secondsColor', '#FF0000');
await fetch('/settings', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: params.toString()
});Get current time:
import requests
response = requests.get('http://esp32c3-v3-8inch.local/time')
data = response.json()
print(f"Current time: {data['hour']}:{data['minute']}:{data['second']}")Set brightness:
requests.post('http://esp32c3-v3-8inch.local/settings', data={
'dayBrightness': 200,
'nightBrightness': 10
})Sync to NTP:
curl -X POST http://esp32c3-v3-8inch.local/syncNtpGet diagnostics:
curl http://esp32c3-v3-8inch.local/diagSet time:
curl -X POST http://esp32c3-v3-8inch.local/set \
-d "hour=14&minute=30&second=0"Behavior:
- On first boot, if no saved WiFi credentials exist, the device first listens for Improv WiFi over USB serial (bounded window, used by the browser flasher), then opens a WiFi access point
- SSID:
esp32c3-clock-setup(no password required) - IP address:
192.168.4.1 - Portal window (v2.31.0): 10 minutes on a never-provisioned first boot, 2 minutes on a previously-connected unit, 15 minutes after factory reset (then falls through to AP mode)
If all STA connection attempts fail (wrong password, network unavailable), the device starts a software AP and runs the full web server at 192.168.4.1. Clock display, web UI, and settings are all functional in AP mode. NTP, mDNS, and OTA are skipped until a STA connection is established. While nobody is connected to the setup AP, the device retries the known credentials every 5 minutes and reboots into normal mode on success.
Portal page:
- Displays list of available WiFi networks (SSIDs)
- User selects their SSID and enters password
- Device stores credentials (WiFiManager library manages storage)
- Device connects to saved network
- Portal closes automatically
Credential Storage:
- WiFi SSID and password stored in NVS (separate from clock settings)
- Persists across reboots
- Can also be changed any time via the
/wifipage
Error Handling:
- If the portal window closes without credentials, the portal reappears on next boot
- Invalid credentials (password changed in router): portal reappears on next boot
- WiFi network unavailable: portal reappears on next boot
After initial USB flash, firmware updates can be deployed over WiFi using ArduinoOTA protocol on port 3232, or by uploading a .bin on the /update page.
Port: 3232 (TCP)
Authentication: None. Current firmware calls ArduinoOTA.begin() without a password; OTA is open to the local network.
Protocol: ArduinoOTA binary protocol (not HTTP/HTTPS)
# Build new firmware
pio run -e esp32c3_v3_8inch
# Upload via OTA to device at mDNS hostname
pio run -e esp32c3_v3_8inch -t upload --upload-port esp32c3-v3-8inch.local:3232
# Or upload to device by IP address
pio run -e esp32c3_v3_8inch -t upload --upload-port 192.0.2.100:3232- Build phase: PlatformIO compiles firmware (.bin file)
- Connection phase: PlatformIO connects to device on port 3232
- Upload phase: Binary streamed to device (shows progress % in serial monitor if connected)
- Flash phase: Device writes new firmware to flash memory
- Verify phase: Device verifies flash integrity
- Reboot phase: Device automatically reboots with new firmware
- Status animations:
- During upload: inner ring shows blue
- On success: inner ring shows green
- On failure: inner ring shows red
Serial output during OTA:
[OTA] Update starting...
[OTA] Progress: 45000/703962 (6.4%)
[OTA] Progress: 90000/703962 (12.8%)
...
[OTA] Update complete, rebooting...
Port 3232 not responding:
- Device must be on WiFi (test with
ping esp32c3-v3-8inch.local) - Check firewall: port 3232 may be blocked
- Restart device and try again (OTA server initializes ~30-60s after boot)
- Verify mDNS is working: ping should resolve hostname to IP
Network interruption during upload:
- Blue animation stops, device may reboot
- Try again with stronger WiFi signal (move closer to router)
- Check router is not dropping connection on specific devices
Authentication failure:
- Current firmware sets no OTA password, so an auth failure means the device is still running an older build that did
- Reflash once over USB (or the
/updatepage) to bring it to the current firmware