Week 16 — System integration (Forest Spirit v1)

This week aligns with Fab Academy guidance on system integration (see the 2026 Assignments and Assessment hub and the 2025 module text for System integration → learning outcomes). My individual assignment documents the first integrated version of 森之精灵 / Forest Spirit—the plant companion stack I can demo on a breadboard without pretending every future sensor island is wired yet: ASRPRO recognizes fixed Mandarin phrases from an offline table, the Seeed XIAO ESP32‑S3 hub reads those events and (when WiFi is up) calls the DeepSeek API, and the ESP32‑WROOM drives the ILI9341-class TFT so assistant text and UI pages show up on screen. Week 15 proved the display and HTML-to-TFT path; this week is where voice, cloud dialogue, and pixels share one I²C bus discipline and a reproducible firmware tree in code/week15-individual/final/.

Individual assignment

1) Task and motivation

Integration weeks are where separate subsystems either cooperate or fight each other. The v1 acceptance test I cared about was narrow but real:

  1. Say a phrase from the offline ASRPRO table (wake 灵葭 or a command slot)—not open microphone dictation.
  2. Let the S3 hub turn dialogue intents into an HTTPS call when the network is up.
  3. See the result as UTF‑8 text on the ILI9341 through the WROOM five-page UI, driven over the 0x55 I²C TLV link documented in DATA_FLOW.md.

That is intentionally smaller than the full paper sketch (Pico impedance, UNO motion, Tavily lookup)—those stay on the roadmap. v1 is voice menu → API → screen, with UART mirrors on each board so I can debug without guessing.

I also ran a controlled ESP‑to‑ESP I²C loop (§6) and spent time on display + bus hygiene when vertical stripes showed up after rewiring (§5)—both lessons feed the same three-board bench, not a separate science project.

2) What I refreshed — v1 data path

The signal chain below is what I actually flashed for the v1 demo. It extends the Week 15 TFT work but names each MCU’s job explicitly.

  ┌──────────────┐   3-byte mailbox    ┌──────────────────┐   HTTPS (online)   ┌─────────────┐
  │ ASRPRO V2.0  │ ─── I²C 0x56 ─────▶│ XIAO ESP32‑S3    │ ─────────────────▶│ DeepSeek    │
  │ offline ASR  │   + UART 115200    │ integration hub  │                   │ API         │
  │ fixed phrases│                    │ DHT11 + light    │◀──────────────────│ assistant   │
  └──────────────┘                    └────────┬─────────┘                   │ text        │
         │ mic / speaker                      │ I²C TLV 0x55                             │
         │ vendor TTS                         ▼                                          │
         │                           ┌──────────────────┐                                │
         │                           │ ESP32‑WROOM      │◀───────────────────────────────┘
         │                           │ ILI9341 + FT6336 │
         └───────────────────────────│ 320×240 UI pages │
                                     └──────────────────┘
            
Forest Spirit v1 — integrated path: ASRPRO owns wake + command decoding; the S3 owns policy + cloud dialogue; the WROOM owns pixels and touch. Phrase text is table-driven (snid), not streaming ASR.

I²C ties the boards together: open-drain SDA/SCL, ~4.7 kΩ pull-ups to a stable 3.3 V reference, common GND. On the XIAO, D4/D5 map to the documented SDA/SCL pair (not “D4 = GPIO4” by habit—see §5). The WROOM listens at 0x55; the voice mailbox uses 0x56 with three bytes per hit (flag + snid)—details in DATA_FLOW.md.

Arduino-ESP32 “GPIO numbers” ≠ silk D# labels still bit me during bring-up; I now photograph silk plus the datasheet row whenever I breadboard.

3) Plan — v1 map vs. what I built this week

Before touching the bench I sketched a first-version system map for Forest Spirit. v1 in production is the centre column of that sketch; the Pico, UNO, and Tavily boxes stay future islands.

Hand-drawn v1 block diagram: ESP32-S3 hub, ASRPRO voice, WROOM display stack, sensors, Pico impedance, DeepSeek API, and UNO motion island
Forest Spirit v1 plan (paper sketch): the shaded path this week is ASRPRO → S3 hub → DeepSeek → WROOM ILI9341; other blocks are placeholders on the same page.

