Week 16 — System integration

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): how separate subsystems in a final project gain power, signal paths, mounting, and clear ownership of firmware roles. For my individual assignment I stepped back from glossy packaging and documented a real merge problem: the ILI934-class display from Week 15 needs more pins and bus attention than my XIAO ESP32‑S3 happily offers while I keep other peripherals free, so the ESP32‑WROOM module continues to anchor the TFT. Separately, NanoStat impedance readout sits on an ESP32 Pico footprint for now—which means sooner or later I need a reliable MCU-to-MCU link. I used a breadboard I²C loop (XIAO as master → WROOM as slave) driven from Cursor-assisted sketches, but flashed with the Arduino IDE, because I wanted one controlled variable at a time ( hardware + protocol sanity before tightening the mechanical envelope). In parallel, ASRPRO / Tianwen carries the offline voice UI for the Forest Spirit plant companion: a fixed Mandarin phrase table, a three-byte I²C mailbox toward the ESP host, and a Cursor-assisted firmware pass when the block toolkit refused to finish 生成模型 (“generate model”) on my first try.

Individual assignment

1) Task and motivation

Integration weeks are rarely about a single flashy demo—they are where assumptions collide. Two constraints pushed me toward this experiment:

  • Pin budget vs. TFT: The XIAO is attractive for footprint, but the display block wants SPI, reset/backlight housekeeping, optional touch I²C, and breathing room away from picky strapping pins (I chased that lesson hard in Week 15). Parking the heavy GPIO/SPI workload on WROOM keeps the UI path understandable.
  • Sensor island: For the impedance front-end I still treat the Pico-class board as its own power/ground domain early in development. That isolates analog noise experiments but creates an explicit communication requirement back to whoever owns the UX (today: probing I²C; tomorrow: pinning down message format & error handling alongside packaging).
  • Voice MCU vs. host MCU: ASRPRO owns wake/command decoding inside the vendor runtime; my Week 15 UI stack should not pretend to be a speech recognizer. A narrow I²C mailbox keeps auditability: one snid per hit, plus UART traces while I shake out timing (see the ASRPRO subsection).

The concrete engineering goal for this weekly entry is modest but measurable: I²C characters exchanged between two Espressif-class chips on a solderless breadboard, reproducible wiring, firmware in-repo, with a terse failure log (pull-ups & pin translation) so future me cannot repeat the same mirage.

2) What I refreshed while wiring

I²C is a shared, open-drain bus. Masters and slaves only ever pull lines low; highs come from pull-up resistors—either on-breakout defaults, purposeful resistors on the carrier, or (as in my case) explicit 4.7 kΩ ladders to 3.3 V. When both sides are naked module IO without those resistors, the scope looks sleepy and Wire.endTransmission() collapses into NACK / timeout chatter long before any protocol nuance matters.

Arduino-ESP32 “GPIO numbers”≠“D# silk”: XIAO boards print Dx labels that fan out through the module pinmux. I assumed D4 → GPIO4 because the digits matched; reality on my unit maps the I²C test lines to GPIO 5 (SDA) and GPIO 6 (SCL) when referencing Seeed/XIAO documentation—exactly why I now photograph the silk + datasheet row whenever breadboarding.

3) Plan

Before touching the breadboard I sketched a first-version system map for 森之精灵 / Forest Spirit. Integration weeks are where that map meets pin counts, bus roles, and “who owns the UI stack”—so I kept the sketch visible while this week’s bench work stayed deliberately narrow (see the checklist at the end of this section).

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): one ESP32‑S3 (Seeed XIAO class) as the coordination MCU, with satellite boards for display, voice, sensing, impedance, cloud dialogue, and (eventually) motion.

v1 architecture — what each island owns

  • ESP32‑S3 (XIAO) — host / orchestrator: My intent was to treat the S3 as the integration brain: it talks to the display stack over I²C, ingests voice events from ASRPRO on another I²C mailbox, samples environmental sensors, pulls impedance packets from the Pico/NanoStat slice, and forwards prompts to the cloud when dialogue needs more than offline phrases.
  • ESP32‑WROOM — display & touch front-end: Week 15 already showed this module is a better home for the heavy SPI → ILI9341-class TFT workload. In v1 the WROOM also owns I²C → capacitive touch and renders whatever the S3 decides the plant companion should show. The S3 stays the policy layer; WROOM stays the pixels-and-touch layer.
  • ASRPRO — offline voice UI: Microphone in, speaker out, wake/command decoding inside the Tianwen runtime. Recognized intents cross to the S3 as compact I²C events (not raw audio), so the host can trigger on-screen states or cloud dialogue without pretending to be a speech engine.
  • Environmental sensors on the S3: DHT11 (temperature / humidity) plus a light sensor on a simple single-wire / GPIO path—enough context for “how is the room treating this plant?” before interpreting impedance traces.
  • Pico + NanoStat — plant-side impedance: Impedance front-end on its own board, reporting structured readings back to the S3 over I²C so analog experiments stay isolated from the UI wiring mess.
  • DeepSeek API + Tavily — cloud dialogue & lookup: When the companion needs open-ended conversation, the S3 would ship voice-derived prompts (plus local sensor context) to DeepSeek and stream replies back into the UI/voice loop. For calendar date, weather, and lightweight news/search I planned a Tavily integration so the robot is not stuck pretending it knows “today” without network access.
  • Arduino UNO island (future motion): The sketch also reserves a separate UNO path for ultrasonic ranging, a PS2-style controller, and motor drive—not wired into the S3 hub in v1 on paper, but marked so holonomic motion does not get designed as an afterthought.

That v1 map is intentionally optimistic about how many buses one XIAO can master at once. This week I therefore scoped execution to one measurable link at a time—plain ESP‑to‑ESP I²C on the breadboard, plus the parallel ASRPRO mailbox thread documented in §5—before pretending the whole diagram is simultaneously stable.

This week’s bench checklist (kept deliberately narrow)

  1. Power both boards cleanly, tie GND, add >=4.7 kΩ pull-ups on SDA/SCL.
  2. Use the XIAO as I²C master transmitting chunked UTF‑8 payloads; compile in Cursor.
  3. Flash via the same Arduino IDE / esptool path I already trusted from earlier weeks.
  4. Run WROOM slave firmware mirroring common ESP32 patterns: onReceive → ring buffer → loop → Serial.print, never blocking inside the ISR (Serial in IRQ context is bait for deadlock).

4) Build & bring-up diary

Breadboard with two ESP boards, jumper wires for I2C, and discrete pull-up resistors to 3.3 V
Breadboard bring-up: two microcontrollers sharing SDA/SCL; once I bridged GND and added ~4.7 kΩ pull-ups toward 3.3 V, the bus stopped behaving like an RC ghost story.
Reference photo of Seeed Studio XIAO ESP32-S3 labeling next to breakout wiring
Reference reminder that silk-screen D-numbers are not magically equal to MCU GPIO IDs—this is the sheet I leaned on after mis-assigning D4/D5.

Mistake A — forgetting pull-ups: Everything compiled, flashing succeeded, Serial showed my master banner, yet endTransmission() returned NACK/error codes. Only after metering the highs did I admit the lines were drifting instead of snapping up between edges. Discrete 4.7 kΩ tied to 3.3 V brought textbook rise times back.

Mistake B — swapped GPIO assumption: Even with healthy pull-ups, I chased ghosts until I accepted that XIAO D4 ⇒ GPIO 5 and D5 ⇒ GPIO 6 for the pins I routed to the WROOM GPIO19 (SDA)/GPIO21 (SCL) pair documented in the sketch comments. Updating the Wire.begin() pins ended the hallucination instantly.

Computer screen showing Arduino-style source code edited in Cursor with syntax highlighting
Firmware drafting inside Cursor (AI-assisted scaffolding for chunking/long timeouts) before I hand off to Arduino IDE for upload—same toolchain split I used Week 15 because it trims context switching during hardware debug.
Serial monitor showing master I2C send failure or NACK before fixes
Early master trace while missing pull-ups plus wrong pins still produced NACK/timeout storms—classic “looks like software, hurts like analogue.”
Serial monitor showing successful chunked I2C transmission from ESP32-S3 master
After the resistor + pin corrections, the master logs each phrase every two seconds—the UTF‑8 payloads follow a chunking discipline so the slave ring buffer is not overrun in one giant transaction.
Serial monitor attached to ESP32 WROOM decoding received I2C text from the slave firmware
Matching WROOM slave capture: characters echoed over USB, proving the bytes crossed the breadboard—the integration milestone I wanted before merging NanoStat payloads.

5) ASRPRO (天问) voice mailbox — parallel integration thread

The breadboard I²C exercise above is intentionally vanilla ESP‑IDF / Arduino Wire. The voice path for 森之精灵 / Forest Spirit is different: it runs on an ASRPRO module with the Tianwen (天问) graphical toolchain—offline wake + command slots, vendor audio resources (playid), and hooks that only make sense once you accept their asr.h / setup.h contract instead of pretending it is a normal setup()/loop() sketch.

Why I cared this week: integration is not only wires—it is also who owns which API. 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 and sensors. A tiny I²C mailbox (flag + 16‑bit snid) keeps that boundary explicit; details are in DATA_FLOW.md.

What went wrong first: inside Tianwen’s block flow, hitting 生成模型 sat spinning for a long time and never returned a usable build. Rather than burning another evening on the Cloud-only step, I exported the vendor C++ example, dropped it into Cursor, and asked an agent to learn the template and merge in my phrase map, UART banner, mute/listen timing, and the mailbox publisher. Once the source looked like a coherent ASRPRO project again, the desktop compile path behaved; the block page stayed my place to define phrases and IDs, not the bottleneck for every iteration.

Tianwen pipe — three things that stalled me mid-debug

Compile noise from Cursor snippets: When I pasted AI-generated patches into the Tianwen C++ branch, the first passes did not compile: stray punctuation—extra commas, bogus preprocessor tokens, and other junk the model had inferred around Chinese comments—showed up as hard errors rather than benign warnings. I treated those lines like diff noise: compare against the toolchain error list, strip the offending characters, and re-compile until only real semantics were left behind.

