Add Xingzhi Cube 1.83" TFT WiFi (2mic) board support#31
Conversation
…ixels Most ST7789/CO5300/SH8601 ports consume RGB565 pixels in host-endian byte order, but some MIPI-SPI controllers (notably NV3023) expect bytes MSB-first. Rather than per-board byte-swapping inside draw_bitmap, expose the format through a new HAL function and wire main.cpp to forward it into lv_display_set_color_format(). Existing ports return LV_COLOR_FORMAT_RGB565 (no behavior change); ports that need MSB-first can return LV_COLOR_FORMAT_RGB565_SWAPPED and stream buffers verbatim. Template gains a small comment pointing new ports at the choice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ESP32-S3-N16R8 + 1.83" 284x240 landscape panel + 3 buttons. Despite its silkscreen and the chip name floating around in forum posts, the panel is actually NV3023, not ST7789 — both Arduino_GFX and TFT_eSPI init sequences silently fail. Driver is hand-rolled (~190 lines) and transcribes the init sequence from the xiaozhi-esphome project: https://github.com/RealDeco/xiaozhi-esphome/blob/main/devices/Xingzhi/xingzhi-cube-1.83-2mic.yaml Port summary: - Hand-rolled NV3023 driver over Arduino SPI (no GFX library dep) - 284x240 landscape with CASET column offset 36 - MADCTL 0xA0 (orientation only, no BGR bit) + no INVON; pairs with LV_COLOR_FORMAT_RGB565_SWAPPED so the panel sees MSB-first bytes - Backlight on GPIO13 via LEDC PWM (DC level alone does not light it) - GPIO21 power-latch held high in board_init() so the board stays on off USB power - Three buttons: BOOT (GPIO0) primary, VOL_DOWN (GPIO40) secondary, VOL_UP (GPIO39) routed through power_hal as the screen-cycle button (no PMU on this board) - Battery percent from ADC2 GPIO17 voltage divider, charging-status from GPIO38; no IMU, no touch Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The NV3023 on this board never gives clean colors with the standard INVOFF/MADCTL knobs — anti-aliased mid-tones drift through several hues across configurations, and pure black/white pixels come out with inverted polarity in some configs. After exhausting the obvious init-sequence tweaks, ship a high-contrast monochrome path: - display_hal_draw_bitmap thresholds each pixel's max channel against MONO_THRESHOLD (150 / 255) — bright → white, dark → black - Output is pre-inverted before going to the panel to land white on bright pixels and black on the background, matching what LVGL meant - INVOFF (0x20) added to the init sequence to keep panel polarity in a known state Result: Clawd reads as a crisp white silhouette on solid black, text stays sharp, and the splash + Usage + Bluetooth screens are fully readable. Define MONOCHROME_RENDER can be commented out in display.cpp to fall back to native (color-shifted) output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed a follow-up commit ( The NV3023 on this specific hardware never settles into clean colors with the standard
|
… boards Previously, the splash-screen PWR-press handler unconditionally called splash_next() to cycle animations. That assumes a tap on the touchscreen is the way to leave splash — fine for the AMOLED boards, but on the Xingzhi Cube 1.83 (no touch panel) the user has no other way to reach the Usage screen and is stuck on splash forever. Add a BoardCaps.has_touch field (true for both Waveshare ports, false for Xingzhi). When false, the splash-screen PWR-press now calls ui_toggle_splash() instead of splash_next(), routing the user straight to Usage (and back, since toggle remembers prev_non_splash). The cycle behavior on Usage / Bluetooth is unchanged for everyone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Discovered + fixed one more issue during real-hardware verification ( On the splash screen, PWR-press was hard-coded to call Added Verified on hardware: BOOT (GPIO 0), VOL_UP (GPIO 39), VOL_DOWN (GPIO 40) all toggle correctly, and PWR now successfully transitions Splash → Usage → Bluetooth on the Xingzhi. |
The Usage screen previously hardcoded font_tiempos_56 for the title
and font_styrene_48 / _28 for the percent and reset labels — sizes
designed for the 368-px and 480-px panels. On the new 284x240
Xingzhi board the title overlapped with the 80x80 logo, the reset
text overlapped the progress bar, and the percent number didn't fit
inside the panel.
Layout struct now carries the Usage-screen fonts (title / pct / pill
/ reset / anim), a bar height, pill padding, and a show_chrome flag.
compute_layout() picks them per breakpoint:
- height >= 460 → large (unchanged, same fonts as before)
- height >= 280 → compact (unchanged)
- otherwise → tiny: styrene_24 title, styrene_28 pct, styrene_12
pill, styrene_20 reset, mono_18 anim, 12-px bar,
show_chrome=false (logo + battery icons hidden so
the title + content fit in 240 vertical pixels)
ui_show_screen() and apply_battery_visibility() now respect
show_chrome — when off, both icons stay hidden on every screen.
Verified with screenshot.sh on real hardware: title, two usage
panels, and the animation status footer all fit within 284x240.
Existing Waveshare layouts are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Followed up with Previously the Usage-screen widgets hardcoded Layout struct now carries Usage-screen fonts, bar height, pill padding, and a Verified on real Xingzhi hardware with |
Touchless boards can opt into a global "rotate screens" timer by setting BoardCaps.auto_cycle_ms > 0. main.cpp's loop calls ui_cycle_screen() at that cadence, skipping splash (user toggles in/out of it manually) and pausing while idle_is_asleep() so a dark panel doesn't burn cycles. PWR-press resets the auto-cycle phase, so manual interaction never fights with the timer — a tap gives the user a full interval on whichever screen they just moved to. xingzhi_cube_183 opts in at 5000 ms. Existing Waveshare ports leave auto_cycle_ms = 0 and see no behavioral change. Verified on hardware: after pressing VOL_UP into Usage, the panel flips between Usage and Bluetooth every ~5 s as long as the display is awake. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Added one more commit ( The Xingzhi opts in at 5000 ms: after VOL_UP toggles out of splash into Usage, the panel auto-flips Usage ↔ Bluetooth every 5 seconds. The timer pauses on splash and while Verified on hardware. (The user also asked about adding Codex usage tracking on the same screens — that's a meaningful scope expansion since OpenAI doesn't expose Claude-style |
The Bluetooth screen overlapped its own text on 284×240: the 48×48
bluetooth glyph crowded the "Connected" status, the 48×48 trash icon
overran the "Reset Bluetooth" label, and the two-line credit footer
collided with the reset zone.
Add Layout flags bt_show_icons and bt_show_credits. The tiny
breakpoint sets both to false so the panel becomes:
[title] Bluetooth
[panel] Connected
Device: Claude Controller
Address: 90:70:69:...:FE:FD
[reset] Reset Bluetooth (text-only)
init_bluetooth_screen() now shifts the status label flush-left when
icons are off, tightens vertical row spacing, and only emits the
credit footer when bt_show_credits is true. Existing Waveshare
layouts keep both flags true → no behavioral change.
Verified with screenshot.sh on real Xingzhi hardware.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Codex CLI hits chatgpt.com/backend-api/codex/responses and gets back the same shape of rate-limit signal Claude Code exposes: x-codex-primary-used-percent (5-hour window) x-codex-primary-reset-after-seconds x-codex-secondary-used-percent (weekly / 7d window) x-codex-secondary-reset-after-seconds Daemon: load_codex_auth() reads ~/.codex/auth.json for the ChatGPT access token + account id, then poll_codex() makes a minimal streaming call to /codex/responses and parses the headers above. Codex polling is best-effort — if the token is missing/expired the poll returns None and Claude data still ships on its own. BLE payload gets four optional fields: cs / csr / cw / cwr (mirror s/sr/w/wr but for Codex) Firmware: new SCREEN_USAGE_CODEX (title "Codex", 5h pill, 7d pill, same panel chrome as the Claude usage screen). ui_cycle_screen() weaves it in only after the daemon has actually sent Codex data (have_codex_data flag in ui.cpp), so a Codex-less setup never sees an empty "Codex" screen. ui_tick_anim() runs the spinner on both usage screens. Verified end-to-end on the Xingzhi: daemon poll_codex() returned cs=3, cw=14 against a real ChatGPT Pro account, and the device renders the new screen with correct geometry on the tiny layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
One more commit ( Turns out the Codex CLI (https://github.com/openai/codex) hits ``` Daemon: Firmware: new Screenshot from the Xingzhi at 284×240 (test data injected for QA):
Happy to drop this commit if you'd prefer to keep this PR scoped strictly to the board port — it's clean enough to lift to a separate PR. |
Antigravity ships a `language_server` binary that talks to Google's
cloud-code API for the user and exposes a GetUserStatus RPC on a
random localhost port. We poll THAT instead of the upstream Google
API — no OAuth refresh needed and we get monthly prompt + flow
credit balances, plan name, and per-model quotas directly.
The endpoint is auto-discovered: pgrep for `language_server`, scrape
its --csrf_token from `ps -ww`, lsof its listening sockets (with -a
for AND-mode), then POST to:
/exa.language_server_pb.LanguageServerService/GetUserStatus
over both http and https variants (one of the two listening ports
serves each scheme).
The daemon adds these payload fields when Antigravity is running:
as / asr — prompt-credit % used + reset minutes
aw / awr — flow-credit % used + reset minutes
apn — short plan name ("Ultra", "Pro", …)
apf — formatted prompt count e.g. "500/50K"
aff — formatted flow count e.g. "100/150K"
Firmware: new SCREEN_USAGE_ANTIG (title "Antig <plan>", panels
"Prompt" + "Flow"). The credit count strings render in place of the
reset clock — monthly credits don't have a useful short reset.
ui_cycle_screen() now weaves Antigravity into the rotation only when
have_antig_data is true, mirroring the Codex screen logic. So users
with only Claude see Usage ↔ Bluetooth, Codex-only adds the Codex
screen, and a full-stack user with all three services sees:
Splash → Usage → Codex → Antigravity → Bluetooth → …
LaunchAgent fix: pgrep / ps / lsof are now resolved via /usr/bin and
/usr/sbin absolute paths so the daemon works under launchd's stripped
PATH (where /usr/sbin isn't included by default).
Verified end-to-end on the Xingzhi: device shows "Antig Ultra" with
99% / 500/50K prompt and 100% / 100/150K flow, fed by live data from
the user's Google AI Ultra subscription.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Final commit ( Turns out Antigravity ships a `language_server` binary that already talks to Google's cloud-code API on the user's behalf and exposes a `GetUserStatus` RPC on a random localhost port. Polling that locally means no OAuth refresh dance is needed and we get monthly prompt + flow credit balances, plan name, and per-model quotas directly: ``` The daemon auto-discovers the endpoint (pgrep `language_server` → scrape `--csrf_token` from `ps -ww` → `lsof` for the listening port → POST to `/exa.language_server_pb.LanguageServerService/GetUserStatus`) and adds `as`/`asr`/`aw`/`awr`/`apn`/`apf`/`aff` to the BLE payload. Firmware: new `SCREEN_USAGE_ANTIG` with title "Antig <plan>", panels "Prompt" + "Flow", credit counts ("500/50K") rendered in place of the reset clock. The cycle order with all three services is now: ``` Each "extra" screen is only inserted once we've seen its data, so a Claude-only user still just sees Usage ↔ Bluetooth. Verified live on Xingzhi against my actual Google AI Ultra subscription — device shows `Antig Ultra` / `99% / 500/50K` / `100% / 100/150K`. Maintainer: if this scope is too wide for a single board-port PR, all four Codex/Antigravity commits (`c6acb4f`, `61898ee`, plus the screen + auto-cycle infra) sit on the back of the branch and can be cherry-picked into a follow-up PR. The base port commits (`ec0395f` through `5e97d41`) are independent. |
Antigravity's GetUserStatus RPC also ships a per-model quota list (Claude Sonnet 4.6, Gemini 3.5 Flash, etc.) with remainingFraction and per-model resetTime. The daemon now formats up to 8 of those as compact "Name|status" strings: Sonnet 4.6|20% (partial quota) GPT-OSS 120B M|ok (full quota) G3.5 Flash H|47m (depleted, refreshes in 47 minutes) Names are aggressively abbreviated to fit 284 px width: "Gemini 3.5 Flash (Medium)" → "G3.5 Flash M" "Claude Sonnet 4.6 (Thinking)" → "Sonnet 4.6" Firmware: new SCREEN_USAGE_ANTIG_MODELS shows the 8 rows stacked vertically using L.usage_reset_font. ui_cycle_screen() weaves it in right after the existing Antigravity credit screen, so the full cycle on a 3-service setup is: Splash → Usage(Claude) → Codex → Antig credits → Antig models → Bluetooth → … The new screen is part of the same have_antig_data gate — Claude / Codex-only setups never see it. Verified on the Xingzhi with screenshot.sh: 7 model rows fit cleanly in 284×240 alongside the title and footer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously each row was a single label rendering "Name|status", which left the status text floating wherever the name happened to end — short names had their status mid-row, long names had it crammed near the right edge. Each row now uses two labels: the name is anchored to the left margin (COL_TEXT) and the status is anchored to the right margin (COL_DIM). The pipe separator is split off in firmware so the daemon payload stays unchanged. Status values now line up vertically in a clean right-flush column: Sonnet 4.6 20% GPT-OSS 120B M ok G3.5 Flash H 47m Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… breakpoint The earlier commits in this branch added a new HAL function (\`display_hal_lv_color_format\`), two new BoardCaps fields (\`has_touch\`, \`auto_cycle_ms\`), a third "tiny" UI breakpoint (\`height < 280\`), and a third reference port (Xingzhi) that hand-rolls a non-AMOLED display driver. The porting docs hadn't caught up — fix that here. - \`hal-contract.md\`: add a row for \`display_hal_lv_color_format\` on the display HAL table; add the tiny breakpoint to the responsive-UI section. - \`capability-flags.md\`: split a new "Runtime-only BoardCaps fields" section for \`has_touch\` and \`auto_cycle_ms\` — they don't have \`BOARD_HAS_*\` macros because no per-board source dead-strips on them. - \`adding-a-board.md\`: broaden the "Hardware you need" lists to cover non-AMOLED displays, no-touch boards, and bare-Li-Po power; expand the reference-port table to mention Xingzhi for each HAL file (display.cpp without Arduino_GFX, touch.cpp stub, power.cpp without PMU, imu.cpp full stub). - README.md: bump "Two boards" to "Three boards" and link the Xingzhi board README. No code changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two convenience tweaks for the always-plugged-in Xingzhi use case: power.cpp: power_hal_is_vbus_in() now unconditionally returns true. The previous implementation read GPIO38 (CHRG status), but that pin only goes HIGH while the charger is actively pushing current — drops back to LOW the moment the battery tops off, and is ambiguous when no battery is fitted. There's no dedicated VBUS-sense pin on the kit, so report "on USB power" outright. Combined with the default IDLE_SLEEP_WHEN_CHARGING=false, the screen no longer blanks after 30 min while the cube sits on the desk. (Trade-off: no idle sleep on battery-only runs either, which this kit isn't really designed for.) ui.cpp + board_caps.h: new BoardCaps.cycle_skip_bluetooth flag. When true, ui_cycle_screen() routes "after extras" straight back to SCREEN_USAGE instead of stopping at SCREEN_BLUETOOTH. The Bluetooth screen still exists and is reachable through a direct ui_show_screen() call — only the auto-rotation skips it. The Xingzhi sets the flag; existing Waveshare ports default to false and behave exactly as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds a third reference port: Xingzhi Cube 1.83" TFT WiFi (2-mic variant) — ESP32-S3-N16R8 + 284×240 landscape display + 3 physical buttons.
Despite "ST7789" on every forum/silkscreen reference for this board, the panel is actually NV3023. Both Arduino_GFX and TFT_eSPI ST7789 init sequences silently fail — the panel needs a specific ~30-command init that lives in the xiaozhi-esphome project. This port transcribes that sequence into a small hand-rolled SPI driver (~190 lines, no GFX library dependency for this board).
Two commits
hal: add display_hal_lv_color_format()— new HAL function so boards whose MIPI-SPI controller wants pixels MSB-first (like NV3023) can ask LVGL to write swapped bytes directly, avoiding a per-board CPU byte-swap indraw_bitmap. Existing ports returnLV_COLOR_FORMAT_RGB565and behave identically.Add Xingzhi Cube 1.83" TFT WiFi (2mic) board support— the actual board folder + platformio env + CLAUDE.md entry.Board hardware summary
0xA0(no BGR bit), no INVONpower_hal, no PMU on this board)What was tested
BoardCapsreportsXingzhi Cube 1.83, 284x240fillScreencolors render correctlypio run -e xingzhi_cube_183→ Flash 19.8%, RAM 31.3%Known limitations
0xE0/0xE3/0xE5in the init sequence).Test plan for reviewers
pio run -e waveshare_amoled_216still builds (no regressions from HAL change)pio run -e waveshare_amoled_18still buildspio run -e xingzhi_cube_183builds and produces a valid factory.bin🤖 Generated with Claude Code