# Build your own AI suggestion device

Shake it for a new idea. Tap to learn more. Scan its QR code to open the activity’s full instructions on your phone.

This guide builds the Feedforward suggestion device on the **Waveshare ESP32-C6-Touch-AMOLED-2.16**. Its suggestions and QR codes are stored on the device; you do not need Wi-Fi or an AI account to use it. Your phone needs internet access to open the instructions.

The original unit has recorded successful flashing, startup, button wake and shake-to-suggestion behavior. The source below is the later **0.2.1 preparation**. A fresh matching-board build and full phone-scan walkthrough remain to be demonstrated. The recorded working image is a separate **0.2.0-content DEV** bench image.

## Get the build kit

- [Download the complete build kit](https://bingo.feedforward-collective.com/device/build/bingo-device-2026-09-15.tar.gz): this guide, source archive and inventory, pinned content, toolchain record, licenses and the recorded bench image.
- [Source archive](https://bingo.feedforward-collective.com/device/build/source.tar.gz) and [source inventory](https://bingo.feedforward-collective.com/device/build/source-inventory.json).
- [Pinned 181-move handoff](https://bingo.feedforward-collective.com/device/build/magic8-handoff.json) and [imported firmware content](https://bingo.feedforward-collective.com/device/build/firmware-moves.json).
- [Download this guide as Markdown](https://bingo.feedforward-collective.com/device/guide.md), [toolchain record](https://bingo.feedforward-collective.com/device/build/toolchain.json), [download manifest](https://bingo.feedforward-collective.com/device/build/manifest.json) and [SHA-256 checksums](https://bingo.feedforward-collective.com/device/build/SHA256SUMS).

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](https://docs.waveshare.com/ESP32-C6-Touch-AMOLED-2.16) 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](https://github.com/espressif/esp-idf/tree/662a3be354759d9487bf4b1a629fadb766cb1800/docs/en/get-started). 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.

1. 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.
2. Rest the device screen-down on the cloth. Remove the four rear screws and lift off the cover. Leave the screen and board mounted.
3. 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](https://files.waveshare.com/wiki/ESP32-C6-Touch-AMOLED-2.16/ESP32-C6-Touch-AMOLED-2.16-Schematic.pdf) identifies the connector and polarity.
4. Hold the plastic plug and seat it straight and gently. Do not force a mismatched plug or push on the wires.
5. 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.
6. 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.
7. 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:

```sh
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:

```sh
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:

```sh
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:

```sh
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:

```sh
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:

```sh
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:

```sh
./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](https://docs.waveshare.com/ESP32-C6-Touch-AMOLED-2.16) explains BOOT/PWR; [Espressif’s C6 boot-mode reference](https://docs.espressif.com/projects/esptool/en/latest/esp32c6/advanced-topics/boot-mode-selection.html) 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.

1. 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.
2. 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.
3. 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.
4. 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.
5. 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:

```sh
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](https://bingo.feedforward-collective.com/device/moves.json) uses **`move-012`** IDs and a different schema; it is not a drop-in replacement for this pinned handoff. Its [identity map](https://bingo.feedforward-collective.com/device/identity-map.json) and [live export manifest](https://bingo.feedforward-collective.com/device/manifest.json) 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/` and `tools/generate-fonts.sh`. Install locked npm dependencies, regenerate with `bash 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 `--dev` does 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:

```sh
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](https://openfontlicense.org/); 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:

```text
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.
```
