Get the build kit
- Download the complete build kit: this guide, source archive and inventory, pinned content, toolchain record, licenses and the recorded bench image.
- Source archive and source inventory.
- Pinned 181-move handoff and imported firmware content.
- Download this guide as Markdown, toolchain record, download manifest and SHA-256 checksums.
The kit contains source and generated display assets. It downloads the SDK, managed components and flashing tools from their pinned upstream sources during setup. It does not require access to the private firmware repository. Retained development documents inside the source archive describe the original project; follow this guide for the public build.
What you need
- The exact Waveshare ESP32-C6-Touch-AMOLED-2.16, with its case. Waveshare’s board documentation lists a 480 × 480 touch AMOLED, 16 MB flash, QMI8658 motion sensor and AXP2101 power management. The similar-looking S3 model needs a different port.
- For portable use, the compatible 3.7 V battery supplied for this board, with a two-pin MX1.25 plug. The original assembly used a separately supplied 1000 mAh battery. Waveshare sells versions with and without a battery; check your actual package.
- The shaped black polycarbonate electrical-insulation sheet between board and battery, a PH00 Phillips screwdriver, a clean microfiber cloth and a tray for four screws. The recovered assembly note specifies nonconductive, metal-free, flame-retardant PC film, 0.20–0.25 mm thick, rated UL 94 V-0 or VTM-0, if the supplied sheet is missing.
- A USB-C data cable, a Mac for the documented build/flashing path, and a phone camera for the QR check. USB power alone is enough to begin if you are not fitting a battery.
- Git, Python 3, CMake and Ninja, plus the ESP-IDF host prerequisites. Node/npm are needed only when regenerating content or fonts.
The recovered host used Node 24.16.0, npm 12.0.1, Python 3.14.7, CMake 4.4.3 and Ninja 1.13.2. These are recorded environment versions, not a demonstrated compatibility range. Windows/Linux source and flashing helpers are retained, but this guide’s complete host sequence is for macOS.
Install the battery and close the case
If your battery is already installed, check its insulation and wire routing before continuing. If you are using USB power only, skip to source setup.
- Disconnect USB-C. If running, hold the middle PWR button until off. Do not use a swollen or damaged battery, and leave its protective tape intact.
- Rest the device screen-down on the cloth. Remove the four rear screws and lift off the cover. Leave the screen and board mounted.
- Find the small two-pin socket marked BAT, beside USB-C. Match the battery’s red wire to the board’s + mark and black wire to −. Use the printed marks rather than an assumed left/right orientation. Waveshare’s schematic identifies the connector and polarity.
- Hold the plastic plug and seat it straight and gently. Do not force a mismatched plug or push on the wires.
- Place the shaped black insulating sheet over the exposed board. Route the wires through its cutout, clear of screws, buttons and the case edge. Lay the battery flat on the sheet, with its wire end toward BAT. The battery pouch must not touch the exposed board.
- Check that the cover sits flush without pressure. If the battery rattles, the recovered assembly uses a small piece of 0.5 mm closed-cell EVA electronics foam on the inside of the rear cover, just enough to restrain it. Foam does not replace the insulating sheet. Do not squeeze the pouch or force the cover shut.
- Refit the four screws and tighten diagonally until snug.
Inside, the order is rear cover → optional thin foam → battery → insulating sheet → circuit board → screen. A newly connected battery may be uncharged. Record when you first connect USB for flashing; charging starts then. Required charging time: STILL OPEN, pending measurement on a matching unit. No charging duration or battery runtime is established by this guide.
Unpack and check the source
Use a new empty workspace for this build. Download the complete kit above, then run these commands from the directory containing it:
tar -xzf bingo-device-2026-09-15.tar.gz
cd bingo-device-2026-09-15
shasum -a 256 -c SHA256SUMS
tar -xzf source.tar.gz
cd magic8-device-firmware
The source revision is 2d626b134add1ff7722c5e3378a5e03438e6ac81; its VERSION is 0.2.1. The archive preserves tracked source, generated C/fonts, lockfiles, tools and executable modes. Eight large macOS esptool executables were excluded; their original hashes are in ../source-inventory.json.
Fetch the pinned tools for both Mac architectures, so either Mac can run the installer:
bash flashing/vendor/fetch_esptool.sh macos arm64
bash flashing/vendor/fetch_esptool.sh macos amd64
Check the restored executable hashes against the source inventory:
python3 - <<'PY'
import hashlib, json
from pathlib import Path
inventory = json.loads(Path('../source-inventory.json').read_text())
for entry in inventory['files']:
if not entry['included']:
path = Path(entry['path'])
assert hashlib.sha256(path.read_bytes()).hexdigest() == entry['sha256'], path
print('OK', path)
PY
Establish a clean local Git commit for the release guard. Git may ask you to configure your own author name/email first:
git init
git add .
git commit -m "Recover device source from upstream 2d626b1"
git status --porcelain --untracked-files=all
The final command should print nothing. This is your local recovery commit, not the upstream commit ID. Keep both identities in your build record.
Install the pinned SDK
This firmware uses ESP-IDF v6.0, pinned to commit 662a3be354759d9487bf4b1a629fadb766cb1800, targeting esp32c6. Follow the pinned SDK’s host prerequisites above, then install it beside the unpacked firmware source:
git clone --recursive --branch v6.0 https://github.com/espressif/esp-idf.git ../esp-idf
git -C ../esp-idf checkout --detach 662a3be354759d9487bf4b1a629fadb766cb1800
git -C ../esp-idf submodule update --init --recursive
cd ../esp-idf
./install.sh esp32c6
. ./export.sh
cd ../magic8-device-firmware
In a later Terminal session, run . ../esp-idf/export.sh from the firmware root before building. Use the supplied firmware/dependencies.lock: it pins LVGL 9.5.0 and the display/touch components. The SDK pins RISC-V GCC 15.2.0, package esp-15.2.0_20251204. The separate toolchain record lists dependency versions; it does not reconstruct every historical Python package.
Build a merged image
The approved 181 suggestions, QR matrices and fonts are already generated. An unchanged build needs no Node install or regeneration:
python3 tools/import_moves.py --check
bash tools/check-generated-fresh.sh
idf.py -C firmware set-target esp32c6
python3 flashing/build_release.py
Successful output is dist/magic8-0.2.1-esp32c6-merged.bin. The release builder empties dist/, compiles, checks the approved collection, requires a clean Git tree, keeps serial-console shake off and battery light sleep on, then merges the actual build’s bootloader, partition table and application into one file.
Keep the supplied SH8601 driver and Arduino-example pin map: display CS GPIO15, touch interrupt GPIO5. Waveshare lists CO5300/CST9220 chips, but the recovered working firmware uses SH8601 and a CST9217-named touch component. Those component names do not call for a driver swap. Do not select the historical alternative pin map.
If a build updates a tracked lockfile or generated input, inspect and resolve that change before committing and rebuilding. --dev is not a fix for a refused release. A plain idf.py build compiles but does not update dist/; use the release builder to produce the file you flash.
Flash your matching device
Close other serial monitors. Connect just the intended C6 unit using the data cable. From the firmware root:
./flashing/flash_one_unit.command
The installer chooses this Mac’s pinned esptool and the newest named image in the adjacent dist/. Confirm the printed image name is the one you just built. It writes the merged file at 0x0, using --chip esp32c6 --baud 460800 write-flash, and resets afterward. Keep only the intended image in dist/ and only one unit connected. There is no dry-run mode.
Look for both esptool’s verified data hash and “PASS -- unit flashed and verified”. Press Enter to close the installer prompt. Then verify startup and the experience below: write verification alone does not prove that the screen, battery or QR works.
If the device is not detected, check the data cable and USB connection. For manual download mode, hold BOOT while reconnecting USB power, then release it and retry. A battery-powered unit may first need powering off with PWR. Waveshare’s button documentation explains BOOT/PWR; Espressif’s C6 boot-mode reference explains manual entry. Keep esptool’s default reset; --after no-reset can leave the unit in download mode.
For a macOS downloaded-script warning, use Finder’s right-click → Open on the intended installer. A blank screen after a successful write calls for a boot log and a check of the supplied driver/pins, rather than switching drivers at random. A missing dist/ image calls for a successful merged-image build.
Use it and verify your build
The case has PWR, BOOT and a third KEY button. PWR controls board power; KEY wakes the suggestion interface. A sleeping suggestion interface ignores shaking.
- While USB-powered, confirm the device starts. To record version/peripheral startup, run
idf.py -C firmware monitor; exit with Ctrl-] before flashing again. Record the version, source/local commit, image SHA-256 and write result. - Press KEY to wake. Shake deliberately: a swirl should lead to a suggestion. Another separate shake should give a new suggestion. Continuous shaking should not queue repeated answers.
- Tap the suggestion to open its short explanation; tap again for its QR. Scan with your phone. Confirm the signed-out page’s move number and title match the device, and that full instructions open. Try another suggestion and QR as well.
- Let the interface time out. On battery, press KEY to wake and repeat the interaction. USB-powered idle behavior is different: USB keeps the main loop awake for development.
- After charging, unplug USB while the unit is running and repeat wake/shake/tap/scan. Then power off with PWR and do a fresh battery-only cold boot. Both checks are needed to establish battery operation. If it fails, check charge and BAT seating before concluding the battery is defective.
Record USB and battery results separately, including the actual charging interval. The expected deck has 181 moves, permanent numbers 001–181; its source order is not strictly numeric. Physical matching-unit reproduction and phone scans are the next demonstration, not results claimed for this guide.
Update the activity content
The download is a pinned snapshot, not an automatic feed. facts.json pins content/handoff/magic8-handoff.json by SHA-256. Its rows contain move_number, source_id, title, detail and url; the importer produces firmware fields id, source_id, move, detail, url in the same order.
For a future approved replacement collection, preserve its exact handoff bytes and approval record, update the handoff path/hash in facts.json, then run from your source root:
npm ci
python3 tools/import_moves.py --write
node tools/generate-qr.mjs
python3 tools/gen-docs.py --write
bash tools/check-generated-fresh.sh
git diff
Review and commit the handoff, pin, imported content, generated C/manifests and generated documentation, then run the release builder and flash the new image. Record the new content hash and image version. Never edit generated C by hand. npm ci uses the lockfile: QR encoder 1.5.2 and font converter 1.5.3.
Titles must fit 60 printable ASCII characters, authored device invitations 71, and URLs 62 bytes for the established QR contract. All supplied content fits. The generator’s larger emergency description ceiling is not permission to exceed the authored 71-character limit. Length alone does not prove display fit; use the renderer checks below for changed text.
Firmware IDs are three-digit strings such as 012, linking exactly to https://bingo.feedforward-collective.com/moves/012. Preserve existing numbers, wording and destinations. The site’s live generator export uses move-012 IDs and a different schema; it is not a drop-in replacement for this pinned handoff. Its identity map and live export manifest are separate from the build-kit manifest.
Customize the look or behavior
Work in your own source copy and name each changed build in VERSION. Keep the device’s title, explanation and QR destination aligned.
- Colors and type:
ui/theme.h; screen layouts:ui/screen_*.c. Keep comfortable text sizes and a QR quiet zone. Changing a color needs no content regeneration. - Timeouts and animation:
ui/timing.h. The default QR screen lasts 90 seconds; changing it should still leave time to reach for a phone. - Shake sensitivity:
ui/motion_params.h. Change one value at a time and test deliberate shaking and ordinary handling on the physical unit. - Fonts:
assets/fonts/andtools/generate-fonts.sh. Install locked npm dependencies, regenerate withbash tools/generate-fonts.sh, and commit source fonts plus generated files/manifests. Retain the supplied font licenses. The existing font subset is printable ASCII; other languages need a font/generator/layout change. - Your own activity deck or website: make a separate firmware fork. The recovered importer and release guard intentionally accept only the pinned Feedforward collection and permanent URL prefix. A custom deck needs its own source/provenance contract, importer and guard, coordinated QR destinations, and renderer checks. Merely replacing JSON or using
--devdoes not produce a guarded custom release. This guide does not revise Feedforward’s shared content or tier policy.
For changed content, fonts, layout or QR rendering, use the retained simulator’s actual screens before flashing. On a Mac with SDL2, pkg-config, CMake, Ninja and ZBar installed:
cd sim
bash fetch_lvgl.sh
cmake -S . -B build -G Ninja
cmake --build build --target magic8_screenshot
./build/magic8_screenshot
cd ..
python3 tools/check-rendered-qr.py
The simulator renders all moves and checks layout bounds; the QR checker compares every rendered code with its content URL. Read sim/README.md for host prerequisites and the affected state-machine/input/shake tests when those behaviors change. Simulator success does not establish panel appearance or a physical phone scan.
Use another simple device
The idea needs only a way to choose an activity, show a title and invitation, and open its instructions. A button-driven screen, touchscreen kiosk or small web app can replace shaking. A motion sensor is needed only if you want the shake interaction.
Use the downloadable content and identity mapping as inputs. The ui/ screens are separated from the C6 hardware layer, which helps a firmware port. Another microcontroller needs its own display, input, power and memory integration; a smaller screen also needs layout and QR checks. The C6 binary is only for this exact C6 board. No other hardware port is demonstrated here.
Versions, licenses and the recorded bench image
The current source archive is preserved byte-for-byte from the recovered revision above. Its inventory identifies included files and excluded executables. The kit adds this guide, toolchain/content copies and upstream font license notices; it adds no general open-source license to the Feedforward project. Third-party terms remain with their materials: JetBrains Mono and Roboto Serif use the SIL Open Font License; Espressif esptool license files are retained beside its fetch helpers. SDK/components carry their own upstream licenses.
For developers comparing against the original unit, the kit’s bench/ directory contains magic8-0.2.0-content-DEV-esp32c6-merged.bin, built from b17b9736d3379cc35b74db3b0bfff87c2273f72f. It is 831,520 bytes; SHA-256 9ec3358a64192aec415f6c5ad9e54ac26533e341b9135385319361d4c8aa09a8. It has console shake enabled and release checks were waived for the historical bench build. It is retained for comparison, not the default installation image or a guarded distribution release. A newly compiled image need not match those bytes because build timestamps and paths differ.
Ask your LLM to help
Copy this prompt into an assistant that can read web pages and help with local files:
Help me build the AI suggestion device described at
https://bingo.feedforward-collective.com/device
Read its complete Markdown guide at
https://bingo.feedforward-collective.com/device/guide.md
and download manifest at
https://bingo.feedforward-collective.com/device/build/manifest.json.
Use the linked build kit and pinned source, not a private repository or a
guessed Arduino/S3 setup. First establish my actual board, computer and
battery state. Help me through assembly, source checks, SDK setup and
building in a new workspace. Before flashing, identify my intended unit
and merged image. Keep the recorded DEV image separate from the 0.2.1
source preparation. Record build/write, USB interaction, QR phone scans
and battery-only cold boot separately. Charging time is still unmeasured.
For customization, inspect the actual source and preserve generated-asset
freshness and content provenance. Explain any custom-deck port separately.
Do not claim a physical step passed unless I report it or you observe it.