Skip to content

Companion callout#20

Open
skitzo2000 wants to merge 26 commits into
anthropics:mainfrom
skitzo2000:companion-callout
Open

Companion callout#20
skitzo2000 wants to merge 26 commits into
anthropics:mainfrom
skitzo2000:companion-callout

Conversation

@skitzo2000
Copy link
Copy Markdown

No description provided.

skitzo2000 and others added 26 commits May 17, 2026 11:28
Rewrite platformio.ini per PORT.md §4 for ESP32-2432S028R (CYD): TFT_eSPI
pin map in build_flags (ILI9341 driver, MISO=12 MOSI=13 SCLK=14 CS=15 DC=2
BL=21), 240 MHz CPU, XPT2046_Touchscreen + AnimatedGIF + ArduinoJson deps.
Restrict build_src_filter to +<main.cpp> until HAL exists in M2.

Two deviations from PORT.md §4 (noted inline):
- platform pinned to espressif32 @ ~6.10.0 (Arduino-ESP32 2.x). The
  unpinned 7.x core needs the framework-arduinoespressif32-libs sidecar
  package and PIO did not install it; esp32dev variant folder also gone
  in that layout. 6.10 is the last single-package release.
- paulstoffregen/XPT2046_Touchscreen version constraint dropped; PIO
  registry only carries 0.0.0-alpha+sha for that author/lib.

Preserve upstream main.cpp as main.cpp.upstream for line-by-line reference
during M2 HAL swap. New main.cpp is empty setup/loop + boot banner.

Verified on user's CYD: clean build (RAM 6.5%, Flash 12.5%), flashable
over /dev/ttyUSB0, serial prints boot banner. Display sanity sketch
(ad-hoc M1.5, reverted) confirmed ILI9341 driver + pin map render
correctly — no need for the ILI9341_2 fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add src/hal/board.{h,cpp} exporting `tft`, `ts`, boardBegin(), boardLoop()
per PORT.md §3 pin map + §4 dual-SPI pattern. Display lives on the default
SPI instance (VSPI) — pin map comes from build_flags. Touch uses a
separate HSPI bus (SCK=25, MISO=39, MOSI=32) bound via SPIClass; TFT_eSPI's
built-in TOUCH_CS sharing does NOT apply on the CYD.

Widen build_src_filter to +<main.cpp> +<hal/> (widens again in M2.3 once
the main.cpp swap and buddies/ port land).

XPT2046_Touchscreen lib source: switched from PIO registry to GitHub HEAD
(PaulStoffregen). The registry alpha predates the begin(SPIClass&)
overload required for binding touch to HSPI rather than the default SPI.

