Repository navigation
Conversation
A desk clock for the ESP32-S3-RLCD-4.2 (Arduino, U8g2): date and time with NTP, the hardware RTC and a measured clock drift; Open-Meteo weather for three days with the chance of rain and the moon; indoor temperature and humidity; Spotify now-playing with the KEY button as remote; a battery gauge with a runtime estimate and a low-battery shutdown; info, power and legend pages. Settings come from a file on an SD card (read at boot and kept in flash): WiFi and a backup network, units and formats, location, Spotify, battery and power options. WiFi can be switched off. The firmware can be updated from the SD card too: copy the .ino.bin that the Arduino IDE exports to the card and restart. The file is checked against its SHA-256 before anything is written, then written to the other app slot; the running firmware is not touched until the new image has verified. A new firmware is on trial for its first minute: a reset in that time makes the bootloader put the old one back. The sketch compiles without a secrets.h, so a fresh clone builds as it is. Written and tested on a PC (renderer checks, fuzz, simulations, about 33,000 host-side checks) and compiled for the board. The SD card features, the deep-sleep shutdown, the runtime estimate and the backup network switch have not run on the real board yet; the README lists what is untried. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Incomplete weather data is accepted, failed settings persistence is reported as successful, and failed OTA confirmation disables further handling.
Review effort: Balanced
Findings: 3
Open (3)
What changed in this PR
Adds a full-featured Arduino desk-clock example for the ESP32-S3-RLCD-4.2, including hardware integration, online services, configuration, update handling, documentation, and host-side validation tools.
Changes:
- Implements clock, weather, battery, Spotify, display, networking, and power-management functionality.
- Adds SD-card settings and rollback-capable firmware updates.
- Adds documentation, screenshots, tests, preview tooling, fuzzing, and generators.
Paths are relative to 02_Example/Arduino/11_Clock_Dashboard/.
| File | Description |
|---|---|
.gitattributes |
Configures repository attributes. |
.gitignore |
Excludes generated tool outputs. |
11_Clock_Dashboard.ino |
Coordinates startup and application runtime. |
README.md |
Documents setup, features, and operation. |
ST7305_U8g2.cpp |
Implements the display driver. |
ST7305_U8g2.h |
Declares the display driver. |
app_model.h |
Defines UI data models. |
app_settings.cpp |
Loads and persists configuration. |
app_settings.h |
Declares configuration integration. |
app_state.cpp |
Implements shared application state. |
app_state.h |
Declares synchronized state access. |
battery_est.h |
Estimates battery runtime. |
buttons.h |
Implements button-state handling. |
calc.h |
Provides calculation helpers. |
charge.h |
Detects charging state. |
config.h |
Defines compile-time defaults. |
datefmt.h |
Formats dates. |
drift.h |
Estimates RTC drift. |
frame_plan.h |
Schedules display frames. |
fw_logic.h |
Encapsulates update decisions. |
fw_update.cpp |
Implements SD firmware updates. |
fw_update.h |
Declares update operations. |
link_page.cpp |
Renders Spotify linking pages. |
link_page.h |
Declares linking-page rendering. |
log.h |
Provides logging helpers. |
low_battery.h |
Implements shutdown decisions. |
moon.h |
Calculates moon phases. |
net_task.cpp |
Handles WiFi, NTP, and weather. |
net_task.h |
Declares network-task control. |
power.cpp |
Implements deep-sleep handling. |
power.h |
Declares power operations. |
sdcard.cpp |
Implements SD-card access. |
sdcard.h |
Declares SD-card operations. |
secrets.example.h |
Provides optional secret templates. |
sensors.cpp |
Integrates RTC, SHTC3, and battery sensing. |
sensors.h |
Declares sensor operations. |
settings.cpp |
Parses and formats settings. |
settings.h |
Defines settings structures and APIs. |
spotify.cpp |
Implements Spotify OAuth and control. |
spotify.h |
Declares Spotify integration. |
spotify_helper_script.h |
Embeds the linking helper script. |
spotify_parse.cpp |
Parses Spotify responses. |
spotify_parse.h |
Declares Spotify parsers. |
timeutil.h |
Provides calendar calculations. |
tls.h |
Supplies TLS trust configuration. |
tz_table.cpp |
Contains generated timezone mappings. |
tz_table.h |
Declares timezone lookup. |
ui.cpp |
Renders dashboard screens. |
ui.h |
Declares UI rendering. |
util.h |
Provides encoding and string helpers. |
weather.cpp |
Parses weather and geocoding responses. |
weather.h |
Declares weather parsers. |
weather_codes.h |
Maps WMO conditions. |
wifi_pick.h |
Implements network failover selection. |
tools/gen_helper_header.py |
Embeds the Spotify helper. |
tools/gen_tz_table.py |
Generates timezone mappings. |
tools/spotify_link.py |
Provides desktop Spotify linking. |
tools/tests/battery_sim.cpp |
Simulates battery estimates. |
tools/tests/charge_stats.cpp |
Evaluates charge detection. |
tools/tests/check.h |
Provides host-test assertions. |
tools/tests/fixtures/esp32s3_app_head.bin |
Supplies application-image test data. |
tools/tests/fixtures/esp32s3_bootloader_head.bin |
Supplies bootloader test data. |
tools/tests/fixtures/open-meteo-forecast-vasse.json |
Supplies forecast test data. |
tools/tests/fixtures/open-meteo-geocode-vasse.json |
Supplies geocoding test data. |
tools/tests/fixtures/spotify-player-ad.json |
Supplies Spotify advertisement data. |
tools/tests/fixtures/spotify-player-episode.json |
Supplies Spotify episode data. |
tools/tests/fixtures/spotify-player-track.json |
Supplies Spotify track data. |
tools/tests/run_charge_stats.sh |
Runs charging simulations. |
tools/tests/run_tests.sh |
Builds and runs host tests. |
tools/tests/test_features.cpp |
Tests settings, power, and update logic. |
tools/tests/test_logic.cpp |
Tests parsers and utility logic. |
tools/tests/test_spotify_link.py |
Tests the linking helper. |
tools/ui_preview/build_u8g2.sh |
Builds host-side U8g2 objects. |
tools/ui_preview/fuzz.cpp |
Fuzzes UI rendering. |
tools/ui_preview/host_display.h |
Implements the preview framebuffer. |
tools/ui_preview/preview.cpp |
Generates preview scenarios. |
tools/ui_preview/run_fuzz.sh |
Runs sanitizer-backed fuzzing. |
tools/ui_preview/run_preview.sh |
Builds and runs previews. |
tools/ui_preview/to_png.py |
Converts previews to PNG. |
docs/ESP32-S3-RLCD-Config.example.txt |
Provides an example configuration. |
docs/battery-empty.png |
Illustrates empty-battery UI. |
docs/battery-states.png |
Illustrates battery states. |
docs/dashboard-12h.png |
Shows the 12-hour dashboard. |
docs/dashboard-idle.png |
Shows the idle dashboard. |
docs/dashboard.png |
Shows the primary dashboard. |
docs/firmware-update.png |
Shows update progress. |
docs/info.png |
Shows the information page. |
docs/key-feedback.png |
Shows key feedback. |
docs/legend.png |
Documents dashboard elements. |
docs/low-battery.png |
Shows low-battery UI. |
docs/now-playing.png |
Shows Spotify playback. |
docs/power.png |
Shows power information. |
docs/weather-band.png |
Shows forecast rendering. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ba7d7c7 to
f9f6c39
Compare
…r reply) * Saving the SD card's settings to flash now reports a failure: every clear, write and remove is checked (an empty value is read back, as putString() returns 0 for it too), and a failed save is shown on the screen instead of only logged, since the card then comes out and the settings are gone at the next restart. * A new firmware's trial flag is cleared only after the bootloader has accepted the confirmation. A failed confirmation is tried again every 5 s, five times, and the Info page says "NOT confirmed yet" until it works. * The weather reply has to carry every field the clock shows as a fact (the current temperature, feels-like, humidity, wind, condition and day or night, and each day's condition and temperatures) instead of turning a missing one into a clear sky and 0 % humidity. The rain chance, sunrise, sunset and UV stay optional: the service leaves them null where it has no figure. A zone name without an offset no longer counts as a zone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The ADC and the 1:3 divider can read a few percent low: a full cell (4.20 V on a LiPo tester) showed as about 4.13 V, so the gauge stopped at 93 % and the charge detector, which calls a cell full from 4.16 V, never said so. * New setting battery_calibration, a multiplier for the measured voltage (the real voltage divided by the one shown; 0.80 to 1.25, default 1). readBattery() applies it, so the gauge, the runtime estimate, the shutdown and the charge detection all work with the corrected voltage, and the Power page shows the factor. * Serial command "batcal 4.20" measures for two seconds (the median of 25 readings), works the factor out from what a tester shows, saves it and restarts; "batcal reset" undoes it. * config.h has defaults for both battery settings (BATTERY_CAPACITY_MAH and BATTERY_CALIBRATION), which secrets.h can override. The factor arithmetic and the setting are covered by host tests; the ADC and the serial command have not run on the board yet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The charge state is now drawn inside the battery body instead of as a separate icon beside it: a bolt in a cleared window of the fill while charging, a tick cut out of the solid fill when full, nothing when it runs on the battery. The slot beside the gauge is gone, so the gauge no longer shifts sideways when the state changes. The pixel check makes sure the art stays inside the frame at every level and in the inverted blink phase, that the gauge never moves with the state, and has negative controls for art on the frame and for art that is missing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
wifi_power_save and ten more settings (wifi on/off, ntp_server, time and date format, week numbers, spotify on/off, battery auto/none, low-battery shutdown and its cutoff, cpu_mhz) could only be set from the SD card file. Each now has a macro in config.h that secrets.h can replace (WIFI_POWER_SAVE, TIME_FORMAT, CPU_MHZ, ...), so a build can come pre-programmed with anything the card could say. APP_HOSTNAME and WEATHER_INTERVAL_MIN could not be replaced from secrets.h before; they can now. All the build defaults, the old ones included, go through build_defaults.h and the same checks as the card (the same words, the same limits). A value that is refused leaves the factory value in place and is reported: in the serial log, in a banner at start-up and on the Power and settings page. test_builddefaults is built four ways, with a header standing in for secrets.h: nothing set (equal to the settings' own defaults), everything set (equal to the card file setting by setting, so a setting added without a build-time default fails and is named), mistakes (refused and reported) and values on the limits. It also checks that every macro is guarded by #ifndef and documented in the README and in secrets.example.h. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pulled from USB while full, the cell gives no step, and with a light load (the clock with its radio mostly off draws 10 to 45 mA) the top of the curve falls by only 0.07 to 0.3 mV a minute: the twelve-minute trend called that flat, so the gauge kept its tick at 96 % and the Left line said "full" instead of working out a runtime. charge.h now also keeps one level per five minutes for four hours. A charger holding the cell keeps it within a few millivolts of its plateau, a load takes it down for good: FULL ends when a slow level is 13 mV under the plateau, and once FULL has ended for any reason flat cannot bring it back until the last hour has not fallen by more than 1.2 mV. In simulation a fall of 0.3 mV/min is seen after 48 minutes (24 with the usual step at unplugging) instead of 81, a fall of 0.07 mV/min after about three hours instead of never. What a charger does itself (a plateau that moves by +-4 mV, the cell relaxing after the end of charge) still changes nothing. Tests: slow-fall, plateau-jitter (300 sequences), relaxation, re-plug and stickiness scenarios in test_logic, the same light loads in charge_stats, 16 mutants of the new rule all caught. The test run now stops on any undefined-behaviour report (they were only printed before), which found no problem in the existing tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tery
Two new settings, both off by default and both untried on the board:
wifi_mode = sync switches the radio off between sessions. A session starts
when something needs the network (the weather every weather_interval_min, the
network time about hourly, a look at Spotify, a key press, the Now Playing page,
music playing), lasts while there is something to do and ends 2.5 seconds after.
A network that cannot be joined is retried after 1, 2, 5, 10 and then every 15
minutes; the weather and the place back off the same way while the radio is on.
radio_plan.h holds the rules (pure, simulated over a day on a PC: the radio is
on 0.6 % of the time, 96 sessions), net_task.cpp starts and stops the WiFi
stack and SNTP around each session, spotify.cpp tells the planner when it needs
the radio and lets go of its connections and the link page when it goes off.
cpu_idle_mhz (off, 80, 40, 20, 10) lowers the CPU clock while the radio is off
(also with wifi = off). clock_policy.h keeps it at cpu_mhz while the radio is
on or about to be (WiFi works from 80 MHz up), for 8 seconds after a button
press, and while a computer is on the USB port (the console may stop below 80
MHz). The UI loop, which owns the SPI bus, applies it; FramePlanner is told
about each change so the first frame is planned with the new draw and send costs.
Also: the picker goes back to the network that worked after a session, the Info
and Power pages show the radio's share and the next session, and the console
has a "power" command. Both settings have build-time macros (WIFI_MODE,
CPU_IDLE_MHZ), documented in the README ("Saving power").
Tests: test_radio (sessions, back-off, a simulated day, clock policy, the frame
planner across clock changes) and 28 mutants of it, all caught.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…r settings Follow-ups to the WiFi sync mode and the idle clock. Like those, nothing here has been run on the board. A network that is not there is no longer a failed join. The WiFi stack's "no such network" (counted from the station's disconnect events; radio_plan.h, Search) ends the session after one scan per network instead of after 25 or 45 seconds, and the clock looks again every 2 minutes for the first ten and every 5 from then on, so a hotspot switched on at work is found without waiting out a back-off. Any button makes it look at once, and a key press buys one attempt, not 45 seconds of them. The 1, 2, 5, 10, 15 minute back-off stays for a network that is there and refuses the clock. Should the radio report differently from what the code expects, the old behaviour is what is left. spotify_live (SPOTIFY_LIVE): off lets the radio sleep while music plays and looks again when the track should be over. Default on, as before. console (CONSOLE_MODE): auto shuts the USB serial console down when no computer has been on the port for ten seconds, off does so at the end of start-up: no more logging, the driver stopped, the USB transceiver off. KEY held at start-up keeps it for that run, and a new "set name = value" command changes a setting from the console. cpu_idle_mhz no longer offers 10 MHz: a frame would take a quarter of a second to draw. 40 is the lowest clock Espressif gives current figures for, 20 the lowest the Arduino core lists. With an idle clock set the buttons are also read by interrupt and replayed to the click detector, so a tap that begins and ends inside a slow frame is not lost. The unused audio chips are told to power down at start-up, with what Waveshare's audio example writes when it closes them, and their I2S lines are given a level (AUDIO_CHIPS_STANDBY 0 leaves them alone). Fixed on the way, found by simulating weeks of uptime: times kept as 32-bit milliseconds came round again after 24.8 days. In sync mode the planner then stopped starting sessions by itself, and a key press, the Now Playing page or a button press from 25 days before held the radio or the CPU clock up. The battery estimate and the low-battery guard now take their seconds from the 64-bit timer. Tests: test_radio 75,928 checks (the planner by hand and over simulated days: away, refused, music with and without spotify_live, the wrap), test_logic 1,480, test_features 32,714; 103 mutants of the new logic, all caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two more times kept as 32-bit milliseconds where 0 stood for "none" or "now": * a rejected access token was marked with 0, which reads as "valid for another 25 days" once millis() is past 2^31, so the clock would have kept sending the token Spotify had just refused; * each radio session of wifi_mode = sync set the next poll to 0 for "now", which after 24.8 days means "in 25 days": the session's look at the player would never have happened, and every session would have ended as a failure after three minutes. Also: a poll that has been due for weeks (no network all that time) stays due, and with console = on the USB port is not looked at at all, as before. Not run on the board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Sketch > Export Compiled Binary" (the README's way to get the file for an SD card update) writes the images into build/ inside the sketch folder, and they hold whatever secrets.h had: the WiFi password, the Spotify id. Nothing ignored them, so a broad git add would have committed them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SD card is only read while the clock starts, and until now the only restart without a computer was to take the power away. KEY held for five seconds shows a banner, and letting go restarts the clock: a card that was put in while it ran gets read (settings, a firmware file) without the battery coming out. It waits for the button to come up because KEY held while the clock starts means "keep the console" (and "start anyway" on the battery-empty screen). HoldRelease in buttons.h, fed from the click detector's debounced state; the legend page's line for KEY says so. Also: a console that was shut down (console = auto / off) left the two USB pins as outputs held low, which a restart that is not a power-on does not undo; consoleBegin() lets go of them before the port is started, so that such a restart can bring the USB port back. Tests: test_logic 1,503 checks (the hold: arms once, fires on release only, chatter, the wrap of millis(), fed from recorded changes), 8 mutants caught; the UI preview measures the legend's KEY column against the BOOT column and the new banner (the longest one) in its band, each with a negative control. Not run on the board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first card tried on the board was FAT32 by Windows' account and the clock called it "not FAT32". It had a GUID partition table: the FAT library in the core is built without GPT (and without exFAT), and the clock said the same thing for every card whose file system would not mount. Now it looks: after a failed mount the card is started once more without a file system and its first sector is read, and that of its first partition (sdcard.cpp). sd_layout.h (pure, host-tested) tells a GUID partition table, exFAT, NTFS, an unformatted card, a FAT volume that should have mounted and anything else apart, and the banner, the Power and settings page and the log say which: "SD card: GPT partitions, needs MBR", "SD card is exFAT: format it as FAT32". The README says how to put each right. Tests: test_features 33,056 checks (made-up sectors of every kind, the lines for the screen), 20 mutants caught; the UI preview measures each line in its banner. The look at the card itself has not run on the board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first firmware update tried on the board ended in "Cannot read the file (card error)" with the bar almost full. The updater read the file's first 208 bytes for a look at the header and then began the checksum pass from where that read had left off, not from the start: 208 bytes off, it ran out of file at the very end, on every file. Nothing was written. The code had only ever been compiled; the tests covered which file to accept, not how it was read. The two passes (check the whole file against its SHA-256, then write it to the other slot) are now fwlogic::installImage(), in which every read names its place in the file, written against three small interfaces: the file, the hash, the slot. fw_update.cpp plugs in the card, mbedTLS and the Update class. tools/tests runs the same code with a made-up file and flash: good files of many sizes and read sizes, a flipped bit anywhere, a copy cut short, card and flash errors in either pass, nothing written before the check is through, and the mistake itself (a file read as "what comes next" fails just as the clock did). 21 mutants, all caught. A clock running a build from before this fix cannot be updated from the card and needs one upload over USB. Seen on the board with that try: the card mounts, the file is found and read end to end. Still not run there: writing the slot, the restart into it, the trial minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The version is a constant that nobody had touched since the sketch was
published: every build of this branch said 1.3, the one with the mended SD
card updater as much as those without it. It is 1.4 now, and the README has
a "Versions" section that says what the number stands for (it goes up by hand
with each batch of changes handed over to run on a board) and what came with
1.4 and with 1.3.
A version and a date still cannot tell two builds of one day apart, so the
Firmware line of the System info page now ends in a build id:
Firmware RLCD Clock 1.4, 2026-10-07, build 3f9a12c
It is the first seven hex digits of the ELF hash in the app descriptor: the
hash by which the updater already tells builds apart and which "fw" prints on
the console. It is the SHA-256 of the .ino.elf built along with the .ino.bin
(compared on an exported build), so a file on a PC can be matched to a clock.
The start-up line of the log has it too. A static_assert keeps the row from
being cut short by a longer name or version.
tools/tests: the version in config.h has to be the one the README gives, at
its top and first in its list of versions; the check is shown to fail when
either place says something else.
Not run on the board: the call that reads the hash there has not been seen to
answer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found on the board: a card with four new settings and a firmware file. The update went in, and the first banner of the new firmware said "SD settings: nothing new" of settings that had just been added. They were in force. The start that installs a firmware applies the settings file first, saves it, and restarts before it gets to its banner; the next start reads the same file and finds nothing left to change. The number of settings a card changed is now kept in flash until a banner has told of it, so the start after an update says "SD settings: 4 changed". A firmware installed by 1.4 or older gets no such count: it says "SD settings: 4 in force" and leaves "none new" unsaid, as it cannot know. Apart from updates, a file whose settings all apply already says "4 in force, none new" (was "nothing new"), and a file that is all comments "the file sets nothing". The texts come from one pure function (formatSdBanner, settings.h): tested with the card's own four lines applied twice, 17 mutants caught, every text measured on the rendered banner. Keeping the count in flash cannot be tested off the board. README: what the owner saw on the board today is noted where it was listed as untried (a settings file applied from a card, a firmware installed from it, the processor at 20 MHz with the pages drawn at that clock), and "Versions" has 1.5. Not run on the board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…harger Found on the board: restarted on its battery at 99 %, with the radio mostly off and the processor at 20 MHz, the clock said "Left: full" and gave no estimate. Two things were behind it, both of them old, and both harmless only as long as the clock drew enough for the voltage to be seen falling. charge.h called a cell full when it sat flat at the top of the curve for twelve minutes. Under a light load a cell that carries the clock is just as flat (0.03 to 0.1 mV a minute), so every start on a nearly full battery read as full eleven minutes later, for hours. Flat at the top is now FULL only for a cell that was seen charging up to there, or that has not come down in 160 minutes of the slow history. "Not come down" is judged against the highest stretch in the window, not the oldest: a charge that ends inside the window starts low, and the fall after it passed for "no lower than before" (the rule that lets FULL come back was fooled the same way; the tests had only ever started on a flat cell, never with the charge before it). The estimator was fed only once the detector said "discharging", which needs a fall of 0.3 mV a minute in twelve minutes. A clock that draws 10 mA never shows one, so after any restart on the battery there was no estimate, at any charge level. It is now fed whenever no charger shows (battest::Gate): the state is neither charging nor full. The Left line says "no drain to measure (on a charger?)" where nothing falls, and the Info page "on battery?" where it said "unknown". Cost: a clock that is started on a charger which has already finished shows the tick after about three hours instead of eleven minutes. tools/tests: the detector's setups now charge the cell to full instead of starting flat; new cases for a start on the battery at 99 % (0.03 to 0.12 mV a minute, sixteen hours, never full) and on a finished charger; the detector, gate and estimator wired as in the sketch give a figure within the hour and the right rate after five. 19 mutants, all caught. charge_stats: 0 false "full" in 1200 simulated sixteen hour runs. Simulated noise, not the board's: none of this has run there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
To see what a power setting saves, one run is made with each on the same cell
and the clock's own readings are written down. docs/power-log.csv is the
sheet for that: one line per reading, with the settings of the run, what the
two info pages show, and room for a meter's figure. It comes with a line for
each of five runs (stock with and without WiFi, a middle ground, the two
lowest). The README says how a run is made and where each column is read
("Logging a battery run").
The Left line gave the rate with one decimal. With the radio mostly off the
whole figure is 0.2 to 0.7 %/h, so two settings a third apart read the same:
it has two decimals now (below 10 %/h), it keeps giving the rate when the time
left is beyond a month (">30 d (0.11 %/h over 480 min)" where it said "more
than a month at this rate"), and the Current line has a decimal below 100 mA.
Nothing else changes in the firmware, so a run made with 1.5 compares with one
made with 1.6.
tools/tests reads the sheet as it will be read later: every line needs as many
fields as the heading, the heading's names have to be explained in the README,
and a column named after a setting may hold only what the settings file would
take (so "synch" or 30 MHz in a hand-filled line is named). The checker is
tried on made-up sheets that are wrong in one way each.
docs/info.png and docs/power.png show the new lines; their sample figures now
agree with each other (87 % of 2500 mAh at 30 mA is three days), and the Power
page sample has the rows in the order of the real page.
Not run on the board.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read on the board: 4.223 V on the charger, 4.217 V seconds after it was pulled, 4.210 V a minute later. A cell just off the charger sits above the top of the discharge curve, and so does any cell whose charger holds a shade over 4.20 V or whose calibration is a shade high. The gauge stops at 100 % from 4.20 V up, which is right for a gauge, and the estimator took its percentage from the same function. While such a cell came down to 4.20 V (ten or twenty millivolts: hours, at 0.05 mV a minute) it saw a level that stood still, reported no drain, and then averaged the hours after with that. A run started from a full charge, which is how the battery runs are meant to be made, read far too low for most of a day. calc::batteryPercentOpen() carries the curve on above its top with the slope of its last stretch (10 mV to the percent), and the estimator uses it; the time left is still worked out from no more than 100 %. tools/tests: the curve above the top; an estimator fed 4.215 V falling 3 mV an hour; and the detector, gate and estimator on that trace with noise, which gave no figure at all before. 2 mutants, both caught. README: the Versions entry of 1.6 no longer says that nothing but the display changed; a reading a little over 4.20 V on the charger is not proof of a wrong calibration. Not run on the board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Battery runs are compared by the readings of the two info pages, which had to
be copied by hand into docs/power-log.csv. With power_log = on (off by
default; POWER_LOG in secrets.h) the clock writes them down itself: one line
of CSV ten seconds after every start and one every ten minutes, in the FAT
partition of its own flash ("ffat" in the partition scheme the README asks
for, which nothing used until now).
The lines have the sheet's thirty columns, by the same names and in the same
order, so they paste under the sheet's heading and one script reads both; a
test compares the two headings. What the clock cannot know (the name of the
run, whether the card is still in, a meter's figure) stays empty. The first
line of a run says why the clock started.
Getting it out: a start with an SD card in copies the log to the card as
ESP32-S3-RLCD-PowerLog.csv, once the card's settings are in force, so a card
that switches the log off still gets it; "powerlog" in the serial console
prints it and "powerlog clear" forgets it. A Log row on the Power and
settings page says how much there is and when the last line was written, or
what is wrong, and "power" says how long the last line took to write.
Kept cheap. The log is two files: when the newer one reaches half a megabyte
the older is deleted and the newer takes its place, so the newest 0.5 to 1 MB
stay (three to six weeks) and nothing is ever written out a second time. The
partition is mounted once a run and stays mounted: unmounting makes the IDF's
wear levelling move a sector every time and mounting reads its position
records one by one, so doing both for every line would cost several times the
line. A line rewrites two 4 KB sectors (the end of the file and the directory
entry), 0.1 to 0.3 s of flash activity, begun right after a second has started
so that the next one is drawn on time; by datasheet figures about 5 mA*s,
0.008 mA on average. A clock that never had the log on never touches the
partition, and a failed mount is cleaned up by hand (the IDF leaves the wear
levelling running).
power_log.h holds everything that also runs on a PC: the line, when one is
due, the steps on the two files (against a small file interface), the POSIX
calls behind that interface, and the rows of the page. tools/tests runs the
steps on a made-up file system that can be told to fail and on a real folder,
and puts every line through the sheet's own checks; 113 single changes to the
header were each caught by them.
power_log is the 32nd setting, so the userSet mask is full: the next one
needs a wider mask.
Not run on the board, none of it: the FAT file system in the flash, the first
format, how long a line takes, flash writes at 20 MHz, the copy to the card.
The README says so item by item, and what to look at first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The power log's test copies the longest status row for its report. Built without the sanitizers the compiler took the two rows of the array for one string and warned that it might be cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KEY and BOOT held down together for ten seconds bring up a cube that turns, with copies of itself falling away inside it for ever (an infinity mirror in wireframe, white on black), until a button is pressed or a minute is over. It is in no legend and not in the README, on purpose. buttons.h: ChordHold says when two buttons have been down together long enough. What matters more is what must not happen meanwhile: KEY alone restarts the clock after five seconds and BOOT alone inverts the screen after one. From the moment both are down until both are up and quiet again, neither button's own gestures count, the restart hold is cancelled (HoldRelease::cancel, also when KEY had already armed it and BOOT then joined), and the press that ends the cube is not a click. infinity_cube.h is the geometry (the lines of a frame at a time, for a screen of a size); ui.cpp draws them. While the cube is up nothing of the clock is drawn; the battery is still read, so the charge detector does not start afresh, and the processor stays at its working clock. tools/tests: the two detectors, the restart hold and the chord wired as in the sketch (together, a little too short, one after the other, one let go in the middle, across the wrap of millis); the geometry. The UI preview measures three minutes of rendered frames against the edges of the screen, with a negative control. 12 single changes to the two headers were each caught (one by a crash). Not run on the board: how many frames a second it manages there is not known (a frame is about 130 lines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What KEY and BOOT bring up after ten seconds is no longer a wireframe. There
are two cubes now, and both are solid: only the faces that look at you are
drawn.
* One with sand on its walls (sand_cube.h): every face has its own grains in
a grid of 28 by 28, and they run to whichever side of the face is down the
screen at the moment, heap up there, and pour away as the cube turns on.
* One of mirrors (infinity_cube.h): each face is a window on the lattice of
the cube's own lit edges, reflected without end, clipped to the face in
three dimensions and fainter with every reflection (solid, dashes, dots,
nothing). A pulse of light comes up out of the depth now and then.
They share one tumble (infcube::tumble): about all three axes at once, each
now faster, now slower, now the other way, by slow swings that a seed picks
anew for every showing. It is a function of one number, so counting that
number down turns the cube exactly back the way it came.
While a cube is up, a click of KEY does that (turns it back), a click of BOOT
shows the other cube where this one is, and a long press of either brings the
clock back, as do two minutes without a button. Before, any press ended it.
tools/tests: the buttons wired as in the sketch, with the cube taking their
gestures while it is up; the tumble (never skewed, never a jerk, every side
comes to the front, another seed another way); the sand (none lost in ten
minutes of tumbling, it ends up where down is on the screen, a flat face
keeps it still); the lattice. The UI preview measures three minutes of each
cube, for two seeds, against the edges of the screen. 22 single changes to
the three headers: 20 caught by a check, 2 by the test program falling over.
Not run on the board: what frame rate either cube reaches there is not known.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The outline of the analogue clock was two circles, one inside the other (r and r - 1). Each is rounded to the pixel grid by itself, and where the two round differently a pixel between them stays empty: the owner saw them on the panel as single-pixel holes. ui.cpp now draws the outline as one ring, every pixel whose middle lies between r - 1.5 and r + 0.5 from the centre, in runs. The UI preview measures it: over twelve times of day no pixel of that band is missing and none lies outside it. Its negative control draws the two circles again and finds 32 pixels of the band left white. Five of the README's pictures show the clock and are rendered anew. While a cube is up, a click of KEY no longer turns it back the way it came. It gives it a shove (infcube::Shove): off in a direction that chance picks, at 260 degrees a second to begin with, dying away to a third in every 0.6 s, while the tumbling goes on underneath. The shoves add up, and what they have turned the cube by goes into tumble() beside the time. BOOT and the long press are as they were. tools/tests: a shove has its set speed whatever the direction, the directions are all different and average out to none, it comes to the same counted in one go or frame by frame, it is still going after a sixth of a second and over within two, and a cube with a shove in it is still three unit axes at right angles. Eight single changes to it, all caught. 1,903 checks in test_logic. Not run on the board: the outline is measured on the host's rendering only (the same drawing code), and the shove has not been seen there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hi @platima , Thank you for sharing this impressive project and for all the effort you've put into it! It's great to see the ESP32-S3-RLCD-4.2 being used in such a comprehensive application. Given the scope and complexity of the project, we'd suggest maintaining it in a dedicated GitHub repository rather than including it directly in our example collection. This would also give you more flexibility to continue developing and improving it independently. If you decide to create a separate repository, please feel free to share the link with us. We'd be happy to consider featuring your project in the Community Showcase section of our product documentation, so more users can discover it. Thanks again for your contribution and for supporting the community! Best regards, |
|
Hey Wulu,
Not a problem at all. I was going to create a separate repo anyway, but
would you like me to cancel this PR and submit a new one for just the
basic clock functionality with wifi & NTP?
Best,
P
|
A checklist of everything that has been tested on a PC but not yet on the board, 36 items, each marked seen, partly, not yet or problem. The statuses come from what the board's owner has reported and from the clock's own power log of 8 to 17 October (firmware 1.7 to 1.9, sync mode at 20 MHz), which settles several of them: the build id call, the charging and full indicators, the reading on the charger (4.28 V, against 4.20 V for a full cell off it), the log itself and its copy to the card. It also shows one problem: just after a charge the runtime estimate said 8 to 16 days, and the cell went from 4.20 V to 3.71 V in about 57 hours. The voltage barely moves for the first 12 hours after a charge, and the battery curve counts that stretch as 5 %. The README's "Not yet tried on the board" points to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the battery out, on USB at 5 V, the clock draws 21.1 to 21.4 mA at the 20 MHz idle clock, about 29 mA at 80 MHz and 33 to 36 mA with the radio on. With the radio on about 2 % of the time, the idle clock is nearly all of the energy. The cell is a good-quality 3400 mAh one. At that draw it should last roughly five days, yet 4.20 to 3.71 V took 57 hours, so at 3.7 V this cell is probably still about half full where the gauge says 15 to 20 %: the battery curve looks wrong at the bottom as well as the top. Item 12 now asks for a run down to the shutdown with the log on, which settles it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Summary
Adds
02_Example/Arduino/11_Clock_Dashboard, a desk clock for the ESP32-S3-RLCD-4.2 for the Arduino IDE (U8g2; the display driver is derived from10_U8G2_Test). It is one new folder; nothing else in the repository changes.What it does
.ino.binto the card and restart (hold KEY for five seconds); no cable, no WiFi. The file is checked first and installed into the second app slot, and it is kept only if it runs for a minute, otherwise the bootloader puts the old firmware back.Building
Same board settings as
Tools-Configuration.png, with PSRAM disabled and CPU 80 MHz (the example's README has the exact list and anarduino-cliline). It needs U8g2 (the copy in01_Arduino_Libraries/U8g2is fine) and ArduinoJson 7.x. It compiles as it is: the privatesecrets.his optional, because the WiFi details can come from the SD card.Status
Run on a real board: the first version (clock, weather, indoor sensor, battery gauge, display), the 80 MHz setting and its frame timing (1154 frames sent, none late), settings read from a file on an SD card, firmware installed from the card (twice so far, one version to the next), and the processor slowed to 20 MHz between syncs.
Only compiled and checked on a PC so far: the deep-sleep shutdown, how good the runtime estimate is, the switch to the backup network, the clock-drift measurement, the Spotify linking, and what the optional power settings save (not measured yet). The example's README goes through it item by item under Not yet tried on the board.
The PC-side checks are in
tools/: about 115,000 host-side checks (settings parser, moon phase, battery estimate and charge detection on simulated voltage traces, firmware-update decisions, the radio schedule over simulated days and more), a pixel-level layout test of the renderer in which every check has a negative control, and a fuzz test.Since this was opened
The branch has moved on (20 commits; the example is version 1.7, and its README has the full list under Versions). What was added, by version:
1.4
wifi_mode = sync(the radio on only for the weather and time syncs, with its own rule for a network that is out of range),cpu_idle_mhz(a slower processor while the radio is off),spotify_live(whether music keeps the radio on),console(the USB serial console shut down when no computer is attached), and the unused audio chips powered down at start-up.1.5
1.6
docs/power-log.csvis a sheet for logging battery runs.1.7
power_log = on(off by default) makes the clock write its own power readings into its flash every ten minutes and copy them to an SD card at the next start. Running on a board now; not yet confirmed there.The Copilot review comments on the first commit are dealt with (926d51c).
Notes
Generated with both Human and LLM input. Fixes and features added since it was opened are in this PR's later commits, as listed above.
-Platima