Three boards — who owns what in v1

  • ASRPRO — offline voice UI: Microphone in, speaker out, wake/command decoding inside the Tianwen runtime. Each hit publishes snid over the mailbox and prints the mapped Chinese phrase on UART—not raw audio to the host.
  • XIAO ESP32‑S3 — hub: I²C master on D4/D5; polls 0x56, samples DHT11 + light sensor, calls DeepSeek for dialogue intents (snid ≥ 7 when WiFi is up), pushes TLV packets to 0x55.
  • ESP32‑WROOM — display front-end: Week 15’s SPI → ILI9341 + FT6336 touch stack; renders five swipe pages and paints chat text from the hub.

Bench checklist (v1)

  1. Flash wroom-display/, then s3-hub/ (with local secrets.h), then ASRPRO in Tianwen.
  2. Wire shared I²C: XIAO D4/D5, WROOM GPIO19/21, ASRPRO PA2/PA3; pull-ups + GND.
  3. Say 灵葭 and a dialogue command; confirm Serial on each board and text on the TFT chat panel.
  4. If the panel stripes or touch glitches, fix power domains before blaming the LLM (§5).

4) ASRPRO (天问) — offline phrases and mailbox

The voice path runs on ASRPRO with the Tianwen (天问) toolchain—offline wake + command slots, vendor audio resources, and hooks that only make sense once you accept their asr.h / setup.h contract.

Why a mailbox: I needed one deterministic bridge from “chip heard phrase n” to “host MCU can act on it,” without stuffing a full voice stack into the same firmware that already juggles display traffic. A tiny I²C mailbox (flag + 16‑bit snid) keeps that boundary explicit.

What went wrong first: inside Tianwen’s block flow, hitting 生成模型 sat spinning for a long time. I exported the vendor C++ example, merged phrase map and mailbox code in Cursor, and compiled locally once the source matched vendor entry points (ASR_CODE(), hardware_init()).

Tianwen error dialog stating Chinese-mode recognition phrases cannot contain punctuation, digits, English letters, or spaces
Phrase table rule: in Chinese mode, wake/command strings must be pure Han characters—no punctuation, digits, English, or spaces—or Tianwen blocks 生成模型 before the C++ branch compiles.
Tianwen Block IDE with Generate Model dialog stuck at low progress while compiling ASRPRO firmware
生成模型 spinner—one reason I stopped treating the cloud step as the only compile gate.
Cursor editor session with ASRPRO C++ source and AI agent helping refactor the example project
Cursor-assisted pass: compare block export to the official example until structure and mailbox timings matched hardware tests.

Bus-role note: I initially wanted ASRPRO as an I²C peripheral the S3 could poll. Tianwen did not give me a workable slave binding, so the production v1 tree in final/asrpro/ uses vendor asr_i2c_slave.* with the S3 as master on the shared bench bus—the same three-byte layout either way. The earlier master/slave pivot narrative is archived in asrpro_forest_spirit_voice_test.cpp for documentation.

Breadboard with ASRPRO V2.0, ESP32 WROOM, and XIAO ESP32-S3 wired for voice mailbox bring-up
Three-board bench: ASRPRO (left), WROOM display stack (centre), XIAO hub (right)—shared I²C after pull-up and GND discipline.

5) ILI9341 bring-up — stripes, burn-in, and I²C power domains

Once v1 firmware was flashed, the panel sometimes showed vertical tree-like stripes across the whole image. I treated that as an integration failure, not a font bug: if the display bus is marginal, both touch and “API reply on screen” look broken even when DeepSeek returns 200 OK.

Bench setup while re-debugging ILI9341 capacitive touch and display wiring after reconnection
Touch + display re-debug (2026‑05‑25): full power cycle, then FT6336 checks and jumper re-seat— separating software regression from breadboard mechanics.

I reflashed the Week 15 screen-test firmware (patterns + touch readbacks only). Stripes still appeared, so I suspected panel persistence / burn-in from long bright demos. Resting the backlight cleared most ghosting; I still avoid harsh static white fields between sessions.