Verified on hardware: boardBegin() returns without crash; serial confirms
`cyd-claudepet: boardBegin() returned`. Bottleneck cleared for M2.2's
6-way parallel HAL fan-out (input/imu/rtc/audio/led/power).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six HAL modules in src/hal/ providing thin wrappers per PORT.md §5A so
main.cpp can be swapped from M5.* to hal/* in M2.3 without caring about
board specifics.

- input.{h,cpp}: stub InputButton (wasPressed/wasReleased/isPressed/
  pressedFor) returning false; inputA/inputB externs. Real touch-zone
  hit-test arrives in M3.
- imu.{h,cpp}: stub imuRead → (0, 0, 1.0). Face-up forever, never
  shaken. Real MPU6050 path TBD behind -DHAS_IMU.
- rtc.{h,cpp}: real software RTC. time_t + millis() drift, NVS-backed
  in namespace "rtc"/key "epoch". Saves on minute rollover only
  (~1440 writes/day max). Drop-in RTC_TimeTypeDef / RTC_DateTypeDef
  matching M5 field layout. Default epoch 2025-01-01.
- audio.{h,cpp}: real LEDC tone scheduler on GPIO 26 (DAC2), channel 2.
  Non-blocking; auto-stop after dur ms via audioLoop().
- led.{h,cpp}: real RGB LED on GPIO 4/16/17, active-LOW. Direct
  digitalWrite — no PWM (the CYD onboard LED isn't PWM-wired).
- power.{h,cpp}: real backlight on GPIO 21 via LEDC channel 0 (5 kHz,
  8-bit); powerScreen toggles backlight + screenOff flag; powerSleep
  deep-sleeps; powerStatus returns fixed {100, 5000, 0, true}.

LEDC channel allocation: 0=power(backlight), 2=audio(tone). led/ does
not use LEDC. Avoids collision.

Pulls in Preferences (built-in Arduino-ESP32 lib) as a transitive dep
for rtc's NVS layer.

Build: RAM 6.6%, Flash 13.8%, clean link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port the upstream M5StickC firmware to drive the CYD entirely through the
hal/ surface defined in M2.1/M2.2. Verified on hardware: boot shows the
"Hello! / a buddy appears" splash, transitions to a sleeping capybara
(ASCII species index 0), serial logs `buddy: ASCII mode`, and BLE
advertises as `Claude-B522`.

main.cpp (the big swap, 15 enumerated edits per PORT.md §5B):
- #include <M5StickCPlus.h> → hal/{board,power,led,audio,input,imu,rtc}.h
- TFT_eSprite spr(&M5.Lcd) → spr(&tft)
- W=135 H=240 → W=240 H=320, CY_BASE 120→160
- M5.begin/Imu.Init/Beep.begin/setRotation → boardBegin() + per-module
  Begin() calls; explicit Serial.begin(115200) (M5 used to do this)
- M5.update() + M5.Beep.update() → boardLoop() (which ticks
  inputUpdate/audioLoop/rtcLoop internally)
- M5.Lcd.* → tft.* (16 sites)
- M5.BtnA/B → inputA/B (7 sites) — stubbed, real touch in M3
- M5.Imu.getAccelData → imuRead — face-up stub
- M5.Axp.ScreenBreath → powerBacklight
- M5.Axp.SetLDO2 → powerScreen
- M5.Axp.PowerOff → powerSleep, menu item "turn off" → "sleep"
- M5.Axp.GetBtnPress block deleted (no left power button on CYD); a
  touch-corner gesture is wired in M3 per PORT.md §6
- M5.Beep.tone → boardTone
- M5.Rtc.{Get,Set}{Time,Date} → rtc{Get,Set}{Time,Date}
- drawInfo page 3 rewritten: drop battery/temp telemetry (no fuel-gauge
  or temp sensor on CYD), show USB-powered + uptime/heap/bright/bt
- _onUsb = powerStatus().usb
- bare GREEN/RED color constants → TFT_GREEN/TFT_RED (M5 lib aliased
  these; TFT_eSPI requires the TFT_ prefix)
- Removed duplicate `bool screenOff = false;` (extern from hal/power.h)
- Sprite forced to 8bpp colour depth (240*320*1B = 76800 vs 153600 at
  16bpp) — internal heap can't fit a full 16bpp sprite after BLE +
  LittleFS load; CYD standard variant has no PSRAM. Auto-palette
  approximates RGB565 values.
- Clear sprite after splash delay so loop's incremental pet render
  doesn't leave residue on the wider canvas.

data.h (M2.4): M5.Rtc.SetTime/SetDate → rtcSet*; include hal/rtc.h.
xfer.h (M2.5): status response now reads via powerStatus() instead of
  M5.Axp.GetBatVoltage/Current/VBusVoltage; include hal/power.h.

buddy.cpp: BUDDY_X_CENTER 67→120, BUDDY_CANVAS_W 135→240 to centre
species art on the wider CYD canvas (PORT.md §5B).

buddy.cpp + character.cpp + 18 buddies/*.cpp: include swap only,
M5StickCPlus.h → hal/board.h.

hal/board.cpp: boardLoop() now ticks inputUpdate + audioLoop + rtcLoop
so main.cpp's single call covers all per-tick HAL housekeeping.

platformio.ini: build_src_filter widened to +<*> +<buddies/> +<hal/>.

Flash: 63% (1.32 MB), RAM: ~74% at runtime after sprite alloc. M3 +
M4 layout retune still needed before this is usable end-to-end —
buttons (touch zones) and panel geometry land next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the M2.2 input stub with a working touch-zone implementation
and a first-boot calibration UI per PORT.md §6.

hal/input.{h,cpp}:
- InputButton now owns a rectangular hit-test (x,y,w,h) and a small
  edge-detection state machine (was/is pressed, was released, pressed
  for N ms). wasPressed()/wasReleased() are consumed-on-read so a single
  tap fires exactly once.
- Three zones defined:
  - inputA (ZONE_APPROVE): bottom-right half, lower 80 px
  - inputB (ZONE_DENY):    bottom-left half,  lower 80 px
  - inputSleep:            top-right 50×50 corner
- inputUpdate() polls XPT2046 via the existing `ts` global on HSPI,
  applies the saved calibration (raw 0..4095 → display 0..240/0..320),
  debounces the down-state at 30 ms, then dispatches inside/outside to
  each zone.
- inputReadDisplay() exposed publicly so future code (per-pixel touch
  features, gesture detectors) can read calibrated coords too.

hal/touch_calibration.{h,cpp} (new):
- touchCalibrateIfNeeded() runs the 4-corner crosshair UI iff NVS
  namespace "tcal" has no "xMin" key. Idempotent on later boots.
- touchCalibrateForce() exposed for a future settings-menu entry.
- Per-corner: draws crosshair at MARGIN=20 from each edge, waits for
  finger up → finger down → 60 ms settle → re-sample → finger up.
  Re-sampling stabilises the reading after the initial press transient.
- Auto-detects axis orientation: if raw "x" varies more between top-left
  and bottom-left than between top-left and top-right, the panel is
  rotated relative to the display → swap axes. Flips inferred from the
  TL→BR direction. Persists swap/flipX/flipY + the four extrema to NVS.
- Extrapolates from the 20 px margins to the 0/edge bounds so input.cpp's
  linear remap doesn't need to know the margin.

hal/board.cpp: boardBegin() now calls touchCalibrateIfNeeded() after
ts.setRotation() — first boot stops to run the UI; subsequent boots
proceed straight to the pet flow.

main.cpp: re-wire the screen-sleep gesture deleted in M2.3. Tap on
inputSleep (top-right corner) toggles screenOff via the existing
powerScreen()/wake() path. The original wake-on-A/B-press logic is
unchanged.

Verified on hardware: 4-corner calibration UI runs on first boot;
"calibrated" ack appears; firmware proceeds to splash + sleeping pet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Widen panel layouts and bump font sizes per PORT.md §5C. Minimum-viable
retune: dimensions adjusted, text bumped only where the visual win is
clear (clock, passkey, approval tool name, menu/reset items). drawHUD's
SHOW/LH/WIDTH retargeted so 4× wrapped transcript lines fit at the
40-char width the wider screen affords.

main.cpp:
- drawMenu: mw 118→200, mh = 24+N*24+hint, item text size 1→2, line
  spacing 14→24. drawMenuHints reset to size 1 after.
- drawSettings: mw 118→220, line spacing 14→18 (size 1 kept — 10
  settings need to fit). Value column shifted from mw-36 to mw-56 for
  the wider panel.
- drawReset: mw 118→200, size 1→2, line spacing 14→24.
- drawClock portrait: HH:MM size 4→6 at y=180; seconds size 2→3 at
  y=230; date size 1→2 at y=268. Clear-region starts at y=110 so the
  larger buddy zone isn't disturbed.
- drawPasskey: digits size 3→5, recentered at (W-180)/2, label at y=80,
  hint at y=260. Bigger, easier to read across the room.
- drawApproval: AREA 78→120. Tool name size up to 3 (was 2) when it
  fits ≤13 chars (was ≤10). Hint truncations bumped to 40 chars (was
  21). Button labels (A: approve / B: deny) reflow to the wider bar.
- drawHUD: SHOW=3→4, LH=8→16, WIDTH=21→40; disp[32][24]→[32][48]
  buffer (matches the wrapInto signature change below). 64 px tall HUD
  band shows 4 transcript lines at 40-char width vs 3 lines at 21.
- drawPet/drawPetStats: TOP 70→100 (was 70 to clear below the smaller
  M5 buddy; now 100 to clear below the centred-on-240 buddy art).
- drawInfo: TOP 70→100. BUTTONS page renamed to TOUCH ZONES with text
  reflecting CYD's gesture model: bottom-right = A, bottom-left = B,
  top-right corner = sleep. Removed the M5 "Power left side" entry.
- drawInfo CREDITS page: hardware line "M5StickC Plus / ESP32 + AXP192"
  → "CYD ESP32-2432S028R / ESP32-D0WD-V3".
- wrapInto signature: char out[][24] → char out[][48].

The DEEP retunes from PORT.md §5C (per-pet-page Y positions, settings
landscape clock font sizes, drawInfo content reflowed to use the full
240px width) are deferred — current rendering uses ~half the horizontal
real estate but is readable and non-overlapping. Iterate after M5 if
the layout still feels off.

Build: Flash 63.9% (same), RAM 22.9%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PORT.md §6 defines ZONE_DENY = bottom-LEFT (B) and ZONE_APPROVE =
bottom-RIGHT (A) on the CYD, but drawApproval was inherited verbatim
from the upstream M5StickC where BtnA was the front button (visually
on the left) and BtnB the side button — so the labels "A: approve"
sat LEFT and "B: deny" sat RIGHT.

End-to-end test on Linux via the claude-hardware-buddy plugin: tapping
where "A: approve" was rendered actually fired the bottom-LEFT zone =
inputB = deny. Caller saw decision="deny" despite intending to approve.

Swap: "B: deny" now renders at x=4 (left), "A: approve" at x=W-70
(right). Matches the actual touch zones in hal/input.cpp.
Two related fixes to the approval flow that surfaced once the
claude-hardware-buddy MCP plugin started driving the device on Linux.

drawApproval button blocks
  Bottom 40px of the approval bar now renders as two filled rounded
  rects — red `DENY` on the left, green `APPROVE` on the right —
  with size-2 white text centred on each. Replaces the tiny size-1
  labels that were invisible at any distance on the 240×320 panel,
  especially with the 8bpp sprite's colour quantisation. Layout
  matches the touch zones in hal/input.cpp (bottom-LEFT = inputB,
  bottom-RIGHT = inputA).

_applyJson `clear_prompt` sentinel
  The previous behaviour cleared `tama.promptId` whenever an incoming
  JSON message did NOT contain a `prompt` field. That meant every
  keepalive (~12s) and every status push from the host wiped any
  active approval prompt off the screen before the user could tap.

  New behaviour: prompts get set only when an explicit `prompt`
  object arrives; they get cleared only on the explicit
  `clear_prompt:true` sentinel. Messages without either field leave
  the active prompt alone. Daemon-side companion change lives in
  claude-hardware-buddy.git.
PORT.md and MILESTONES.md were the spec + execution plan that produced
the cyd branch. They lived outside the firmware repo during development
in a sibling working directory (~/Projects/CYD-ClaudePet/) which has
now been collapsed; folding them in here so the docs live with the
code they describe.

`docs/upstream-snapshot/` (a 4 MB clone of anthropics/claude-desktop-buddy
used as a reference during the port) is gitignored — the firmware repo's
`main` branch already tracks upstream identically, so anyone wanting the
reference can `git checkout main` here.

docs/README.md adds an entry point pointing readers at the port spec,
the milestone plan, and the companion claude-hardware-buddy plugin.
Add include/M5StickCPlus.h + src/m5_shim.cpp providing the small M5
API surface (Rtc, Axp) used by upstream firmware code, routed through
the HAL. This lets 20 upstream files (18 buddies + character.cpp +
xfer.h) revert to byte-identical with upstream/main.

Move docs/{PORT,MILESTONES,README}.md to the hardware-buddy plugin
repo where the rest of the bridge docs live.

Delete src/main.cpp.upstream — git history retains the reference
snapshot.

Scaffold src/cyd/ + add +<cyd/> to build_src_filter so a future
move of the CYD main.cpp under that dir (leaving src/main.cpp
upstream-identical) is a one-step git mv.

Overlay shrinks: 45 → 21 files changed vs upstream, +854/-139.
Modified upstream files now: platformio.ini (irreducible board
config), src/main.cpp (planned future revert), src/buddy.cpp
(2-line 240-wide canvas geometry), src/data.h (5-line clear_prompt
sentinel), .gitignore. Build verified on env:cyd.
main now mirrors Anthropic byte-for-byte and visitors landing on
skitzo2000/claude-desktop-buddy hit the cyd branch as the default.
The upstream README assumed an M5StickC Plus, button controls, and
the closed-source Anthropic Hardware Buddy GUI as the only client.

Rewrite cover: CYD hardware specifics (2432S028R, USB-only, no IMU/
battery/RTC), two integration paths (Claude Desktop GUI vs the
claude-hardware-buddy plugin for Claude Code), touch zone diagram
replacing button controls, branching strategy (main = upstream, cyd =
port), and pointers into the deep design docs (PORT.md, MILESTONES.md)
that now live in the plugin repo.

GIF format and the seven pet states are unchanged from upstream, so
those sections link out to main's README rather than duplicating.
Earlier draft listed "no IMU, no battery, no RTC" as gaps. That misread
the HAL design — those features have safe stubs and the firmware boots
fine on a bare CYD. The HAL leaves clean slots for optional add-ons but
none of them are needed to run the buddy.

Lead the section with the bare-CYD path. Move the add-on table behind a
"skip on a first build" note so newcomers don't think they need to
source an MPU6050 to use this.
Slim CYD overlay vs upstream: M5 shim + plugin-docs move
The bufo GIFs were redistributed third-party artwork carved out of the
MIT license. Removing them lets the whole repo be cleanly MIT, which
matters for binary redistribution (e.g. a web flasher hosted from this
repo). The firmware does not bundle character packs in the build —
they're pushed over BLE at runtime — so removing the example pack has
no functional impact. Users can flash their own packs via
tools/flash_character.py.

Also fixes the README's license line, which incorrectly said Apache 2.0.
Users can flash a CYD by opening a webpage in Chrome/Edge and clicking
Install — no PlatformIO, no Python, no command line. Uses ESP Web Tools
over Web Serial.

Pieces:
- .github/workflows/release.yml builds firmware with PlatformIO on every
  v* tag, merges bootloader + partitions + boot_app0 + firmware into a
  single merged.bin via esptool, and attaches it (plus the individual
  .bin files for power users) to a GitHub Release.
- docs/index.html is the install page — esp-web-tools install button
  plus a graceful fallback message for browsers without Web Serial.
- docs/manifest.json points the flasher at
  releases/latest/download/merged.bin.

Activation: tag a v* release to trigger the first build, and enable
GitHub Pages on the repo serving from /docs on the cyd branch.
Drop bufo character pack and clarify license as MIT
Without a pull_request or branch trigger the workflow only fired on v*
tag push or manual dispatch — neither of which work before the file
lands on the default branch. That meant the workflow couldn't be
smoke-tested from a PR.

Now:
- runs on every PR (build only, doesn't publish)
- runs on every push to cyd (build only)
- still publishes a release on v* tag push
- merged.bin and the individual .bins are uploaded as downloadable
  workflow artifacts on every run (14-day retention), so you can grab
  and test a build without cutting a release
setup-python@v5 with cache: pip needs a requirements.txt or
pyproject.toml to compute the cache key. We don't ship one — pip
install platformio happens inline. The cache lookup was failing
before the build step could run.
PlatformIO bundles esptool inside its toolchain (used during upload),
but it's not on the system Python path, so 'python -m esptool merge_bin'
in the merge step couldn't find it. Pip-install it alongside platformio.
Forward the web flasher onto cyd (PR #3 landed on the wrong base)
…ection

End users now flash from the browser — that should be the first thing
they see. The pio command is still here for contributors but lives
below the pairing and details sections, framed as the source-build
path rather than the default.
README: lead with web flasher, demote pio
The companion Claude Code plugin moved from an internal gitea instance
to GitHub and was renamed from claude-hardware-buddy to
ai-hardware-buddy. Updates:

- README link in 'Pair it with Claude' (plugin install pointer)
- README link to PORT.md (deep design doc lives in the plugin repo)
- docs/index.html link on the web flasher page

No other content changes — this is a pure link migration so the
public buddy repo stops pointing at the private gitea host.
Point plugin links at the new GitHub repo (ai-hardware-buddy)
…o release

The CYD firmware (this repo) and the ai-hardware-buddy plugin
(separate repo) just both went public. Each works standalone, but
together they cover every combination of (Claude Desktop | Claude
Code) x (M5StickC Plus | CYD). Pulling that story into a top-level
section with a support matrix so the relationship is obvious — no
buried plugin link in the middle of the pairing section.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant