Let the simulator serve the clock's pages - #72
Merged
Conversation
Looking at the panel needed a board or a host process built out of the ESP32 backend - node in front of a binary whose httpd_* functions were fakes - and that was never a property of the pages. It was a consequence of the web front end having been written inside a backend. Now that WebFrontend is in the firmware, the simulator can answer the same routes, and ./build/bin/Wordclock opens http://localhost:8080/ beside its window. The transport underneath it is written out rather than pulled in: a desktop build has no framework to ask for a server, and what a browser needs is a socket, a little HTTP and RFC 6455's handshake - which is why there is a SHA-1 and a base64 in here, for a checksum a browser demands and nothing else. Polled from the wxTimer rather than run in a thread, and that is the design decision worth recording. Both hardware backends give their server its own task and pay for it with a lock-free buffer between the two; SerialShim buffers in a wxString, which would not survive that. Running task() on the tick means a browser's command is injected on the thread that reads it and a frame goes out on the thread that gathered it - no second thread, so nothing here has to be made thread-safe. It costs a write blocking the tick, which on loopback returns at once for everything this sends. Loopback only, and port 8080: this serves a console with no password on it, a desktop is not the place to open that to a network, and 80 would need root. A port already taken is not fatal - it says so and the window runs on, the same shape the ESP32's server failure has. No /update, because a host process has no second partition and no filesystem an image belongs in. That one route is still only reachable through platform/esp32/test/run.sh serve, which is why that harness stays for now. Two things the simulator was missing came with it: SerialShim gained inject() and a line sink, and the backend now provides WordclockSerial.h - the name the firmware reaches the port by. Fixed on the way past: finishOutputLine() only cleared its buffer when a window had attached, so a run without one would have grown a single line forever. Harmless while nothing else read that line, and not once a socket does. Verified by driving it: the panel renders from build/bin/Wordclock with 17 of 111 cells lit, "ES IST ZEHN NACH EINS", the clock at 13:12, the header on "connected" and brightness 255 read back over the socket rather than the unfilled default - so the handshake, the framing and the answer path all work. Every route answers, /nope gives a 404. The simulator's own tests pass, both host suites pass, and the AVR-Dx image is byte-identical at 48078/1700. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serve.js and webhost.cpp put a browser in front of the pages by running node
for the HTTP and the web socket, with a host build of the ESP32 backend behind
a line protocol on stdio. What that arrangement never did was exercise this
backend's server: node owned every socket, /update was node's own fake, and
the handlers a browser reached were the two the line protocol forwarded. So
what it proved was about the pages and the firmware core - which is exactly
what the simulator now does, with one binary, a real HTTP server and a real
web socket.
Gone with it: 371 lines across the two files, the serve branch and the webhost
link in run.sh, and node from the host-test job's requirements.
The one thing the harness could do and the simulator could not was answer
/update, so the simulator answers it first: nothing is installed, because a
desktop has no second partition, and it says how much arrived and refuses
anything too small to be an image. That is the same fake node was, and it
stands in for the same thing - the panel's progress and its two outcomes,
which is the part of that card nothing else reaches without a board.
Both answers were driven against build/bin/Wordclock: a 100 kB body gives
200 and {"ok":true}, a 1 kB body gives 400 and the sentence the panel shows.
Documentation followed to six files. Two claims had gone stale and are
corrected rather than moved: web/README.md counted the backends that serve the
pages as two, and CLAUDE.md still called them the one part of the project the
simulator cannot show. Added there instead: the first browser load after a
server starts can miss even a 20 s wait on the plate and every load after it be
instant - retry once before reading anything into it, which cost me a run
before I recognised it.
All four targets build warning-free, both host suites pass, the simulator's own
tests pass, and the AVR-Dx image is byte-identical at 48078/1700.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three findings and one build step, all from the same cause: WebFrontend moved into firmware/, which is where the analyser looks. The two expressions it flagged had been in platform/esp32/include, outside the filter, so the move did not introduce them - it exposed them. - FrameSize multiplied PIXELS_NUMBER_OF_LEDS by the colour count in int and widened the result to size_t. Neither operand can overflow an int here, but the analyser cannot know a display's size and is right to ask rather than to work it out. Now multiplied in size_t. - ChunkWriter::putNumber dropped snprintf's return. Taken and then used: it is the length, so the digits go out without a second walk for their terminator, and a negative - snprintf's encoding failure, unreachable for "%u" - is refused rather than trusted. The catalog is byte for byte what it was, 6430 bytes over 16 commands. - The simulator's smallest-plausible-image constant multiplied in unsigned int. The build step is the interesting one. The clang-tidy job configures without building, deliberately, since it only needs compile_commands.json - but WebPage.h is generated rather than checked in, so the one translation unit that includes it does not compile, and clang-tidy reports that as the file being missing. That reads like a bad include path rather than a build step that was skipped. The job now builds that one target, which is the python script and nothing else. Run locally the way CI runs it this time, which is what I should have done before pushing: run-clang-tidy over the same filter is silent, and so is cppcheck. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage D. Looking at a page no longer needs a platform —
./build/bin/Wordclockopenshttp://localhost:8080/beside its window, and the node harness that used to do that job is gone.Two commits: the simulator's server, then the removal it made possible.
Why the old harness could go
serve.jsandwebhost.cppput a browser in front of the pages by running node for the HTTP and the web socket, with a host build of the ESP32 backend behind a line protocol on stdio. That arrangement never exercised this backend's server: node owned every socket,/updatewas node's own fake, and the only handlers a browser reached were the two the line protocol forwarded. What it proved was about the pages and the firmware core — exactly what the simulator now does, with one binary, a real HTTP server and a real web socket.Gone: 371 lines across the two files, the
servebranch and the webhost link inrun.sh, and node from the host-test job's requirements.The design decision worth reviewing
Polled from the wxTimer, not run in a thread. Both hardware backends give their server its own task and pay for it with a lock-free buffer to the firmware.
SerialShimbuffers in awxString, so mirroring that here would have meant retrofitting mutexes into the simulator's port. InsteadWebHost::task()runs on the tick: a command is injected on the thread that reads it, a frame goes out on the thread that gathered it. No second thread, so nothing here has to be made thread-safe.The cost is stated rather than hidden: a write blocks the tick. Everything written goes to loopback, where the largest of them — a 70 KB page — fits in the socket buffer many times over. A client that stopped reading entirely could stall the window; a development tool may pay that, a clock on a wall may not.
Also deliberate: loopback only, port 8080 (this serves a console with no password; 80 would need root, and a taken port is not fatal), and a SHA-1 written out because RFC 6455 makes the handshake the base64 of SHA-1(key + GUID) — a checksum a browser demands, not a secret, and a dependency for eighty lines of arithmetic would be a poor trade in a build that otherwise needs only wxWidgets.
/update, honestlyThe one thing the harness could do and the simulator could not. It answers now: nothing is installed — a desktop has no second partition — and it reports how much arrived, refusing anything too small to be an image. The same fake node was, standing in for the same thing: the panel's progress and its two outcomes.
Both driven against the binary: a 100 kB body →
200and{"ok":true}; a 1 kB body →400and{"ok":false,"error":"1000 bytes is too small to be a firmware image"}.Verified by driving it
build/bin/Wordclock: plate 350×256, 17 of 111 cells lit, "ES IST ZEHN NACH EINS", clock 13:12, header "connected", brightness 255 read back over the socket (not the 128 default) — so handshake, framing and answer path all work. No page errors./nope→ 404.What the simulator was missing
SerialShimgainedinject()and a line sink, the backend providesWordclockSerial.h, and CMake gained a rule to generateWebPage.h(until now only PlatformIO and the test harnesses produced it).Fixed in passing:
finishOutputLine()cleared its buffer only when a window had attached, so a run without one grew a single line forever. Harmless while nothing read that line — not once a socket does.Documentation
Six files. Two claims had gone stale and are corrected rather than relocated:
web/README.mdcounted the backends that serve the pages as two, andCLAUDE.mdstill called the pages "the one part of this project the simulator cannot show".Added to
CLAUDE.md: the first browser load after a server starts can miss even a 20 s wait on the plate while every load after it is instant — retry once before reading anything into it. That cost me a run before I recognised it, and the same paragraph already says as much about the MIT-SHM error.🤖 Generated with Claude Code