Tianwen error dialog stating Chinese-mode recognition phrases cannot contain punctuation, digits, English letters, or spaces
Separate but related friction in the phrase table: in Chinese mode, wake/command strings must be pure Han characters—no punctuation, digits, English, or spaces—or Tianwen blocks 生成模型 before the C++ branch even compiles.

Could not bind ASRPRO as I²C slave in Tianwen: I initially wanted ASRPRO to behave as an I²C peripheral so the ESP host could clock out the mailbox. In practice the Tianwen GUI never gave me a workable “ASRPRO as slave device” configuration for that topology, so I pivoted the bus hierarchy: ASRPRO acts as master on the dedicated voice mailbox I²C, while the Seeed XIAO ESP32‑S3 listens as peripheral on that pair. Separately—this is why I harp on isolation—the XIAO still masters its own TFT / UI peripherals on a second logical I²C path (different SDA/SCL pins and routing). Same chip, two roles across two wires: mailbox side is slave to ASRPRO; presentation side stays an ESP‑side master toward the ILI934‑class stack documented in Week 15. That decision forced an explicit rewiring session so I wasn’t multiplexing contention on one bus alias.

Breadboard with ASRPRO V2.0, ESP32 WROOM, and XIAO ESP32-S3 wired for voice mailbox bring-up
Bench snapshot after the bus-role pivot: ASRPRO V2.0 (left), ESP32‑WROOM (centre), and XIAO ESP32‑S3 (right) share ground while I re-routed the mailbox pair away from the TFT I²C pins documented in Week 15.

生成模型 (generate model) vs. trusting the exemplar: The cloud/blocked toolkit step stalled my first “happy path”; the fix that stuck was pairing the earlier punctuation cleanup with a deliberate study-the-official-sample workflow in Cursor (figures below)—having the agent internalize vendor entry points until the merged source structurally matched the reference tree and built locally. Same three‑byte semantics on the wire; iteration speed recovered once I stopped treating the spinner as the authoritative compile gate.

Tianwen Block IDE with Generate Model dialog stuck at low progress while compiling ASRPRO firmware
生成模型 spinner in Tianwen Block: progress crawled for a long time (here still at single-digit percent) even after phrase cleanup—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: I fed the official ASRPRO example to the agent, compared its structure to my stuck block export, and iterated the C++ until it matched both the vendor entry points (ASR_CODE(), hardware_init()) and my mailbox plan. This was faster than waiting on a stuck cloud model step when I still had to verify timings on hardware.

Firmware behavior (test build): ASR_CODE() runs on each hit: vol_set mutes the DAC briefly, set_state_enter_wakeup opens a 5 s listen window, the mailbox publishes snid (wake = 0, commands ≥ 1), and Serial at 115200 8N1 prints both the mapped Chinese phrase and the three mailbox bytes for cross-checks. Phrase text is not open microphone transcription—it is the offline table mirrored in snid_to_phrase().

Artifacts: asrpro_forest_spirit_voice_test.cpp (project-local copy for documentation—drop into your Tianwen ASRPRO tree where asr.h lives) and DATA_FLOW.md for the host-facing summary.

6) Reproduce this bench (minimal table)

Signal XIAO ESP32‑S3 (master) ESP32‑WROOM (slave)
SDA GPIO 5 (my routing from D4 silk) GPIO 19
SCL GPIO 6 (my routing from D5 silk) GPIO 21
References Common GND; SDA/SCL pulled to 3.3 V with ~4.7 kΩ; logic at 3.3 V only.
Parameters Slave address 0x55; start near 100 kHz; master uses chunked writes plus a generous Wire timeout guard for clock stretching.

7) Source sketches in-repo

Libraries/tooling: Arduino-ESP32 core, built-in Wire helper. Flashing/upload: Arduino IDE board profiles for ESP32-S3 Dev Module and a stock Espressif dev-module target for WROOM.

8) Conclusion & next integration hooks

Packaging is still explicitly unfinished—Fab Academy explicitly allows logging integration work before enclosure glamour shots land. Locally I proved that MCU islands cooperate once both I²C pull-ups and the XIAO GPIO numbering trap are respected. That gives my NanoStat Pico slice a plausible neighbor bus when I merge impedance packets back toward the TFT stack.

On the voice side, the ASRPRO mailbox is the complementary lesson: sometimes integration means respecting a vendor runtime and carving out a narrow protocol (three bytes + UART debug) instead of rewriting everything on the host. When Tianwen would not expose the ASRPRO peripheral binding I assumed, flipping to ASRPRO master → XIAO peripheral on a dedicated mailbox bus—while leaving the TFT path on XIAO’s separate I²C master instance—proved easier than fighting the GUI. Next I need to harden the ESP reader/listener logic against missed transactions and line up the snid map with whatever UI states final-project.html actually exposes on screen.

The next mechanical pass will cover real strain relief, disciplined ground returns, and I²C routing once the breadboard disappears—those bullets feed directly back into final-project.html packaging notes whenever the CAD stabilizes.