Skip to content

Latest commit

 

History

History
439 lines (384 loc) · 25.1 KB

File metadata and controls

439 lines (384 loc) · 25.1 KB

Stack-chan firmware

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.md and PRODUCT.md avoid these terms entirely.

Current target and dependencies

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.

Body calibration and boot selection

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.

Runtime ownership

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.

Physical behavior

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 09 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 AZ and space, usage labels add 09, 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.

Build and verification commands

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.json

This 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 buildfs

Install 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.