StackChan Phantom Touch on CoreS3: Why We Reverted to a Physical Key
Contents
Update (2026-09-27): Follow-up on feeding room climate (CO2/temp) into LLM context, investigating phantom petting touch triggers, and driving neck motion from response emotion → StackChan CoreS3 with SCD41: Feeding Room CO2 into LLM Voice Context
In the previous post, we rebuilt StackChan’s voice chat firmware with ESP-IDF and linked it directly to the local voice server over Tailscale.
The trigger for talking was tapping the top of the head. Since phantom taps were detected right after boot without anyone touching it, we had added a “warming-up” grace period that ignores taps until startup settles.
The following day, about 22 hours after boot, StackChan started recording on its own and spoke a reply.
No one was touching it, and serial logging was not running.
Since the false triggers were not limited to right after boot, we started isolating where they were coming from.
Test Environment
| Item | Environment Used |
|---|---|
| Hardware | M5Stack CoreS3 (ESP32-S3), StackChan Body (robot chassis housing the CoreS3) |
| Overhead Touch | 3-zone capacitive touch on top of the Body (Si12T touch sensor IC, wired to CoreS3 internal I2C) |
| Voice Chat Firmware | ESP-IDF 5.5.4, arduino-esp32 3.3.10, M5Unified 0.2.17, StackChan-BSP 1.1.0 |
| Reference Firmware | Official m5stack/StackChan firmware (ESP-IDF 5.5.4) |
| Placement | Desk surface, clear of surrounding objects |
Overhead Touch Was the Only Path to Start Recording
In this voice chat firmware, recording could only be triggered by an overhead tap or a test command sent over serial.
Serial was disconnected at the time.
Lifting or moving the unit did not trigger it either, ruling out accidental external physical contact.
The capacitive touch IC was reporting “touched” to the CoreS3 despite no actual contact.
The tap detection logic required the maximum intensity across the three touch pads (0–3) to reach 3.
Logging Conditions for Every Touch Event
Each time an intensity-3 touch occurred, we logged the timestamp, uptime, time elapsed since the last face redraw, neck servo state, speaker/recording state, and Wi-Fi RSSI.
| Parameter | 17 Events During Grace Period | 14 Events After Grace Period |
|---|---|---|
| Servo Active | 11 times | 5 times |
| Speaker / Recording | All idle | All idle |
| Time Since Redraw | Scattered (42 ms – 66 s) | Scattered |
| Wi-Fi RSSI | Around -55 dBm | Around -55 dBm |
After boot, the neck servo is only supposed to move once to return to its home position, so why it reported “active” remained unclear.
No clear correlation appeared with any other parameter either.
Double-Tapping Failed to Filter False Triggers
To distinguish ghost inputs from actual user touches, we changed the trigger to require two taps within 1.5 seconds before starting recording.
After the first tap, the screen displayed “Tap once more”.
Within 90 seconds after the grace period ended, double-taps triggered four times, and all four advanced all the way to recording.
The false triggers were not arriving as isolated single hits; they arrived in tight clusters.
The “Tap once more” prompt kept appearing without anyone touching it, rendering it unusable.
Leaving overhead touch in logging-only mode without triggering recording captured 504 contact events in roughly 10 minutes, 257 of which reached intensity 3.
The Official Firmware Avoids Taps
We checked the overhead touch implementation in the official firmware (hal_head_touch.cpp, head_pet.h).
| Parameter | Official Firmware | Voice Chat Firmware |
|---|---|---|
| Handled Gesture | Petting only (single taps do nothing) | Tap starts recording |
| Low-Power Sensor Scan | Enabled | Disabled (turned off to avoid dropping light taps) |
| Baseline Recalibration | Never | Frequently (after Wi-Fi connection, after dialogue, etc.) |
| Polling Interval | Every 50 ms via dedicated task | Every ~10 ms inside main loop |
Petting is calculated by deriving a weighted centroid position (-100 to 100) from the intensity of the three pads, triggering when the centroid moves more than 40 units from the initial touch point.
The official documentation notes that swiping vertically along the top touch area makes it happy.
The sensitivity (LOW level 3) and detection registers matched our configuration.
Switching Speech Triggers to a Key Unit
Since tapping was unreliable, we switched speech triggering back to a physical switch.
We plugged the single-button Key unit (U144) used previously into the Grove port on top of the StackChan Body.
StackChan has three Grove ports. In the official PinMap, PORT.A maps to G2/G1, PORT.B to G9/G8, and PORT.C to G17/G18.
PORT.A on the side was kept free for the CO2 sensor, so we used the overhead port.
Restoring the auto-detection routine from our earlier firmware (which monitors the four pins across Ports B/C and binds to whichever pin is pressed first) assigned it to Port C (G18).
| Stage | Measured Time |
|---|---|
| Recording (1.3 s until speech began) | 4,132 ms |
| Transmission (64,187 bytes) | 295 ms |
| Server emotion response arrived | 4,035 ms |
| Download first sentence audio (39,724 bytes) | 186 ms |
| Playback start of first sentence | 7,869 ms |
| Full dialogue completion | 11,726 ms |
Overhead “double-tap” events continued to fire in the background during this test, but since touch was set to logging-only, recording never kicked off unexpectedly.
However, the straight Grove cable sticks straight up from the head, hitting fingers whenever attempting to pet it.
Right-angle Grove cables were not readily available as prefabricated cords; items marketed as “Grove right-angle” were 90-degree PCB header sockets (Akizuki Denshi, Seeed Studio).
The USB-C port at the base supplies power and data for the CoreS3 itself, so it cannot be repurposed for a switch.
Ghost Triggers Persisted Even With Petting-Only
We abandoned tapping altogether and set the screen to display a happy face for 2.5 seconds only when a pet gesture was detected.
If ghost triggers still occurred under petting detection, the capacitive sensor could not be used.
Without anyone touching it, over 40 petting triggers fired in roughly 20 minutes.
During this test, the key became unresponsive and the face froze mid-blink.
The PC logging script abruptly dropped the serial port, causing a DTR/RTS transition that froze the display—an issue unrelated to the touch sensor. Once the PC logging script was fixed, testing resumed normally.
Matching Official Settings and Detection Logic
We re-enabled low-power scanning, disabled baseline recalibration, and slowed the polling rate to every 50 ms.
Even so, during roughly 17 minutes following a conversation, 26 petting triggers and 1,034 contact events occurred.
Next, we ported the exact petting detection code from the official repository.
Within 75 seconds of boot, 42 petting events fired, leaving the robot stuck in a permanent happy expression.
Because the official code treats any intensity of 1 or higher as a contact, having pads continuously report phantom intensities of 1–2 caused the weighted centroid calculation to drift, registering as continuous swipe movements.
We disabled the facial reaction and kept it in logging-only mode.
The target registers, bit interpretations, initialization sequence, and parameter values (sensitivity, detection, and low-power scanning) were now completely identical to the official implementation.
Running Only the Official Touch Task
Building and flashing the full official firmware under ESP-IDF 5.5.4 stalled on a setup screen displaying “ID: (MAC address)” and “Look for me in the app to start setup.”
The animated face that responds to touch never appeared until Wi-Fi was configured through the companion mobile app.
Instead, we modified only main.cpp in our local clone, keeping the power subsystem, I2C, and overhead touch polling tasks untouched from the official codebase, while logging touch and pet counts every minute.
Before flashing, we backed up the NVS (non-volatile storage) partition containing Tailscale identity keys.
| Firmware Build | Observation Duration | Contacts | Pet Triggers |
|---|---|---|---|
| Voice chat firmware (identical settings) | ~17 min | 1,034 | 26 |
| Official touch code | 20 min | 0 | 0 |
To verify that zero events did not mean the sensor was dead, we deliberately stroked the head; all 3 petting gestures were correctly detected.
Taps registered in 5 out of 7 attempts, with 1 being misclassified as a pet.
With low-power scanning enabled, polling slows to roughly 200 ms intervals, easily dropping brief taps.
If a tapping finger slides slightly across the pads, the centroid shifts and turns into a pet.
This explains why the official firmware likely restricts touch interactions strictly to petting.
Ghost Triggers Resisted Component Shutdowns
Returning to the voice chat firmware, we shut down active peripherals one by one to see whether contact counts per minute would drop.
| Disabled Component | Contacts per Minute |
|---|---|
| None (baseline) | 62, 77 |
Device update loop (M5.update) | 53, 27, 48, 11 |
| Servo torque | 54, 26, 60 |
| Wi-Fi TX power (20 dBm → 2 dBm) | 63, 28, 63 |
| Speaker | 69, 69, 70 |
| Face redrawing | 75, 68, 64 |
I2C read failures were zero throughout; the sensor IC genuinely reported contact flags.
Disabling individual peripherals did not eliminate the noise on the spot.
The ghost triggers occurred in waves: some boots showed 55–80 contacts per minute, while others started at zero.
On one boot, contacts abruptly plummeted to zero after about 7 minutes, and restoring disabled components (backlight) did not bring them back.
Overnight Run With Official Touch Code
The 20-minute run with zero events on the official code might simply have hit a quiet window.
We ran the official touch task overnight from 20:39 to 00:31, logging for roughly 3 hours and 50 minutes.
| Firmware Build | Duration | Contacts | Pet Triggers |
|---|---|---|---|
| Official touch code | ~3 h 50 min | 4 | 3 |
| Voice chat firmware (active periods) | 1 min | Tens | Over 10 |
Even the official code logged phantom petting roughly once an hour with no one near the device.
Capacitive touch was never completely silent, but our voice chat firmware saw noise escalate by orders of magnitude during certain intervals.
What drives that surge remains undetermined.
For now, speech interaction is handled by the physical Key unit, while the overhead touch sensor remains restricted strictly to logging both taps and pets.