Stripes with known-good test sketch: points to panel health as well as wiring— backlight off helped; recurring comb patterns needed a bus fix (below).
TFT showing vertical line artifacts during I2C communication debug
Vertical lines after another flash pass: pushed me to meter SDA/SCL and compare against a clean display test once the panel rested.

The durable mistake: I had tied 3.3 V to the display module supply and reused that same rail as the I²C pull-up reference. Under load the display’s current spikes droop the pull-up reference, which distorts I²C highs and confuses FT6336 reads and hub traffic together.

Updated breadboard wiring: separate 3.3 V paths for display power and I2C pull-ups
Corrected wiring (2026‑05‑26): display VCC and I²C pull-ups use separate 3.3 V taps; common GND; discrete pull-ups on SDA/SCL. Stripes and touch oddities stopped reproducing on demand—prerequisite for trusting v1 end-to-end.

6) v1 demo — voice → API → ILI9341

The hero clip below is the v1 milestone: I say 灵葭, the module wakes and answers with vendor TTS; for dialogue commands the S3 calls DeepSeek when online and the WROOM paints the reply on the TFT. That matches the flow diagram in §2.

Forest Spirit v1 — dialogue with 灵葭: offline ASRPRO phrases → S3 hub → (online) DeepSeek → ILI9341 chat UI. Archived with the FINAL bundle; interface mockups and first TFT port remain on Week 15.

Firmware tree (reproduce v1):

7) Appendix — controlled ESP‑to‑ESP I²C sanity check

Before trusting the three-board stack, I ran a minimal master/slave loop (XIAO → WROOM) with chunked UTF‑8 and a ring-buffer slave—same pull-up and GPIO lessons, zero ASRPRO in the loop. That work lives in code/week16-individual/ and explains the Serial captures below.

Breadboard with two ESP boards, I2C jumpers, and discrete pull-up resistors to 3.3 V
Two-MCU I²C test: shared SDA/SCL, GND, and ~4.7 kΩ pull-ups—without these, Wire.endTransmission() looked like a software bug.
Reference photo of Seeed Studio XIAO ESP32-S3 pin labels
XIAO pin reference—D4/D5 ≠ GPIO4/GPIO5 on my unit (I²C landed on GPIO 5/6).
Source code edited in Cursor before Arduino IDE upload
Draft in Cursor, flash in Arduino IDE—same split as Week 15.
Serial monitor showing master I2C NACK before pull-up and pin fixes
Master trace with missing pull-ups and wrong pins—NACK storms before the bus was healthy.
Serial monitor showing successful chunked I2C transmission from ESP32-S3 master
Master after fixes—chunked UTF‑8 every two seconds.
Serial monitor on ESP32 WROOM slave decoding received I2C text
WROOM slave echo—proof bytes crossed the breadboard before I merged voice + API + TFT.
Signal XIAO ESP32‑S3 (master) ESP32‑WROOM (slave)
SDA GPIO 5 (from D4 silk on my board) GPIO 19
SCL GPIO 6 (from D5 silk) GPIO 21
Parameters Slave 0x55 in the sanity sketch; ~100 kHz; common GND; 3.3 V pull-ups only.

8) Conclusion

Week 16, for me, is the first integrated Forest Spirit system: fixed offline phrases on ASRPRO, cloud dialogue on the S3 when WiFi allows, and readable answers on the ILI9341. Packaging is still open—Fab Academy allows logging integration before enclosure glamour—but the bench story is auditable: photos, stripe video, FINAL firmware tree, and DATA_FLOW.md.

The surprises were mostly analog-adjacent: panel persistence once, but the recurring comb pattern traced to sharing display VCC with I²C pull-up reference. The controlled two-ESP I²C exercise paid off separately—it taught pull-ups and XIAO pinmux before I debugged voice and API together.

Next (wildcard / v2): open-ended speech for the three Forest Spirits is documented on Week 17 (in progress)—that path retires the fixed snid table while the v1 bench wiring lessons stay valid here.