This document owns the current CoreS3 target, display and hardware behavior, power behavior, build procedure, and hardware verification boundary.
Several words below are used in a narrower sense than ordinary English, and most are also names in the code or on the wire. GLOSSARY.md gives the plain meaning of each one and where the name appears.
../README.mdandPRODUCT.mdavoid these terms entirely.
The firmware target is M5Stack CoreS3 on Stack-chan hardware. PlatformIO pins
the ESP32 platform, M5Unified, ArduinoJson, and StackChan-BSP in
firmware/stackchan/platformio.ini. StackChan-BSP uses commit
f7ed40e6f5d9a1d08440cb926f3a0865b81882f8.
The generated scene package is governed by
../protocol/scene-package.json. The
builder and firmware parser use that one unversioned key-and-limit contract;
there is no legacy scene reader. The scene-contract generator also consumes
the tracked expedition scene and its pinned asset manifests to generate the
firmware resource identifiers, names, kinds, playback, timing, and fixed
presentation bindings. Firmware contains no second handwritten resource
catalog. The current contract requires 21 raw indexed resources, a 320×240
canvas, a 24 Hz presentation rate, at most 16 palette colors per resource,
manifest-owned text and controls, bounded geometry, and a package-size
ceiling.
The Python builder and the C++ parser are two independent implementations of that one contract, and they must be changed together. Treat any change to a package rule — geometry, overlap, alphabets, limits, resource kinds — as one edit spanning both languages.
The guard against divergence is the tracked native parser vector: the builder generates it, and the native suite deserializes it with the production firmware parser, so a package the builder accepts and the parser rejects fails there. That guard only fires if the vector is regenerated after the rule changes and the native suite is run before flashing. Skip either and the first symptom is the device rejecting the package at boot with a scene-contract failure, which reads as a hardware or install problem and is neither. Regenerating the vector is therefore part of changing a rule, not a follow-up step.
At startup the package loader exclusively orders LittleFS mount, directory inspection, exact manifest parsing, PSRAM allocation, raw resource loading, hash and pixel-index checks, visible-layout validation, publication, and failure cleanup. It publishes no frame until every resource passes. Failure releases the allocation and reports one bounded asset-error screen.
The compositor converts one immutable scene snapshot directly into two internal-RAM RGB565 320×16 DMA stripes. There is no full-frame canvas, decoded asset canvas, PNG decoder, display readback, or LCD-controller scroll. Background motion advances only after a complete presented working frame. The absolute 24 Hz scheduler skips expired slots without catch-up rendering and records read-consistent cadence metrics.
Firmware requires one explicit code-chan calibration record before the ordinary protocol application may start. The versioned NVS record contains the confirmed raw yaw and pitch home positions. It does not read StackChan-BSP's legacy zero-position settings, and there is no built-in 460/620 runtime fallback. A missing, malformed, or unsupported record selects the local calibration application. An unavailable NVS boundary shows a calibration persistence fault and cannot be treated as permission to overwrite evidence.
To recalibrate a device that already has a valid record, hold the top-left 64×64 touchscreen corner while resetting it. Calibration is mutually exclusive with the host protocol, scene loader, and expression lifecycle. Its setup frame is transferred and the display backlight-rail enable is confirmed before servo power is admitted.
BodyCalibrationProcess owns the operator and persistence workflow, while the
shared BodyActuationProcess owns every servo transition. That child owner
first initializes the target with the servo-power latch disabled, enables the
servo rail, and requires live readback that both servo torque registers are
disabled. The operator physically centers the head
with the screen vertical, holds it steady, and taps
SET CURRENT POSITION AS HOME. Firmware then reacquires both installed
position ranges and directional dead zones and requires three consecutive
stationary live-pose samples at least 100 milliseconds apart, with no more
than one raw step between samples. The candidate home must keep the complete
supported yaw and pitch motion envelope inside the installed position ranges.
Stable capture is not calibration success. The device next presents
START SAFE MOTION TEST; torque remains disabled until that separate touch is
released. BodyCalibrationProcess then asks the same
BodyActuationProcess to verify the exact candidate it captured. The
actuation owner synchronizes the candidate, confirms torque enable, and runs
the catalogued four-pose program in order: yaw +12 degrees,
center yaw from the right, yaw -12 degrees, and center yaw from the left, all
at pitch 5 degrees. Five degrees is the documented safe lower pitch boundary
rather than the physical zero-degree extreme. Both directional center returns
must pass. Each phase advances only from the motion owner's exact goal
readback and live ControllerSettled result. A load-limited, device-fault,
invalid, rejected, unavailable, or expired operation fails calibration and
runs the output-disable tail; residual size and time never establish successful
return.
Only after the actuation owner reports that all four live poses
controller-settled does the calibration coordinator write the version-2
record. A contradictory verification result cannot reach storage. Success
requires NVS commit, an exact same-boot readback of that
verified record, and confirmed torque-disable and servo-rail-disable commands.
An older capture-only record is invalid and selects calibration again. Only
then does the screen offer RESTART; restart occurs after that touch is
released, so it cannot become the recalibration boot gesture. Failure
preserves its cause and requests cleanup through the same actuation owner,
which attempts both output-disable operations, and offers no
success or automatic restart path. A calibration display failure has its own
typed cause; it requests body cleanup only after body output may have been
admitted. During ordinary activation, the motion
owner rechecks the stored home and complete motion envelope against the
currently installed servo configuration before it can synchronize goals or
permit torque.
DeviceConnection owns bridge epoch, handshake state, the accepted host
replica, activity deduplication, permission answerability, the current
runtime-status delivery, and outbound routing. The first valid hello on the
USB serial stream establishes an epoch on an unowned connection. Stale epochs
cannot mutate the replica or receive decisions, acknowledgements, or touch
telemetry. Firmware retains and replays one runtime status ID and payload until
the host returns the matching current-epoch runtime_status_ack; only a newer
lifecycle result may supersede it. Replay timing transports evidence and never
advances lifecycle.
USB serial becomes Uncertain after 20 seconds without a valid current-epoch
message. A later valid current-epoch message before 40 seconds restores
Connected; continued silence through 40 seconds clears the replica and ends
as connection loss. A new hello establishes a replacement epoch. USB write
completion is transport evidence, not the peer's application-level
acknowledgement.
A current-owner goodbye clears answerability and deduplication together with
the old replica. ApplicationProcess then removes connected expression
authority, publishes the disconnected projection, admits the newer catalogued
disconnected body program, and only then asks DeviceConnection to send
goodbye_ack. The cleared connection retains only the exact goodbye epoch
needed to re-acknowledge a lost receipt while still unowned; an old goodbye
cannot clear a replacement epoch.
ExpressionLifecycle is the sole owner of display/body ordering:
Initializing
-> ActivatingDisplay -> ActivatingBody -> Interactive
-> QuiescingBody -> SuspendingDisplay -> Suspended
-> ActivatingDisplay ... (wake)
-> FaultQuiescing -> FaultPresenting -> Fault (failure)
Initialization requires both a configured board-power boundary and a real display-ready result. Display initialization has a five-second failure deadline. Each accepted display activate or suspend command has a two-second failure deadline. Result admission and periodic observation use the same deadline predicate, so loop order cannot turn a late result into success. Elapsed time never establishes an active or suspended state.
BodyActuationProcess owns body lifecycle policy: activation ordering,
semantic-program admission and supersession, quiescence ordering, the
all-attempt disable tail, and failure-only operation deadlines.
ApplicationProcess assigns a separate body-child generation and correlates
it with the lifecycle generation and purpose without rewriting returned
results. BodyMotionProgramProcess privately owns the pose order, speed,
holds, repetition, and presentation classification for every runtime,
quiescence, and calibration program. StackChanBodyMotion owns each complete
SCS motion operation below that catalog. It reads and synchronizes the current
two-axis pose while torque is disabled, interpolates bounded commands, reads
both final live goal-position registers back exactly, tracks yaw and pitch
independently, and returns one typed terminal result. Callers do not
reconstruct servo behavior from raw samples or supply arbitrary poses.
Enable initializes the target while the servo rail remains disabled and
confirms the rail-enable output command. It then retries non-actuating bus
evidence at a bounded cadence until both powered servos acknowledge
torque-disabled, or the activation deadline establishes failure.
StackChanBodyMotion first reads each servo's installed minimum/maximum
position limits and clockwise/counter-clockwise dead zones while torque
remains disabled. It requires a bounded ordered range, dead zones inside the
documented 0..32 domain, and a calibration home whose complete supported
motion envelope fits the installed ranges. It then reads the current
raw pose, requires that pose inside the corresponding installed range, writes
it to both live SCS goal-position
registers, and requires exact readback. Every later raw command must also stay
inside the installed range. The adapter only reads this EEPROM configuration;
it never rewrites or clamps it. Only compatible configuration, pose, and goal
evidence together synchronizes the trajectory and permits torque enable; a
software-only target assignment cannot do so.
The interval between rail enable and torque-disable confirmation is
unconfirmed. FEETECH specifies torque-disabled as the power-on SRAM value, but
software still requires matching live register evidence and does not turn
elapsed time into a no-motion claim. After the final command, the motion owner
reports ControllerSettled only when both live goals match the requested raw
targets and both axes provide two fresh, valid, stable samples, taken no more
often than every 100 milliseconds, with moving = 0. The result retains the
actual position, signed residual, direction, and whether directed progress was
observed, but it does not claim exact physical reach. Residual size and elapsed
time are not settlement or failure oracles. Repeated stationary pitch evidence
with the independently bounded current/load condition becomes LoadLimited;
a known non-zero device status becomes DeviceFault. Logical-angle conversion
is diagnostic presentation, not raw classification. Invalid
evidence fails immediately, and unavailable goal or feedback observations
consume an operation-local budget that intervening valid observations do not
erase.
The complete activation deadline is six seconds; it bounds bus readiness, goal-register acknowledgement, the neutral trajectory, rate-limited evidence, and stable-sample margin. Unavailable pre-torque evidence remains pending within that bound. Elapsed time never establishes success. Any activation failure enters the same all-attempt safety tail as quiescence.
Quiescence attempts every safety action even if an earlier one fails. The
application dispatches LED darkness and sound stop through their independent
owners, while BodyActuationProcess cancels body presentation, commands the
rest pose, disables torque, and disables servo power. Rest motion is bounded by a seven-second
failure deadline and uses the same final-goal and per-axis terminal evidence.
Every terminal quiescence path still attempts torque and servo-power disable
before reporting success or fault. Its typed result states whether both
disable calls completed, not whether electrical disable was independently
read back. A body fault retains its motion stage, implicated axis, configured
zero, installed position limits and directional dead zones, requested and
observed goals, last positions, moving bits, current, load, and device status.
An interactive expression-pose failure reports
body_presentation_operation_failed; activation and quiescence failures retain
their separate generated meanings.
The local fault frame projects the zero, position-configuration evidence,
range, directional dead zones, goal evidence, last position, load, and status;
it does not add them to the device protocol or logs.
Expression proceeds directly to eligible first-fault presentation when the disable-command tail completed; it requests fault quiescence only while that tail remains incomplete. A newer quiescence command that supersedes activation before neutral motion has begun skips rest actuation and runs the disable tail directly.
One DisplayOutputProcess owns the display from the pre-M5.begin dark gate
through the selected calibration or ordinary runtime. It performs board and
panel detection, presenter initialization, calibration, scene and fault-frame
transfer, wake/frame/reveal ordering, suspension, explicit darkness, and the
common failure safety tail. Calibration and scene code supply content only.
The first visible frame requests panel wake, transfers one complete frame, and
then requires confirmed backlight enable. A normal active command reports
ActiveFrameTransferCompleted only after that boundary. Suspension transfers
the current suspend projection, requests panel sleep, requires confirmed
backlight disable, and reports DisplayDarkRailConfirmed. Initialization or
frame failure preserves its first cause while attempting the same panel-sleep
and rail-disable tail. A newer generation explicitly supersedes unfinished
presentation; stale commands and updates cannot regain visibility. The 120 ms
panel wake delay is a minimum hardware precondition, not successful wake or
visibility evidence. DisplayOutputProcess cannot advance expression state
directly. The display result queue is non-droppable; render updates use a
one-entry latest-value queue.
ApplicationProcess owns the bounded long-hold composition. It blocks new
application input, sends device_stopping, asks ExpressionLifecycle to
quiesce body output and suspend the display, and waits no longer than 9.25
seconds. When a connected bridge admitted the stopping status, graceful PMIC
power-off also requires the exact current stopping acknowledgement owned by
DeviceConnection; enqueueing USB bytes is not receipt evidence. Safety and
acknowledgement may arrive in either order. ExpressionLifecycle adopts an
ordinary-sleep or fault quiescence and an in-flight display suspension instead
of issuing the same owner operation a second time. It emits graceful shutdown
only after body output no longer requires disable and the display has confirmed
its dark rail. The overall bound is derived from the seven-second body-
quiescence bound, the two-second display-operation bound, and a 250-millisecond
scheduler margin; it is failure evidence only.
A matching ShutdownSuspended event is the graceful path. A child fault or
deadline preserves the first fault and reaches the same fail-safe tail. The
application marks the PMIC request attempted before calling the narrow
BoardPowerTarget, and the CoreS3 target itself rejects a second call. A true
return means only that the PMIC register operation was accepted; it does not
prove electrical power removal. Failure to deliver the best-effort stopping
status cannot block child safety; it removes the receipt precondition because
no acknowledgement can follow. A child fault or the aggregate deadline may
enter the PMIC fail-safe tail without communication success. Shutdown has no
touch or button wake transition; ordinary sleep remains a separate expression
operation.
The CoreS3 target verifies and configures the AXP2101 long-hold fallback register at boot. The firmware PMIC request uses the same documented power-off register operation as the pinned M5Unified implementation and reports only whether the I2C register operation was accepted. Physical rail removal, including behavior with USB power present, is a hardware-acceptance question and is not inferred from that software return.
Firmware boots with the servo rail disabled and the display dark. The package
must load, the display must return a complete active-frame result, and the body
actuation owner must return OutputsReady before the lifecycle becomes
interactive and admits runtime body programs, body touch initialization, or
permission input. LedOutput and SoundOutput are independent auxiliary
hardware writers at this stage. They keep only local animation or playback
state and cannot mutate a servo or lifecycle result. A display or body fault
asks the affected owned outputs to return to their safety boundary.
LedOutput alone initializes the Stack-chan RGB target, writes an
all-LED frame, schedules connected-state breathing and outcome duration,
cancels a transient, and writes the black frame used for sleep or shutdown.
The shared hardware adapter exposes that RGB boundary separately from its
servo target; failed LED register or refresh work remains an LED failure.
SoundOutput alone configures speaker volume, selects expression and
outcome cues, orders tones and intentional gaps, cancels playback, and waits
for the target's stopped observation. A tone duration or stop deadline can
schedule work or establish failure, but cannot fabricate cue completion.
Agent state selects bounded non-blocking servo, LED, and sound behavior.
Expression targets are bounded before admission, then rejected if their raw
positions fall outside the installed servo limits. Working and needs-input
count as busy, refresh the inactivity origin, and suppress ordinary sleep.
Waiting does not. It rests the body on the same neutral program the device uses
when no episode is owned, holds its manifest caption, keeps the calm idle LED,
plays no cue, and allows ordinary inactivity sleep, because a retained episode is
not work.
The current connected neutral pose is yaw zero and pitch 300 servo-angle units
relative to the stored calibration home; connected motion cues derive from
that neutral. The working cue holds pitch at the slightly lower 280 units and
uses three logical poses: yaw +80, yaw -80, and center. Once center reports
ControllerSettled, the same simple sweep repeats immediately; there is no
separate recovery pose or up/down oscillation between repetitions. A
completed or failed turn requests the neutral pose;
its face, LED, and sound layers retain the outcome distinction without an
additional high head pose. BodyMotionProgramProcess owns the exact cue data
and controller-settlement-gated holds, while BodyActuationProcess alone
admits each pose and consumes its typed terminal result. Elapsed cue time
cannot advance a phase
before the current SCS motion reports ControllerSettled.
An agent-state change on the same confirmed connection first supersedes the
old pose with neutral; the next-cue delay controls presentation cadence only.
The ordinary rest pose remains yaw zero and pitch 80.
Idle inactivity requests suspension after 30 seconds on battery or 180
seconds with USB power. Those durations request an operation; they do not
prove it completed.
Only a confirmed Connected bridge drives connected motion, LED, and sound
expression. Synchronizing, Uncertain, no-bridge, and fault states are
non-connected expression states. Leaving confirmed connection projects the
non-connected LED state, requests sound stop through SoundOutput, and
admits the newer catalogued disconnected body program, which supersedes an
unfinished connected program without copying its generation. A safety failure
runs the complete quiescence tail.
A short side-button click while interactive requests ordinary suspension when the host replica is not busy. The same click during an ordinary suspend transition requests wake. A touchscreen press while non-interactive may request wake; that press is consumed and cannot also answer a permission. Host connection or accepted host activity may also request wake. Body-touch telemetry cannot wake the panel or reset inactivity.
Connected suspension renders the manifest-owned hero and sleep effect for the suspend projection. Disconnected suspension retains the applicable bridge or USB-power prompt and suppresses hero, drones, decision controls, arrival, and sleep effect. A panel is considered active or suspended only from the matching display result, not from a frame count or wall-clock delay.
The immutable render input keeps producer, host connection, agent state, permission request, and USB-power observation separate. With a synchronized host it renders producer-ready or active work. Without one, the scene manifest selects the bridge-start or USB-connect prompt. Exact text, anchors, background loops, drone slots, and permission rectangles belong to the scene contract and tracked scene manifest, not C++ fallback constants.
Working renders a center caption from the tracked manifest: the shared
WORKING label, or, when the render input carries a bound usage sample, the
manifest usage labels with the reported percentages joined as
CTX 83% 5H 96% 7D 96%. The renderer owns the 0–9 and % glyphs and the
assembly; the labels and their text stay in the manifest, and the contract
bounds the worst-case caption so it always fits the existing caption slot. The glyph table only
decides what can be drawn; each manifest field owns its own alphabet — static
text and control labels accept A–Z and space, usage labels add 0–9, and
the generated caption adds space and % — so a new glyph cannot widen an
unrelated field, and the native parser and Python builder accept and reject the
same per-field alphabets. A snapshot that changes only usage is a display-only
update that refreshes the caption without waking the body, servos, LED, or sound.
USB power is present when USB Serial/JTAG reports a host attachment or PMIC VBUS is between 4000 and 6000 millivolts. Battery charging status is not used as cable evidence. Power sampling changes prompt selection and inactivity request duration; it does not own connection, expression, or board-shutdown state.
The touchscreen classifier emits approve or reject only while expression is
interactive and DeviceConnection has one answerable request. Body-touch
zones and swipes emit telemetry only. A successful permission send suppresses
replay of that exact request; a failed owner send leaves that request
answerable.
The generated LittleFS scene package is ignored and absent from a fresh clone. Set up spritekit as described in its setup section, then generate the package from the repository root:
tools/spritekit/venv/bin/python tools/spritekit/scene_builder.py \
assets/scenes/expedition/scene.jsonThis produces firmware/stackchan/data/scene/scene.json and its .idx4
resources, and refreshes the tracked native parser vector at
firmware/stackchan/test/test_scene_contract/scene_package_test_vector.generated.h.
--check validates inputs and requires that vector to be current, but does not
create installation files.
From firmware/stackchan:
pio test -e native
pio run -e cores3-stackchan
pio run -e cores3-stackchan -t buildfsInstall the generated filesystem before the matching firmware:
pio run -e cores3-stackchan -t uploadfs --upload-port <serial-port>
pio run -e cores3-stackchan -t upload --upload-port <serial-port>Each upload must complete its device-side hash check. The operator selects one unoccupied serial port; the tracked configuration has no machine-specific default.
Native checks exercise only target-independent contracts and owner composition. The CoreS3 build proves target compilation. Neither establishes physical display, touch, servo, sound, LED, PMIC, power, transport, or cadence behavior; those require the identified hardware checks in VERIFICATION.md.
Generated PlatformIO output and generated LittleFS data remain untracked. The generated native parser vector is tracked source evidence and is refreshed only by the scene builder.