Repository navigation
Releases: iamankushpandit/Gume
Release list
5.12.4 — Braino!
The screen turns to all four positions. Settings → Device offered
Landscape and Portrait, which are the two positions with the USB socket where
the board profile puts it. An enclosure that holds the board the other way up
had no answer, and there are many such enclosures for these boards (asked for
in issue 191). The same
button now steps through four -- Landscape, Portrait, Landscape 180, Portrait
180 -- split left and right like the theme button beside it, so one tap too
many is one tap to undo.
Portrait still applies to the menus only, because every game is drawn for
landscape. The half turn applies to everything, games included: landscape
turned over is still landscape. Touch needed no change -- pollTouch() has
mapped all four rotations since the first board, and two of them had simply
never been asked for.
It is one new global setting beside the layout, off by default, so a console
that upgrades sits exactly as it did. set flip on|off sets it from the
serial console, the boot banner prints flip=, and System Info's Layout row
names the position. Nothing about it is transmitted. The touch calibration
wizard is unchanged and still draws in its own fixed rotation.
Percent stops announcing the answer. In the round that asks a player to
shade the circle, the OK button turned green the moment the shaded amount
matched the target -- before anybody had pressed it -- so the round could be
finished by tapping + until a button lit up, and nothing was being asked
(issue 183). OK is now
one colour whatever the circle shows. Pressing it on the wrong amount turns it
red for half a second; before, a wrong press there changed nothing on the panel
at all. The minus and the tick on that round's buttons were characters the
panel's font does not carry, so they are now - and OK.
The same game gave the answer away in two more places, and both are closed.
The round that asks "25% of 40 = ?" printed 10 in the middle of the circle,
beside the four buttons asking for it; the circle now carries only its shading.
And in both multiple-choice rounds a wrong tap flashed the correct button --
in red, so it told the answer and mislabelled it at once. The button that was
pressed is the one that flashes now.
Flashing
Easiest route is the web installer, which flashes over Web Serial from a Chromium browser and needs nothing downloaded from here.
The binaries below are for flashing by hand. FLASHING.txt has the commands, the offsets and a note on what an erase_flash costs you; SHA256SUMS.txt verifies a download. Each environment ships as a single -merged.bin to write at 0x0, and as the four separate parts.
Built from v5.12.4, commit 8dcc988. Every image reports that on About's This build page.
5.12.3 — Braino!
No ages. The documents put numbers on the people using the console: a
range beside every game in the README, and ages throughout the design notes.
They are gone. A game's row now says what it is and what it builds. Where a
note compared two players, it compares a younger player with an older one;
elsewhere it says young player. The checklist in CLAUDE.md and AGENTS.md
no longer asks for an age with a new game. Prose only: nothing on the panel
ever showed one.
Players are players. The comments and documents described the people
holding the console by age rather than by what they are doing on it. They
now say player, or young player where the age is the point of the sentence --
a finger on a resistive panel, a first chess game, a word being traced. It is
prose throughout: no string on the panel changed, so the console behaves
exactly as before and its image differs only in the build stamp.
Four occurrences are not about people and are left as they are: the NVS key
that stores how many profiles exist, which a rename would reset to zero on
every console that upgrades; a search-tree variable in the chess engine; a
CSS selector in the landing page; and one of the short words Trace asks a
player to write.
Flashing
Easiest route is the web installer, which flashes over Web Serial from a Chromium browser and needs nothing downloaded from here.
The binaries below are for flashing by hand. FLASHING.txt has the commands, the offsets and a note on what an erase_flash costs you; SHA256SUMS.txt verifies a download. Each environment ships as a single -merged.bin to write at 0x0, and as the four separate parts.
Built from v5.12.3, commit 1e2a11f. Every image reports that on About's This build page.
5.12.2 — Braino!
Administrator, not adult. The nearby-play lobbies told a player that
"an adult" could switch Beacon and Nearby on, and the docs said the same about
whoever holds the admin PIN. The role is the administrator -- it is what the
PIN, the profile and Settings already call it -- so that is the word now,
everywhere it names the role.
The lobby note could not simply take the longer word. It is drawn in font 1,
six pixels a character and unfitted, on a 320-pixel canvas, and four of the
five were already wider than that: "Nobody nearby. An adult can switch Beacon
and Nearby on." is 336px, so Chess and Sea Battle were losing characters off
both ends of the panel before this change, and Backgammon and Go at 324px
were losing one. All five now carry Ludo's shorter wording, "An administrator
can switch Beacon and Nearby on." -- 294px, inside the canvas with room to
spare.
Two occurrences are left alone because they are not the role: adulterate
in the GRE word list, and a comment in ChessGame.h about chess programs
written for adults.
A licence notice may not break the file it is written into. Three of the
repository's four SVGs did not parse, and tools/check_licenses.py -- the
tool that exists to protect them -- is what broke them. Two faults: --
cannot appear inside an XML comment, and the notice uses it as an em dash;
and prologue_len() counted lines, so an SVG whose DOCTYPE wraps across two
got the notice spliced into the middle of it. A browser neither renders a
malformed SVG nor reports one, so the image is simply absent -- the landing
page's header logo shipped as a broken image beside its own wordmark, and
tools/braino-badge.svg, which the firmware's product mark is generated
from, was unreadable too. The checker now parses every tracked SVG and fails
on one that will not render.
The mock-ups draw the real Home and Settings icons. The generator drew
the word "home" in a box and a bare circle for the gear; the device draws a
house and an eight-toothed gear. The top bar is on every still, so the
placeholder appeared 91 times in the strip a reader looks at first. Both are
now ported from Ui::drawHomeIcon() and Ui::drawGearIcon() rather than
approximated -- the same repair as the launcher tiles above, one level up.
The landing page shows the product, on the boards people own. A lineup
of the supported sizes built from the case meshes, so the proportions are the
hardware's rather than a drawing's; and Braino running on bare boards,
composited onto the manufacturers' own front views, three boards in three
different themes. A board needs neither case nor battery to run this, and the
page now shows that instead of claiming it. The header carries the product
mark itself. in-hand-flags.jpg was shot under an orange lamp and has been
relit. The flasher is untouched: the same pickers, the same ids, the same
manifest.
The launcher mock-ups draw the real icons. Every launcher still -- the
wide one, the tall one and the 3x3 portrait grid -- drew a plain blue circle
in each tile, where the device draws cards, coins, a tricolour, a sudoku grid,
a ruler. Those three images are the picture of the product in the README
gallery, on the installer page and in every theme sheet, so the front page has
been showing a launcher that does not exist. tools/gen_screens.py now ports
the nine front-page icons from src/ui/LauncherIcons.cpp, keeping that file's
palette, its two-pixel stroke rule and its 36px box rather than inventing new
ones.
It was found by trying to composite a generated still onto a photograph of a
real device: the photo showed icons and the mock-up showed circles, which is
the comparison no check performs.
An icon that is not ported yet now stops the build rather than falling
back to a circle. The silent fallback is exactly how this shipped -- a
placeholder that looks deliberate is indistinguishable from art, and it sat on
the landing page for months.
There is no 5.13.0, and there will not be one. 5.13.0-SNAPSHOT was opened
after 5.12.1 and then published to main as the landing-page change rather
than as a release, so the number was spent without a tag ever carrying it.
Reusing it would mean two different sets of bytes answering to one version --
the snapshot some people flashed from the installer, and a later release --
which is the one thing a version number exists to prevent. The next minor
release is 5.14.0; this one is a patch on 5.12, so it is 5.12.2. A gap in
the sequence costs nothing; an ambiguous number cannot be repaired after the
fact.
A new landing page. The installer page now leads with what the thing is
rather than with a wall of mock-ups: the 4 MB line, a one-minute video served
from the site itself, and one chapter per question a reader actually arrives
with -- which board, what it teaches, who decides what a child can open, what
two consoles do in the same room, and what the case costs to print. The
flasher is unchanged behaviour for behaviour: the same board/firmware/version
pickers driven by the same generated BUILDS list, the same per-board
esptool command, the same warning when an older release is selected.
Ninety-one mock-ups became six. The old page showed every generated still
it had -- a wall of twelve at the top, three on each of the forty game cards,
three on each of the nine system screens. They were honest and they were
exhausting, and a reader deciding whether to buy a board does not need to see
the Settings sound tab. The page now shows six chosen ones, larger, and the
game list is simply every game's name, set as type. The README gallery is
still the place for all of them, and it is linked from the section.
Nothing became hand-written in the move. The names, the counts, the board
list, the version, the build figures and the six stills are all still filled
by tools/gen_site.py from the firmware, and check_docs.py still fails if a
placeholder is dropped or a version number is typed into the template. One
check did change meaning: a still on disk that the site does not show is no
longer an error, because the site deliberately does not show most of them.
What that rule protected has not moved -- check_screens() still fails when a
still has no generator entry, when a generated one is missing, and when a
playable game has no screenshot at all.
The page says how to reach a person. A Contact section, and the same two
addresses in the footer: the GitHub account and esp32.gume@proton.me, with
an issue named first because it is public and the next person with the same
board finds the answer.
The photos, the hero video and the A2 poster live in site/assets/ and are
published from the Pages origin, so the page still fetches nothing from a
third party on load except the Install button's own code. YouTube is a
click-to-play facade: nothing from Google is requested until the reader
presses play, and then from youtube-nocookie.com. gen_site.py checks the
asset folder both ways, the way it already checked the stills -- a referenced
file that is not there is a broken image on the landing page, and a file
nothing references is weight in every clone.
Flashing
Easiest route is the web installer, which flashes over Web Serial from a Chromium browser and needs nothing downloaded from here.
The binaries below are for flashing by hand. FLASHING.txt has the commands, the offsets and a note on what an erase_flash costs you; SHA256SUMS.txt verifies a download. Each environment ships as a single -merged.bin to write at 0x0, and as the four separate parts.
Built from v5.12.2, commit 583d148. Every image reports that on About's This build page.
5.12.1 — Braino!
A licence release. The console does exactly what 5.12.0 does. Across
src/, include/, lib/ and test/ this change removes no line and adds
only comments, so nothing compiled behaves differently -- what moves in the
image is the version string and the build stamp, and the flash figure is
5.12.0's to the byte. It is tagged rather than held on dev because a
notice nobody has a copy of is not a notice.
Every file now states its own licence. The repository has been
GPL-3.0-or-later since it was published and LICENSE, NOTICE.md and the README
all said so, but none of that travels: what reaches a stranger is one file --
a .cpp pasted into a forum answer, a script copied into another tree, a page
of documentation lifted into a wiki -- and an unmarked file carries no author,
no terms and no way back to either. Every source file, header, script,
workflow, platformio.ini, the site template and the SVGs now carry an SPDX
notice naming the licence, the holder (iamankushpandit), where the work came
from and what reuse requires; documents carry the same four facts as a visible
footer, because a reader lifting a paragraph out of a rendered page never sees
an HTML comment. Nothing about the licence itself changed -- the grant is the
one that was already there, written where it can be seen.
tools/check_licenses.py is both the check and the fix (--fix), runs in CI
on every pull request and at release, and is imported by every generator so
that regenerating a table cannot quietly strip the notice.
Flashing
Easiest route is the web installer, which flashes over Web Serial from a Chromium browser and needs nothing downloaded from here.
The binaries below are for flashing by hand. FLASHING.txt has the commands, the offsets and a note on what an erase_flash costs you; SHA256SUMS.txt verifies a download. Each environment ships as a single -merged.bin to write at 0x0, and as the four separate parts.
Built from v5.12.1, commit 98553de. Every image reports that on About's This build page.
5.12.0 — Braino!
Two new games, and two old ones taught to get harder. The catalogue is 40.
Space asks forty-four questions about the solar system and the layers of
air above it, and answers every one of them. Right or wrong, the tap is
followed by a plain sentence: that Venus beats Mercury because its clouds hold
the heat in, that the ozone doing the work is in the stratosphere, that the
Space Station orbits inside the thermosphere. The atmosphere is in there with
the planets deliberately -- "where does space start" and "how far up does the
air go" are one question, and a quiz that answers one and not the other leaves
the join out. Three levels, so a five-year-old is asked which planet is red and
never about the Karman line.
Roman runs both ways: read a numeral, or write a number as one. The symbol
key and the rule for the level stay on the screen the whole time, because at
this age reading the notation is the skill and remembering seven letters is
not. Every answer is followed by the number taken apart -- XIV = X + IV = 10 + 4 -- and the wrong answers are the mistakes children actually make rather than
four numbers picked at random: XIV read as 10 + 1 + 5, and 44 written as
XXXXIV. Five levels, each adding the next letter.
Math now asks word problems. From level 2, about one question in three
arrives as a sentence -- "Nia has 21 shells and gives 8 away. How many are
left?" -- over the same arithmetic, the same four buttons and the same scoring.
Level 1 stays pure sums on purpose: a child still working out that 3 + 4 is 7
is being asked to read as well, and at that age the reading is the harder half.
Shapes has four rounds instead of one, each taking an easy cue away. The
four everyone knows, then four told apart by side count -- pentagon, hexagon,
heptagon, octagon -- then the lookalikes a child calls circle and square until
somebody makes them look twice, and finally concave against convex with the
word on the row. Level 4 always deals two of each family, because the word is
only a lesson when it tells two rows apart.
Going idle ends an admin session. An adult who unlocked the admin profile
kept it across the screen saver and across panel sleep, so the console could be
put down with Settings open and picked up by anybody: every device switch, the
per-player game lists and the profile controls, with no PIN asked. begin()
already refused to boot into admin for exactly this reason -- being admin is
not evidence about who is holding the device -- and an idle timeout is the same
fact arriving a different way. Both idle entries now drop to Guest, so the
saver, panel sleep, the Lock button and the lock screen's own timeout are all
covered. The lock screen never closed this and was never meant to: it is an
accidental-touch guard whose hold is deliberately not a secret.
Coming back therefore starts the screen over rather than resuming it, which is
the answer the serial console already gave when a profile changed underneath a
screen. That is not tidiness. Settings' change-PIN pad sits above that
screen's own admin gate, because only an admin can reach it -- a resumed pad
would have handed the next person the PIN itself.
And the Math question panel stopped eating its own border. The panel
occupied rows 76..129 and the prompt strip below it was cleared from row 125,
so every answer erased the bottom five rows of the question -- its border
included -- and nothing put them back until the next question. It had been
there since the screen was split, on every board, and it cannot appear in
docs/screens/ at all, because a mock-up draws elements in isolation with no
clear rectangles. The stack is derived from one set of constants now, with two
static_asserts that fail if the overlap is reintroduced.
Flashing
Easiest route is the web installer, which flashes over Web Serial from a Chromium browser and needs nothing downloaded from here.
The binaries below are for flashing by hand. FLASHING.txt has the commands, the offsets and a note on what an erase_flash costs you; SHA256SUMS.txt verifies a download. Each environment ships as a single -merged.bin to write at 0x0, and as the four separate parts.
Built from v5.12.0, commit 3cfe809. Every image reports that on About's This build page.
5.11.0 — Braino!
About calls two-console play experimental, on the device. It is the one
thing shipping here that has not been exercised end to end on two boards since
the defects in it were fixed, and the note below has now carried that check
over three releases. Saying so where the owner reads it is the honest way to
ship it: a feature that sometimes fails to connect is a disappointment when it
was promised and a curiosity when it was labelled. The claim being qualified is
two consoles, not the radio -- the beacon, the Nearby list and a poke are
mature. The line comes out when the check has actually been run.
Chess has a computer opponent, at two levels. It is the only strategy
game here that did not -- Go has two levels, Ludo two, Backgammon one -- so a
child with nobody to play sat in front of a board that could not move. The
lobby gains a Play the computer row and two chips: Level (Easy or Medium) and
which colour you take (White, Black or Random, resolved once at the start and
then stored as the colour, so a restored game cannot hand you the other side).
ChessGame.h used to state that there was no computer opponent and that this
was a decision rather than an omission, because a search worth playing "would
have to run across frames or on its own task". That was the answer rather than
the objection: Go had already shipped exactly it. Medium is alpha-beta to three
plies, split at the root so one root move is scored per slice and the screen
stops when the frame's 4ms budget is spent -- a partial result can never be
mistaken for a finished one. Easy is one ply plus a look at what can be taken
straight back, so it will not hang its queen on move four, and it chooses at
random among moves that score alike so it does not open identically every game.
Both are deliberately weak. A console that beats a seven-year-old every time is
a worse product than one they beat half the time.
The rules are now pure, and provably. ChessRules.cpp carried a comment
claiming it could be compiled on the host "if it were ever needed to" while
including ChessGame.h, and therefore Game.h, Ui.h and TFT_eSPI -- the
function bodies were honest, the translation unit was not, and nothing could
tell the difference until something tried. The rules moved into namespace Ch
in ChessRules.h with the engine beside them in ChessAi.cpp, both including
<stdint.h> and <string.h> and nothing else, and
test/host/chess_rules_test.cpp compiles them with a host g++. The test is
what proves the purity, since it will not build if an Arduino header returns.
Chess saves grow two bytes and SAVE_VERSION goes to 4, so a game in progress
across the upgrade is retired rather than misread -- which is what the fixed
layout is for.
Find my Braino. The Nearby screen's peer list gains a Find beside each
Poke. A poke was only ever a blip and a banner -- fine for nudging somebody
who is holding their console, useless for the case everybody actually has,
which is a console down the back of a sofa. A find makes the far end ring a
bell every 1.5s for 20s, blink its case LED and wake its screen, and any touch
or the BOOT key stops it.
Nothing new goes on the air. A find is the same poke with one reserved flag bit
set -- the payload is already exactly 31 bytes and had nothing to give -- so
what changed is entirely what the receiving console does about it. The layout
version is deliberately not bumped: a reader rejects the whole manufacturer
block on a version mismatch, so bumping it would have made consoles either side
of this release invisible to each other in Nearby, while a console that does
not know the bit simply pokes as it always did.
A find will unmute a muted console, and mute it again when the alert ends.
A console that has been silenced is precisely the one that cannot be found, so
the alert flips the switch to answer and NearbyPlay::dismissAlert() -- the
single place an alert can end, whether by touch, by the BOOT key or by timing
out -- puts it back. The one-gate mute rule is intact: nothing reaches past
Board::playSound(); the switch itself genuinely changes and genuinely
returns. Settings > Sound > Find alert: Quiet opts out, leaving the banner
and the LED.
Sound::Bell is the first cue in the vocabulary meant to be heard from another
room rather than by whoever is holding the device.
Three refactors landed first, each its own commit and none of them changing
behaviour: BleBeacon.cpp (648 lines) splits into the radio and
BleBeaconPayload.cpp, which now holds the advertisement's one description and
decode(), its exact inverse, alone in one file; AppRuntime.cpp (671) gives
up the header banner to AppRuntimeNotify.cpp, where the three things that can
raise one and the priority between them sit together; and BoardAudio.cpp
(1456) becomes the synthesiser, BoardAudioBackend.cpp for the codec, I2S,
amplifier and DAC, and BoardAudioCues.cpp for the vocabulary -- so adding a
sound now touches one file and no hardware.
The Wi-Fi badge no longer floats away from the network it describes. On
the Wi-Fi screen it was pinned at the right-hand edge of the design while the
SSID was left-aligned, so it sat alone with the width of the row empty between
them -- it was reported off the panel as an icon hanging in air, and portrait
made it worse, because that screen stretches a 320x240 design onto the panel
one axis at a time while the glyph stays a fixed 15px. It is now placed off
the measured width of the SSID and clamped at the right, which is what the
sync badge on the row below already did.
The system apps have portrait mock-ups. docs/screens/ had exactly one
portrait image out of 73 -- the launcher -- while CLAUDE.md requires every
system app to work in both orientations, so for Wi-Fi, Settings, Scores,
About, System Info and Profiles the portrait half of that rule could not be
checked without flashing a board. tools/gen_screens.py now renders all six,
each restating its own screen's layout arithmetic at 240x320 rather than
re-imagining it, so a number that does not survive a narrow panel shows up as
a broken picture instead of a plausible one. Two already do: the Scores best
and worst columns are drawn at a hard-coded x=244 and x=306, both off a 240px
panel, and the top bar's title floor lets the title run under the clock.
Neither is fixed here; the mock-ups are what make them visible.
Both of those are now fixed. Scores draws its value columns off the row's
own right edge rather than at a typed-in x, and offers the second column only
when the row can hold two numbers beside a game name -- a truncated score is
not a smaller score, it is a wrong one, so a 240px panel shows bests alone
rather than two half-numbers. The Device tab had the same fault at x = 236 and
x = 312 and is fixed with it. The top bar no longer floors the title width at
32px: on a narrow panel that floor was wider than the gap before the clock, so
the floor won and the two were drawn on the same pixels. Where there is not
room for both, the clock gives way -- it is repeated on the launcher header
and in Wi-Fi, while the title is the only thing that says which screen you are
on -- and its sync badge goes with it, since a tick with nothing to qualify
says nothing. Landscape is untouched: at 320px the gap is never that small.
The system apps are no longer called games. WifiGame, SettingsGame,
ProfileGame, ScoresGame, AboutGame, SystemInfoGame, NearbyGame and
LauncherGame are WifiApp, SettingsApp and so on, files included. They
are Game subclasses because that is the screen lifecycle every screen here
shares, and they always will be -- but Wi-Fi is not a game, and a reader has
no way to tell which of the names in GameInstances are things a child plays
and which are the console's own screens. The playable catalogue keeps Game,
so the distinction now shows in the names. No behaviour changes.
Two placements the mock-ups and the panel disagreed about. The mute
control in the launcher's portrait header sat at lW-96, beside nothing and
above nothing, and read off both the 2.8-inch and the 4-inch as an icon
floating in an empty row; it now takes the padlock's column, derived from
lockRect() so the pair cannot drift apart. It stays on the profile-name row
rather than joining the badges, because at 240px that row is full by x=155 and
the padlock starts at lW-64 -- about 20px for an 18px glyph. And the lock
screen's battery badge is back at the top right, level with the middle of the
mark: in the bottom corner it shared a row with a footer that is centred across
the full width and deliberately picks the widest wording that fits, so the two
were laid over each other on a narrow panel.
Go. The thirty-eighth game: 9x9 everywhere, 19x19 on the 4-inch console
only (GoGame::BIG_BOARD_AVAILABLE, from the panel's physical width -- a
point on 19x19 is ten logical pixels from its neighbours and only a big
panel makes that a target). Five rule sets on one chip -- Capture 1, 3
and 5, first to take that many stones, the form taught in schools; Area,
stones plus surrounded points plus 5.5 komi; Territory, which ends with a
marking phase where both players agree which groups cannot live -- and two
computer levels: Easy is heuristics that never fill an own eye, Medium plays
random games from the best dozen points, a few per frame so a move takes a
moment and never a frame, and the same playouts are its opinion of the dead
stones. A stone is placed in two taps, ghost then confirm, because a
misplaced stone is for ever and a resistive panel misplaces; on 19x19 the
panel magnifies the five-by-five around the ghost and has a Place button.
Repaint is per point. Undo takes back your move and the computer's reply.
Nearby play rides the existing two-player service with an eleven-bit word
across its two six-bit fields, so a 19x19 point fits and the service's own
presence and ending words cannot...
5.10.0 — Braino!
Two new games, both playable across the room, on five boards that have
each been run on hardware. Ludo seats two to four -- any of them the
computer -- and plays across up to four consoles; Backgammon plays on one
console, against the computer or against a console nearby. Neither changed
what goes on the air: the dice are worked out on every console from a shared
seed and never sent. The two 2.8-inch resistive boards gained sound, the
battery badge shows its percentage and nothing it had to guess -- and none
at all on the dual-USB CYD, which has no battery hardware -- and a board can
now be asked what it is, or set up entirely, over its USB cable. 37 games.
Only boards that have run on hardware are supported: five, down from
seven. The ESP32-2432S028R (micro-USB, ILI9341) and the ESP32-2432S028Rv3
(ST7789) profiles were written from published pin maps on 2026-08-26, while
the dual-USB CYD was being brought up. That board turned out to be neither and
got its own profile, esp32-2432s028-inv, and the two guesses stayed behind:
built by CI, offered by the web installer as supported, and never once run on
a board. Their profiles, board sections, product and bench environments, and
installer entries are gone. What is left is exactly what was flashed and
checked for this release, and the README and the installer's picker now
describe each by what you can see on it:
| Screen | Touch | USB | Board |
|---|---|---|---|
| 4-inch | resistive | one USB-C | E32R40T |
| 3.2-inch | resistive | one USB-C | E32R32P |
| 2.8-inch | capacitive | one USB-C | Freenove FNK0104B, also sold as LCDWIKI ES3C28P |
| 2.8-inch | resistive | one USB-C | E32R28T-1 |
| 2.8-inch | resistive | USB-C and micro-USB | ESP32-2432S028, inverted panel |
A board comes back when someone who owns one runs it.
The dual-USB ESP32-2432S028 has no battery badge, because it has no
battery hardware. Its battery and RGB LED pins had been copied from the
E32R28T-1. Measured on the board with a pack and USB connected, GPIO34 read a
steady 0.21 V where a battery behind the assumed 2:1 divider would read about
2 V -- on a CYD that pin is the light sensor -- so the gauge had been showing
a light level as a charge. Its profile now declares no battery sense, and a
board without it shows no battery badge anywhere -- top bar, launcher, lock
screen, screen saver -- no low-battery warning, and one "None - use USB
power" row in System Info instead of four readings of nothing. The header
layouts, which already pack themselves off the badge's measured width,
close the gap on their own. Run this board from USB power; the README
suggests a power bank for portable use. The LED now follows the published
CYD order, red IO4, green IO16, blue IO17.
ESP32_boardUtil.py --flash no longer uploads to a port whose board has
changed. Ports were identified before the builds and uploaded to after them,
up to twenty minutes later, and COM numbers move in that time -- a replug, or a
USB power surge renumbering everything. A 4-inch E32R40T came back on the port
an E32R28T-1 had been identified on and received the 2.8-inch image: a dark
panel with a working speaker. Every port is now asked again immediately before
its upload and skipped unless it gives the same device id. And a board whose
device id was issued by one model's firmware (R40T-...) while it reports
another model (board=E32R28T-1) is refused as probably carrying the wrong
image, rather than being flashed with that wrong image again; the tags are
read from the board profiles through platformio.ini, not listed.
ESP32_boardUtil.py --flash checks that every board booted, and brings a
stuck 4-inch board back by itself. The reset at the end of an upload
regularly left the E32R40T in a ROM boot loop -- invalid header or flash read err, panel dark -- with an intact image in flash. After the uploads, still
holding the board lock, the tool now asks each board it flashed which build it
is running, without resetting it. One that is silent and whose serial shows the
loop gets a reset from download mode (esptool flash_id staying in the
bootloader, then read_mac with a hard reset), up to three times, which is
what had been bringing it back by hand; it writes nothing, and the MAC esptool
prints is discarded unread. A board still looping is reported as FAILED; one
running a build other than the commit just built is flagged.
Nearby stops repainting its list for every beacon it hears. The peer
table's change counter moves on every advertisement received, not only when
something about a peer changed, so with a few consoles in the room the list was
wiped and refilled several times a second to show the same words -- and each
refill looked up this player's score for every peer. RowList::drawChanged()
now remembers a hash of what each row drew and repaints only rows whose text
moved, overdrawing text rather than clearing it first; the list rect is wiped
only when rows arrive, leave or scroll, and a refresh that changed nothing
draws nothing. A heard advertisement rebuilds the list at most four times a
second, and pressing the Sharing switch no longer repaints the top bar. The
one-second refresh now runs with nobody in range too, so "Listening" no longer
sits on "Radio starting" until a peer turns up.
Ludo, for two to four -- and any of them can be the computer. The classic
race round the cross-shaped board, on one console. Each of the four seats is a
Player, a Computer or Empty; at least two seats and at least one person. The
computer plays at Easy (a random legal move, except that it always brings a
token out when it can) or Normal (capture, get home, come out, reach
safety, escape a threat, advance the leader -- one step of lookahead, no
search). The rules as played are stated in LudoRules.h; the ones that are
choices rather than the only reading are that the third six forfeits only that
roll, a capture earns a roll but reaching home does not, and blocks do not form
on safe squares, so no start square can be walled off.
- The dice are a function of the game's seed, not a stream. Roll k is
die(seed, k), so a move only has to name a token and anything that knows
the seed can check it. That is what a later, connected version needs, and it
costs nothing now: the rules are pure C++ with no Arduino in them. - A 13px cell is too small to aim at, so the tokens that can move light up
and a tap picks the nearest one; a move that is the only one plays itself. - Every colour has its own token shape -- circle, square, diamond,
triangle -- so the game does not depend on telling red from green. - Remembered, like Chess: saved after every roll and every move, including
a roll not yet used, so putting the device down is not a free reroll. End
game asks twice. - Ludo cannot be hidden per player, like Chess, Sea Battle and Cursive: its
launcher index is past the 32-bit visibility mask.
Backgammon, for two -- one console, the computer, or a console nearby. The
full race: roll, tap a checker and the points it can reach light up, Undo until
Done. Every forced-move rule is enforced -- as many dice as possible, the higher
die when only one of two can be played -- and a game ends as a single, a gammon
or a backgammon. The pip count shows how far each side has to go. No doubling
cube, and no score: a win is not a number.
- The computer plays every complete legal sequence through a position score
-- pip lead, made points (home points and runs of them more), opponents on
the bar, blots weighted by how many checkers can reach them -- and shows its
moves one at a time so a child can follow them. One level. - Nearby play is the lobby's third way to play, and it needed no change to
what is transmitted. An earlier plan ruled it out because the turn has no
field for dice; it does not need one. As in Ludo, both consoles derive every
roll from the session and their two tags, a move is the checker's two points
in the same turn Chess sends, and each move received is played only if it is
legal with the dice the receiver computed. - Pure rules, host-tested.
test/host/backgammon_rules_test.cppplays 400
games computer against computer and compares the legal moves with a
brute-force search over every move order on thousands of positions: 117,242
checks, 0 failures. - Backgammon cannot be hidden per player either; its launcher index is 36.
Ludo across up to four consoles, over the beacon Chess already uses. Nearby
in the Ludo lobby lists the consoles in the room; tap them to invite, add up to
two computers, and Start once at least one has joined. The console being asked
sees the invitation in its header and joins from its own Ludo lobby.
- Nothing new goes on the air. No new flag, no version bump, and consoles
on older 5.x firmware can still see this one. A Ludo turn is the same turn
Chess sends -- session, move number, two six-bit numbers and an ack -- read
as a seat and a token. The die is never sent: every console works each roll
out from the table's seed and refuses any move that does not fit it. Agreed
by the maintainer on 2026-09-10 in conversation rather than in an issue; see
the invariant inCLAUDE.md. - Who sits where and who moves first come from the seed, not from the
invitation's coin toss, so the console that set the table up does not choose. - A console replaces its move only once every other console has it, which
is what makes a bonus roll -- or the host playing a computer straight after
itself -- survive a scan window that missed something. - The service can now say which game an invitation is for
(NearbySeat::forThisGame), and every lobby now checks it. Before, the Chess
and Sea Battle lobbies showed any invitation in the room as "A4F2 invites
you" -- a Sea Battle invitation included -- and accepting it left two
consoles playing different games at each other. An invitation ...
5.9.1 — Braino!
A hotfix. 5.9.0 was withdrawn: it broke the display on two of the seven
supported boards, and its Freenove image could not boot at all. Three
defects, every one of them shipped by a change meant to make diagnosis easier.
The boot log's panel-ID read corrupted the display. 5.9.0 printed the
controller's ID register at boot. On the 2.8-inch E32R28T-1 and the Freenove
FNK0104B the result was a screen with the top bar crushed into a band at the
bottom edge and the rest of it never painted. TFT_eSPI's readcommand8() is
not a read: it writes an undocumented 0xD9 index command, toggles CS
mid-sequence, and restores neither the address window nor MADCTL. A panel
that answers gives you an ID; a panel whose MISO is not wired back is left
mid-command, and the next write inherits it.
What made this expensive to find is worth recording. The firmware was healthy
throughout — 48fps, a 23ms worst frame against a 20ms budget, a flat heap, no
stall, no panic, sitting on ctx='Profiles', a screen whose drawing code is
byte-identical between the two releases. Every instrument said the software
was fine and every instrument was right: the corruption was in the panel, put
there by the diagnostic. The field was worthless even where it appeared to
work — across all seven boards it only ever answered 00:00:00 (MISO absent)
or FF:FF:FF (floating high), and not one returned a real ID. It is removed
rather than guarded, and AppRuntime.cpp records what a future board would
have to do to earn it back.
The Freenove's merged image and its web-installer entry put the bootloader
at the wrong offset. pack_release.py hardcoded --chip esp32 and a
0x1000 bootloader offset for every environment, and gen_site.py wrote that
same offset into the manifest esp-web-tools consumes. The ESP32-S3 reads its
bootloader from 0x0, found padding there, and looped on invalid header: 0xff. Every part downloads, every hash verifies, the progress bar completes,
and the board is dead — so nobody could install the Freenove from the web
page for as long as it was offered, and the failure looked like a successful
install. Both tools now derive the offset per chip: pack_release.py reads
the chip id out of the ESP image header of the bootloader it is packing, so it
cannot drift when a board is added, and gen_site.py keys off the same chip
field it already hands over as chipFamily. An unknown chip is a hard failure
in both, because quietly defaulting to 0x1000 is exactly how this shipped.
FLASHING.txt claimed flashing preserves NVS. That is false for the merged
image. The four separate parts do leave it alone — nothing is written between
0x9000 and 0xe000. The merged image is one contiguous blob from 0x0, so
it necessarily paves that region with 0xFF, erasing profiles, scores and the
touch calibration. Right for installing onto a new board, wrong for updating
one somebody is using, and the file now says so per path. It was believed
during this very investigation and cost a development board its stored state.
Still outstanding, carried over from 5.9.0: a two-console regression check
of nearby play. Two defects in that path were fixed late in 5.9.0 — the
acceptor never published its ply-0 answer, and a peer's turn was recorded only
on the sighting that first brought it into range — so the code that shipped is
not the code that was exercised on two boards.
Flashing
Easiest route is the web installer, which flashes over Web Serial from a Chromium browser and needs nothing downloaded from here.
The binaries below are for flashing by hand. FLASHING.txt has the commands, the offsets and a note on what an erase_flash costs you; SHA256SUMS.txt verifies a download. Each environment ships as a single -merged.bin to write at 0x0, and as the four separate parts.
Built from v5.9.1, commit 77788a9. Every image reports that on About's This build page.
5.8.0 — Braino!
Piano and Chess — the first two apps here that are not drills, and the
first that adapt to both orientations.
Piano is one octave, C to C, with the note name on every key. No score, no
round, no right answer. Hold a key and it keeps sounding. It is offered on every
board: where there is no speaker the key still lights and still names the note.
Chess is the full rules for two players sharing the device — there is no
computer opponent. Tap a piece and every square it may legally move to is
ringed, which is both the answer to 26-pixel squares on a resistive panel and
the way you teach the game. Castling, en passant, promotion, check, checkmate
and stalemate are all there; legality is decided by playing each move and asking
whether it leaves your own king attacked, so pins and discovered checks need no
rule of their own. The pieces are silhouettes rather than letters, because a
letter is a literacy test — useless to the youngest players and to anyone whose
sight makes a small glyph hard.
Flashing
Easiest route is the web installer, which flashes over Web Serial from a Chromium browser and needs nothing downloaded from here.
The binaries below are for flashing by hand. FLASHING.txt has the commands, the offsets and a note on what an erase_flash costs you; SHA256SUMS.txt verifies a download. Each environment ships as a single -merged.bin to write at 0x0, and as the four separate parts.
Built from v5.8.0, commit fa941c7. Every image reports that on About's This build page.
5.7.0 — Braino!
The sound was broken on two boards, the beacon could crash the device, and
now the console tells you when there is a newer version.
Cues on the 3.2-inch and 4-inch boards played at a fifth of their length.
The ESP32's built-in DAC cannot clock at 16 kHz -- its divider wraps rather than
saturating below 22050 Hz -- so the synthesiser generated 16000 samples a second
while the hardware consumed nearly 89000. Every beep came out a fifth as long
and an octave and a half sharp, which is not heard as "too fast" but as a click,
or as a speaker that is failing. Measured on the bench across nine rates; the
rate is now 24 kHz on those boards, where it is exact. The Freenove is untouched
at 16 kHz -- its codec clocks off the APLL and was never affected. The spoken
boot phrase is intelligible on a DAC board for the first time.
Nothing in a log would have shown this. i2s_get_clk() reported a healthy
16000.0 Hz in every wrong case, because it returns what the driver was asked
for rather than what the peripheral does.
Switching the Bluetooth beacon off while the clock was syncing could panic the
device. Tearing the BLE controller down underneath live Wi-Fi coexistence
callbacks is a race -- consecutive runs alternated between a crash and a
survivable warning. Advertising now stops immediately, as it always did, and the
controller is released later, once Wi-Fi is idle. "Off" still means off on the
air the instant you ask for it.
A new "Updates" page in About, and a once-a-day notice. With Wi-Fi
configured, the console downloads one small file listing the current version of
every supported board, compares it with its own, and tells whoever is holding
the device: 5.8.0 available - ask admin to update. About shows what is
installed, what is available and where to get it.
Braino still does not update itself, and this is not a step towards it. No
firmware is downloaded and nothing is installed; a person decides, and does it
from the web installer.
This is the fourth thing that leaves the device, and the first added since the
BLE beacon. It was agreed as a change to what the product promises rather than
reviewed as a feature, and it was built so the promise is enforced rather than
stated. The request carries nothing about your device -- no version, no
board, no query string, so it is byte-identical to the one every other Braino
makes, which is why the file lists all boards and the comparison happens on the
device. And the address shown to you is compiled into the firmware, never read
out of the response, so a tampered answer can at worst display a wrong number.
check_privacy.py fails the build if either property is lost. The check is not
separately declinable: it runs when Wi-Fi is configured, and a console with no
Wi-Fi never makes the request.
Nine display themes. Dark and Light, plus Midnight, Dusk, Paper and a
high-contrast option -- and three period looks: Classic, Silver and Pocket.
They are palettes rather than skins, so they cost about 500 bytes in total. The
theme button now steps back on its left half and forward on its right, because
nine is too many to cycle one way.
Three things had to stop being constants to make those possible, and each had
been quietly closing off a whole class of theme: the top bar's text colour
(which is why Light kept a dark bar), the three launcher tile fills, and the
button corner radius. All are palette entries now.
Changing the theme left the old one behind. Settings marked only its
content dirty, so the tab strip -- painted with the background, not with the
content -- kept the palette it was drawn in. Going from Light to Dark gave you
light tabs above a dark screen. It repaints in full now.
The launcher left ghost tiles on a short last page. The empty slots were
erased, but drawButton paints its shadow offset down and to the right at the
same size, so it overhangs the tile it belongs to -- and the erase covered only
the tile. What survived was a two-pixel-wide, three-pixel-tall L in each empty
slot, belonging to a tile from the previous page.
The 4-inch board could not wake from sleep. It went dark and stayed dark,
and the only way back was the reset button -- which, on a console handed to a
child, is indistinguishable from a broken device. The firmware was awake the
whole time: touch was being read, the wake path ran, and the log reported a
normal panel delay. The panel simply was not switched back on. Waking sent
Sleep Out but never Display ON, which is sufficient on the ILI9341 the code was
written against and not on the ST7796. Display ON is now sent on every wake,
for every board -- it costs one byte on a panel that is already lit.
The launcher leads with different games. Page 1 is now Memory, Money,
Flags, Microku, Trace and Counting. It opened with Tic-Tac-Toe and closed with
Whack-a-Mole, neither of which says anything about what a child learns, and
Trace -- letters and handwriting -- was on page 4 where nobody would find it.
The launcher shows six tiles in landscape and four in portrait, so the first
four are the front page. The README gallery, the About game list and the
installer page all derive their order from the same place, so they move with
it.
If you have hidden games for a player, check that list. Per-profile
visibility is stored by launcher position rather than by game, so reordering
reassigns those choices. Every game ships visible, so this affects only a device
where someone has hidden something by hand.
The clock in Time was drawn over its own screen, twice. The dial covered the
question printed beneath it -- worst at the centre, where a circle reaches
lowest and a centred sentence has its middle -- and the score header cleared two
strips wide enough to erase 37px off each shoulder of the dial every time the
score changed, leaving the face as a strip with square bites out of it. Both
were present on every board since the screen was written; the 4-inch panel made
them unmissable. Radii on a panel that is not the canvas size now scale by the
smaller of the two axis scales rather than their mean, which was adding another
6% of overhang on the vertical.
Also fixed: env:audiodiag had been unusable -- its direct-DAC test
uninstalled the I2S driver and never put it back, so every interactive page
crashed on its first note after a completely healthy log. It gains a silent
RATE page that measures the DAC's true rate without needing ears.
identify_boards.py could not learn the first board of a new model, because it
could only copy an environment from another board already recorded; it now
derives it from platformio.ini.
Flashing
Easiest route is the web installer, which flashes over Web Serial from a Chromium browser and needs nothing downloaded from here.
The binaries below are for flashing by hand. FLASHING.txt has the commands, the offsets and a note on what an erase_flash costs you; SHA256SUMS.txt verifies a download. Each environment ships as a single -merged.bin to write at 0x0, and as the four separate parts.
Built from v5.7.0, commit 5cd837e. Every image reports that on About's This build page.