Pet architecture for v0.5.0

Document versionDateSummary
v12026-08-05Initial architecture for the board runtime and embedded asset pipeline release
v22026-08-05Accept the architecture for the board runtime and embedded asset pipeline release
v32026-08-06Record ADR-015 and the deferred single-source manifest grammar
v42026-08-06Record the diagnostic contract, the device evidence model, and ADR-016
v52026-08-07Record ADR-019 and the embedded asset pack contract
v62026-08-18Record ADR-020 and the pack frame placement rule

Status: Accepted

Product version: v0.5.0

Milestone: V0 software companion foundation

Release: Board runtime and embedded asset pipeline

General architecture: Pet architecture

Template: arc42

Versioning: Semantic Versioning 2.0.0

1. Introduction and goals

1.1 Purpose

Version v0.5.0 puts Pet on a physical board and keeps it there.

Every embedded claim the project holds today is a build claim. v0.3.0 proved that the Core and the frontend-neutral components enter an ESP-IDF build, and v0.4.0 proved that the storage contract compiles for the target without an implementation. No board has been flashed, nothing has been drawn on a panel, no button has been read, and no asset has ever reached a device. LIM-012 and LIM-013 record exactly that.

This release closes the distance between a linked firmware image and a running companion. The firmware application stops being a bounded one-shot lifecycle and becomes a continuous time-driven run. A TFT frontend presents the pet on the classic LILYGO T-Display panel. A build-time converter turns the canonical PNG and manifest sources into one embedded asset pack, and an embedded reader resolves frames out of it without copying them. Both board buttons are sampled and filtered, and at least one of them produces an existing semantic action.

flowchart LR
    PNG[canonical PNG and manifest]
    CONV[build-time converter]
    PACK[embedded asset pack]
    IMG[firmware image]
    FW[firmware app]
    TFT[TFT frontend]
    PANEL[ST7789 panel]
    BTN[board buttons]
    CORE[Pet Core]

    PNG --> CONV
    CONV --> PACK
    PACK --> IMG
    FW --> CORE
    FW --> TFT
    TFT --> PACK
    TFT --> PANEL
    BTN --> TFT
    TFT --> FW

The panel comes before the pipeline. The pack decides its pixel encoding after the colour path has been verified on a real panel, because an artefact designed for a display nobody has lit is a guess with a file format.

1.2 Architecture goals

The release must prove that:

  • the firmware application runs continuously and its normal run does not return
  • the elapsed time reaching the Core comes from a monotonic platform clock and is bounded
  • a frontend can present the pet on a panel that has no filesystem, no window system, and no PNG decoder
  • the embedded runtime consumes a generated artefact rather than the canonical sources
  • the pack carries asset facts only, and every board-specific fact stays in the board profile
  • physical buttons become semantic actions without becoming an adapter interface
  • the common rendering and input-filtering decisions are host-testable, so the gate covers them
  • ESP-IDF never enters a hosted composition
  • device claims are recorded as maintainer observation and never as automated evidence

1.3 Included capabilities

  • a firmware application with a continuous non-returning run loop and a bounded elapsed delta
  • frontends/tft, a frontend whose common sources build and test on the development host
  • ESP-IDF display and GPIO backends inside that frontend, selected only by the embedded build
  • a registered firmware-tft composition that builds and tests the common sources on the host
  • a measured classic board and panel profile under boards/t_display
  • a deterministic build-time converter from the canonical sources to one embedded asset pack
  • an embedded pack reader in components/assets, selected out of the desktop build
  • the pack linked into the firmware image as embedded read-only bytes
  • both board buttons sampled through a pure debounce rule, with one mapped to an existing action
  • a contractual key-value serial diagnostic line format with a documented event vocabulary
  • a project command path for flashing the firmware and reading its serial output
  • continued headless and desktop SDL verification

1.4 Excluded capabilities

  • non-volatile storage on the device, including NVS, a flash layout, save timing, and wear policy
  • Wi-Fi, Bluetooth, over-the-air updates, networking, and synchronisation
  • production power management and battery behaviour
  • touch input
  • physical sensor adapters and digital activity adapters
  • any T-Display variant other than the classic one, and any other embedded target
  • a new Core action invented only to occupy the second board button
  • a separate asset partition and the flashing lifecycle that comes with it
  • moving the desktop frontend onto the pack format
  • automatic session continuity on any host
  • identity, traits, memories, maturity, and formative events
  • an automated hardware-in-the-loop verification gate
  • Windows build and runtime verification

Excluded capabilities must not leave placeholder dependencies inside the Core.

1.5 Stakeholders

Developer

Needs the rendering, packing, and debounce decisions to be assertable on the development host, so that a device is required to confirm a result rather than to discover a mistake.

Maintainer

Needs one documented command path to flash the board and read its output, and one documented set of observations that make a device run count as evidence.

Future adapter developer

Depends on board buttons being modelled as direct user input, so that the adapter interface question stays open for the release that has a real external source.

Future hardware developer

Depends on the board profile carrying measured panel facts and on the pack carrying none of them, so that a second panel changes a profile rather than a file format.

Product reviewer

Uses this release as the first evidence that the architecture survives a real device, without believing that the device keeps a pet across a power cycle.

2. Architecture constraints

2.1 Product constraints

  • the release belongs to the V0 milestone
  • one world contains one active pet
  • the pet presents through derived presentation state and never through authoritative state
  • a physical button expresses direct user intent rather than an external observation
  • a missing or rejected asset set produces a calm fallback rather than an error prompt
  • the device keeps no pet across a power cycle in this release

2.2 Technical constraints

  • Pet Core remains portable ISO C99 and gains nothing from this release
  • no ESP-IDF, board, panel, or GPIO dependency enters the Core or any hosted composition
  • the embedded runtime parses no text manifest and decodes no PNG
  • the pack is generated into the build directory and is never committed as a source
  • the pack carries no board offset, no rotation, and no panel colour order
  • the frontend performs no dynamic allocation at run time and uses fixed-capacity storage
  • the classic variant is the only supported board, and its target is esp32
  • desktop SDL and headless builds remain supported compositions

2.3 Documentation and language constraints

  • code and developer documentation use British English
  • C domain identifiers use the pet prefix
  • version architecture documents follow the policy in arch README
  • the pack format, the frame buffer strategy, the run loop policy, and the hosted verification composition each receive an ADR when their epic commits to them
  • code that names an unverifiable platform path records it as a TODO comment carrying the technical reason

2.4 Verification constraints

  • Linux desktop GCC and Clang verification continues to be required
  • the common TFT sources and the firmware orchestration boundary are covered by hosted tests
  • the converter is covered by host tests over known inputs and known outputs
  • device behaviour is recorded as maintainer observation and is never presented as automated
  • a serial transcript is evidence only when it follows the documented line format
  • Windows evidence is not produced by this release and no Windows claim changes

3. System scope and context

3.1 Product context

A user meets the pet on a device for the first time. The board is powered, the panel shows the pet, and a button press produces a visible response. Nothing is installed, nothing is configured, and nothing is saved.

flowchart TD
    U[User]
    B[Classic T-Display board]
    P[ST7789 panel]
    K[Board buttons]
    M[Maintainer]
    S[Serial output]

    U --> K
    B --> P
    K --> B
    M --> S
    B --> S

The desktop and headless applications are unchanged. They remain the development and diagnostic surfaces, and the board becomes the first surface a user could actually hold.

3.2 Technical context

The firmware application owns the lifecycle. It reads a monotonic clock, bounds the delta, drives the Core, requests a snapshot, collects filtered button events, turns them into semantic actions, and asks the frontend to present. It owns no drawing and no pixel.

The TFT frontend owns presentation. It maps a snapshot to a presentation role, resolves that role to a pack frame, composes a frame buffer, and hands that buffer to its platform backend. It owns no product rule and no authoritative state.

The board profile owns the physical facts. Panel geometry, panel offsets, native orientation, transfer orientation, colour order, and pin assignments are board facts, and they are measured rather than assumed.

The pack owns the asset facts. Frame geometry, pixel encoding, transparency, and the entry table are asset facts, and they are the same on every board that ever reads the pack.

3.3 External interfaces

The release interfaces are the public Core API as it already stands, the TFT frontend contract consumed by the firmware application, the pack format consumed by the embedded reader and produced by the converter, the board profile consumed by the ESP-IDF backends, and the serial diagnostic line format consumed by a maintainer and by the release evidence.

No network, cloud, account, sensor, or device transfer interface is present.

4. Solution strategy

4.1 Functional core and imperative shell

Everything that decides is pure and everything that touches the world is thin.

flowchart LR
    SNAP[PetSnapshot]
    ROLE[presentation role]
    SEL[pack frame selection]
    BUF[frame buffer]
    BACK[esp_lcd backend]
    PANEL[panel]

    RAW[raw GPIO level]
    FILT[debounce rule]
    EVT[button edge]
    ACT[PetAction]

    SNAP --> ROLE
    ROLE --> SEL
    SEL --> BUF
    BUF --> BACK
    BACK --> PANEL

    RAW --> FILT
    FILT --> EVT
    EVT --> ACT

The pure half is a function of its inputs, so the host gate can assert it. The thin half is an effect over a vendor SDK, so only a device can confirm it. The release is designed so that the second half is as small as it can be without hiding a decision inside it.

4.2 Main strategies

  1. keep the panel first, so the pixel encoding is chosen against a measured display
  2. keep board facts in the board profile and asset facts in the pack, with no overlap
  3. generate the pack at build time and never commit it, so the canonical sources stay canonical
  4. link the pack into the image, so no partition, no filesystem, and no location policy is needed
  5. keep the run loop non-returning and its delta bounded, so a slow frame cannot become a time jump
  6. register the firmware and TFT composition, so the hosted gate covers what the device cannot assert
  7. keep ESP-IDF inside frontend-local backends selected by a build option, as the desktop reader already is
  8. treat a button as direct user input, so no adapter interface is opened by accident
  9. keep the fallback presentation reachable at every stage, so a failed pack degrades calmly
  10. record what only a maintainer can observe, and never present it as an automated result

4.3 Technology strategy

The firmware runs on ESP-IDF v6.0.2 for esp32, which the project already uses. Panel access goes through the esp_lcd component of that SDK rather than through a project-owned SPI driver, because the SDK already owns bus setup, transfer queueing, and DMA. Button access goes through the plain GPIO API, because a vendor button library would add a dependency for a rule the project wants to own and test.

The converter is a project-owned Python program using the standard library only. Python is already a build and gate dependency, and the canonical sources are a small, fixed subset of PNG, so a decoder limited to that subset is smaller than any dependency that would decode all of PNG. Anything outside the subset is rejected with a named reason rather than decoded by guesswork.

No new C dependency is introduced. No serialisation library, no image library, and no graphics library enters the firmware.

5. Building block view

5.1 Level 1 building blocks

flowchart TD
    PLAT[platforms/esp_idf]
    APP[apps/firmware]
    TFT[frontends/tft common]
    BACK[frontends/tft esp_idf backends]
    ASSETS[components/assets]
    PRES[components/presentation]
    LAY[components/layout]
    BOARD[boards/t_display]
    CORE[Pet Core]
    PACK[generated pack]

    PLAT --> APP
    PLAT --> PACK
    APP --> CORE
    APP --> TFT
    TFT --> PRES
    TFT --> LAY
    TFT --> ASSETS
    TFT --> BACK
    BACK --> BOARD
    ASSETS --> PACK

The Core is unchanged by this release. No public Core operation is added, no Core header gains a field, and the allocation probe surface stays as it is.

5.2 Firmware application

apps/firmware becomes a real host rather than a bounded probe. It owns the run loop, the time policy, the semantic mapping of filtered button events, the snapshot orchestration, and the serial diagnostics.

Its responsibilities are the host responsibilities the general architecture already names. It creates the world, obtains time from the platform, collects frontend input, delivers validated actions to the Core, requests presentation state, and invokes the frontend.

The existing pet_firmware_run one-shot entry point is not deleted for its own sake. The lifecycle gains a stepped form so that a host test can drive a fixed number of iterations with supplied time values, while the device entry point runs the same step function forever.

5.3 TFT frontend, common sources

frontends/tft owns presentation for a small fixed panel. Its common sources hold the mapping from a snapshot to a presentation role, the resolution of that role to a pack entry, the composition of a frame buffer, clipping and integer scaling through components/layout, the procedural fallback appearance, and the pure button debounce rule.

The common sources include no ESP-IDF header. They read a board profile through plain constants and write into a caller-owned frame buffer, which is what makes them testable on the development host.

The debounce rule is a pure state transition. Given a raw level, a monotonic timestamp, and the previous state, it produces the next state and an optional edge. It lives beside the frontend that consumes it rather than in components/, because no second consumer exists and an interface without a second client is a prediction rather than a boundary.

5.4 TFT frontend, ESP-IDF backends

The display backend initialises the SPI bus, the esp_lcd panel handle, the reset sequence, the backlight, the orientation, and the colour order, and it flushes a composed frame buffer to the panel. It translates panel errors into frontend status values and never decides what to draw.

The GPIO backend configures both button pins, samples their levels, and supplies the monotonic timestamp the debounce rule needs. It holds no debounce state of its own.

Both backends are selected by a build option and are absent from every hosted composition, following the pattern that components/assets/src/desktop/reader_stdio.c established under PET_ASSETS_STDIO_READER and that ADR-010 records.

5.5 Embedded asset pack reader

components/assets gains the pack format and a reader over it. The reader validates the header, the entry table, and the declared lengths before any lookup, and it then answers a lookup with a pointer into the pack and a length. No pixel is copied, because the pack is read-only memory the firmware already holds and the composition reads it in place.

The reader also materialises the presentation structure the pack carries into a caller-owned PetManifest, with image indices in place of paths. That is what lets the device reuse pet_presentation_select_frame unchanged, so role selection, animation timing, and the fallback chain behave identically on the desktop and on the board. The device therefore runs the same selection code as the desktop and differs only in where the pixels come from.

The reader is frontend-neutral, so it belongs to the component that already owns asset policy. It is selected out of the desktop build, exactly as the desktop reader is selected out of the embedded build. The existing manifest-based path is untouched, so the desktop keeps loading canonical PNG files through pet_assets_load.

5.6 Board profile

boards/t_display/variants/classic gains the measured facts the panel work produces. Today the header carries vendor-documented pins and geometry with a comment saying that they are unverified. This release replaces that comment with measured values for the visible area, the panel offsets, the native orientation, the transfer orientation, and the panel colour order.

A value that the release measures is recorded as measured. A value that stays vendor-documented keeps saying so, because a header that presents both alike would make the next board’s bring-up start from a false baseline.

A separate study probe outside this repository has already driven this panel through esp_lcd with a swapped axis, a mirrored x axis, a gap of 40 by 52, an inverted colour path, and an RGB element order at 40 MHz. Those values are a starting point for bring-up rather than evidence for this project, so E31 reproduces them here and records what it observed rather than importing them as facts.

5.7 Converter and pack generation

tools/assets/pack.py reads the canonical manifest and the canonical PNG files, decodes the supported subset, converts each image to the pack pixel encoding, and writes one pack file into the build directory. It is deterministic, so equal inputs produce equal bytes, and it fails loudly rather than emitting a partial pack.

The ESP-IDF build runs the converter as a build step and embeds its output. The hosted build runs it too, because the pack reader tests need a real pack rather than a hand-written fixture.

The converter is the second program to read the canonical manifest grammar, after the C parser in components/manifest, and it cannot reuse that parser without putting a host build inside a cross build. ADR-015 decides that the grammar eventually has one reader and that the reader is this converter. This release does not make that change, because the components/assets contract work it requires is a second structural change to a component E35 is already opening.

What this release does carry is the requirement that makes the later collapse cheap. The converter’s grammar reading is an isolated module with its own tests, separate from pack encoding, so the change moves a module rather than rewriting a program. PBI-118 owns that requirement and opens the limitation for the duplication when it lands.

5.8 Proposed source layout

frontends/
    tft/
        CMakeLists.txt
        README.md
        include/
            pet_frontend_tft/
                frontend.h
        src/
            frontend.c
            surface.c
            render.c
            fallback.c
            button_filter.c
            esp_idf/
                display_backend.c
                input_backend.c
        tests/
            test_runner.c
            test_suites.h
            test_surface.c
            test_render.c
            test_fallback.c
            test_button_filter.c
 
apps/
    firmware/
        include/
            pet_app_firmware/
                app.h
                input.h
                diagnostics.h
        src/
            app.c
            input.c
            diagnostics.c
        tests/
            test_runner.c
            test_suites.h
            test_lifecycle.c
            test_input.c
            test_diagnostics.c
 
components/
    assets/
        include/
            pet_assets/
                pack.h
        src/
            pack.c
        tests/
            test_pet_assets_pack.c
 
boards/
    t_display/
        variants/
            classic/
                include/
                    pet_board_t_display/
                        variants/
                            classic.h
 
tools/
    assets/
        pack.py
    tests/
        test_pack.py
 
platforms/
    esp_idf/
        main/
            app_main.c
            CMakeLists.txt

Files are added by implementation PBIs only when they have real responsibility. The layout records ownership rather than a requirement to create empty placeholders.

5.9 Dependency rules

flowchart TD
    PLAT[platforms/esp_idf]
    APP[apps/firmware]
    TFTC[frontends/tft common]
    TFTB[frontends/tft esp_idf backends]
    ASSETS[components/assets]
    BOARD[boards/t_display]
    IDF[ESP-IDF]
    API[Public Pet API]
    CORE[Pet Core]

    PLAT --> APP
    PLAT --> TFTB
    APP --> API
    APP --> TFTC
    TFTC --> ASSETS
    TFTB --> TFTC
    TFTB --> IDF
    TFTB --> BOARD
    API --> CORE
  1. the Core depends on no app, frontend, component, board, or platform target
  2. the common TFT sources depend on components and on plain board constants, never on ESP-IDF
  3. the ESP-IDF backends depend on the SDK and on the board profile, and nothing depends on them
  4. components/assets depends on no frontend and no board
  5. the firmware app depends on the public Core API and on the TFT frontend contract
  6. no component, app, or frontend reimplements the pack format
  7. the pack is a build output, so no source directory contains one

5.10 Composition model

The app and frontend composition model is unchanged in shape and gains one app and one frontend. config/compositions.json declares the app firmware with target pet_firmware_app and the frontend tft with target pet_frontend_tft, which tools/compositions.py resolves to the composition name firmware-tft.

The composition name is derived by composition_name as app-frontend, so it is not a free-form label and no suffix can be added to it without inventing an app or a frontend that does not exist. The hosted composition and the embedded build are the same app and the same frontend, and the only difference between them is whether the ESP-IDF backends are selected. A name that suggested two different compositions would misdescribe that.

The claim the hosted composition supports is therefore recorded where claims are checked rather than in the identifier. The support matrix in section 7.2 and the release evidence both state that firmware-tft is hosted build and test evidence for the firmware orchestration boundary and the common TFT sources, and that it is not a device runtime claim.

The alternative, configuring frontends/tft unconditionally in every composition the way components/ is configured, was rejected. The root CMakeLists.txt deliberately configures only the selected composition’s frontend, and a frontend that ignored that rule would be a second kind of frontend with no registry entry and no selection.

ADR-016 records the decision, its alternatives, and the tooling it moves.

6. Runtime view

6.1 Boot

sequenceDiagram
    participant Plat as app_main
    participant App as Firmware app
    participant TFT as TFT frontend
    participant Back as ESP-IDF backends
    participant Core as Pet Core

    Plat->>Back: bring up the panel and the buttons
    Back-->>Plat: panel and frame buffer, or panel error
    Plat->>TFT: create on the frame buffer the backend owns
    TFT-->>Plat: assets loaded or fallback selected
    Plat->>App: start with the world input, the seams, and the frontend
    App->>Core: pet_world_init
    App->>App: emit boot and readiness diagnostics
    App->>App: enter the run loop

The platform entry drives the bring-up rather than the frontend, because nothing may depend on a backend and the common frontend sources therefore cannot call one. The backend owns the frame buffer, which it allocates DMA-capable once at open, and the frontend takes it as caller-owned storage.

A panel that cannot be brought up is a terminal condition for presentation, and the application says so through a diagnostic rather than by resetting. A pack that cannot be accepted is not terminal, because the procedural fallback needs no pack.

6.2 One iteration of the run loop

sequenceDiagram
    participant App as Firmware app
    participant Clock as Monotonic clock
    participant Back as Input backend
    participant TFT as TFT frontend
    participant Core as Pet Core

    App->>Clock: read the monotonic value
    App->>App: derive and bound the elapsed delta
    App->>Back: sample both buttons
    Back-->>TFT: raw levels and timestamps
    TFT-->>App: filtered button edges
    App->>Core: pet_world_action for a mapped edge
    App->>Core: pet_world_update with the bounded delta
    App->>Core: pet_world_snapshot
    App->>TFT: present the snapshot
    TFT->>TFT: role, pack lookup, compose the frame buffer
    TFT-->>App: composed, or already on the panel
    App->>Back: transfer the frame buffer when it changed
    App->>App: yield until the next iteration

The loop does not return, does not busy spin, and never hands the Core a delta larger than the bound. A delta that arrives larger than the bound is clamped, and the loop records that it clamped rather than silently losing the difference.

6.3 Button to action

flowchart TD
    L[raw level and monotonic timestamp]
    D{stable for the debounce interval}
    S[update the filter state]
    E{edge produced}
    M{mapped to an action}
    A[deliver the semantic action to the Core]
    N[no action]

    L --> D
    D -- no --> S
    D -- yes --> E
    E -- no --> S
    E -- yes --> M
    M -- no --> N
    M -- yes --> A

The filter is pure, so a host test drives it with a level sequence and a clock sequence and asserts the edges. The board supplies levels and timestamps and asserts nothing.

A button number never reaches the Core. The mapping from an edge to a semantic action lives in the firmware application, following the precedent that apps/desktop/src/input.c set for the desktop composition.

6.4 Asset resolution and fallback

flowchart TD
    P[embedded pack bytes]
    H{header and entry table valid}
    R[presentation role]
    L{entry found for the selected frame}
    G{geometry fits the surface}
    B[compose from the pack frame]
    F[compose the procedural fallback]

    P --> H
    H -- no --> F
    H -- yes --> R
    R --> L
    L -- no --> F
    L -- yes --> G
    G -- no --> F
    G -- yes --> B

The fallback is reachable from every stage, so the application presents something calm whatever the pack turns out to be. Reaching the fallback is a normal outcome for the frontend and a failed criterion for the release, and section 10.4 keeps those two statements apart.

6.5 Build-time pack generation

sequenceDiagram
    participant Build as Build system
    participant Conv as Converter
    participant Src as Canonical sources
    participant Out as Build directory
    participant Img as Firmware image

    Build->>Conv: run with the manifest path and the output path
    Conv->>Src: read the manifest and the referenced images
    Conv->>Conv: decode the supported subset and encode the pack
    Conv->>Out: write one pack file
    Build->>Img: embed the pack as read-only bytes

The converter is a build step rather than a manual one, so a changed sprite cannot reach a device through a stale pack that somebody forgot to regenerate.

7. Deployment view

7.1 Logical outputs

Pet Core library, unchanged
Shared component libraries, with the asset pack reader added
TFT frontend library, common sources
TFT frontend ESP-IDF backends, embedded build only
Firmware application library and its host tests
Generated embedded asset pack, a build output
ESP-IDF firmware image containing the pack
Headless diagnostic executable
SDL frontend library and desktop application executable
Test executables
Development runtime asset directory

The pack is a build output and is never committed. No firmware image, no pack, and no board artefact is checked into the repository.

7.2 Platform verification matrix

EnvironmentEvidence targetRequired for release
Linux x86-64 desktop, GCCBuild tested and runtime tested over headless, desktop-sdl, and firmware-tftYes
Linux x86-64 desktop, ClangBuild tested and runtime tested over the same three compositionsYes
firmware-tft hosted compositionBuild and test evidence for the firmware orchestration boundary and the common TFT sources, not a device claimYes
Converter and pack reader on the hostBuild tested and runtime tested over known inputs and rejected inputsYes
T-Display classic variant, ESP-IDF v6.0.2 for esp32Cross-compile tested, including the backends and the embedded packYes
Selected classic T-Display hardwareRuntime tested by maintainer observation, covering the panel, both buttons, and a two-hour continuous runYes
Any other T-Display variantNot selected, unverifiedNo
Any other ESP32 board or embedded targetDesigned, not implemented, unverifiedNo
MSVC on WindowsPortable by design, unverifiedNo
Clang and Ninja on WindowsPortable by design, unverifiedNo
Non-volatile storage on any targetNot implemented, unverifiedNo

Hosted build evidence, embedded cross-compile evidence, and physical device observation remain three separate claims. This release is the first one to hold the third, and it holds it for one variant on one target.

7.3 Build and gate interface

The CMake and Python tooling boundary gains the following:

  • firmware and tft entries in config/compositions.json, resolving to the firmware-tft composition
  • PET_FRONTEND_TFT_ESP_IDF as the backend build selection, defaulting off and set on only by the ESP-IDF build
  • PET_ASSETS_PACK_READER as the pack reader build selection, following PET_ASSETS_STDIO_READER
  • a pack generation target that runs tools/assets/pack.py and is depended on by the builds that need a pack
  • TFT frontend tests, firmware application tests, and pack reader tests registered with CTest
  • tools/tests/test_pack.py for the converter, joining the existing tool suites in the gate
  • ./pet.py flash and ./pet.py monitor, each taking a port, following the existing command-first grammar rather than introducing a nested one

Three existing pieces of tooling change as a direct consequence, and they are named here so that the implementing PBIs meet them by design rather than discover them in a failing gate.

gate.py adds the firmware and board include directories to the header self-containment check through firmware_includes, whose comment says that they deliberately sit outside hosted compositions. Once firmware-tft exists, that helper and its comment describe a situation that no longer holds and must be reconciled rather than left to drift.

config/verification.json decides what the embedded check reads. Its embedded_build.subjects named core_source, apps, boards, and components_embedded, and did not name frontends, because no frontend had ever entered the embedded build. The TFT backend is the first, so the subject set gained frontends_tft.

The narrowing that the backend needs is embedded_build.sdk_include_subjects, a declared list of sources that may be compiled with an include path outside the repository. The existing excluded_subjects could not express it, because that list means a source the embedded build must not compile at all. A subject on the SDK list still has to carry the required arguments and still fails on a forbidden dependency, so the rule stays enforced for every component, app, and common frontend source. The include boundary frontend-common-sources-have-no-esp-idf-or-display holds the same line in the source text, with the backend directory excluded from it.

Daily and maintainer commands otherwise stay as they are:

./pet.py build
./pet.py test
./pet.py verify
./gate.py audit --all-compositions

7.4 Deployment constraints

  • no daemon is installed and no network port is opened on any host
  • the device opens no radio, joins no network, and reaches no service
  • the firmware writes nothing to flash at run time
  • the pack ships inside the firmware image and is not flashed separately
  • the default ESP-IDF partition layout is unchanged
  • no board artefact is produced by a hosted build
  • production packaging remains deferred

8. Crosscutting concepts

8.1 Time

The device has a monotonic clock and no wall clock. The run loop reads the monotonic source through its platform backend, converts it to milliseconds, and derives a delta from the previous reading.

The delta is bounded. A single iteration never hands the Core more than the bound, whatever the platform reports, so a stall, a debugger pause, or a counter anomaly cannot become a time jump inside the world. The bound is a firmware policy rather than a Core rule, and it sits inside the one-day single-update bound the Core already enforces.

The bound is 1000 milliseconds against a 40 millisecond iteration, and a clamp is reported rather than dropped silently. ADR-017 records the policy, why the value was chosen, and the alternatives, and PBI-112 measures the real pace the value was chosen against.

The board supplies PET_WALL_CLOCK_UNKNOWN wherever an absolute time is asked for, which ADR-012 already supports. Since this release adds no device persistence, no absence is ever computed on the device, and LIM-014 is unaffected.

8.2 Randomness

Randomness is unchanged and remains explicit and caller-supplied. The firmware supplies a source in the same way the hosted applications do. No hardware entropy facility is introduced, because nothing in this release needs one.

8.3 Embedded asset pack format

The pack is a flat binary artefact carrying a magic value, a format version, the compiled presentation structure, an entry table, the frame payloads, and a trailing integrity check. Each entry names its image identity, its width and height, and the offset and length of its colour and mask payloads, while the pixel encoding and the transparency representation are declared once in the header. ADR-019 decides the format and the pack contract records it field by field.

The integrity check is a trailing CRC-32 over everything before it, which is the check, the polynomial, and the placement ADR-013 already fixed for the save payload. The project has one way of stating that a binary artefact is undamaged.

The compiled presentation structure is the manifest without its paths. The converter reads the canonical manifest.petasset, resolves its images, animations, roles, and fallback chain, and writes those records into the pack with image indices where the text carried paths. The canonical manifest therefore remains the single source of truth for what the pet looks like, and the pack is its compiled form for a runtime that has no parser, no filesystem, and no PNG decoder. Nothing about presentation is authored twice.

The pixel encoding is RGB565 in the byte order the format fixes and declares in its header. The order is the one an ST7789 consumes over SPI, so the classic backend transfers a composed buffer without touching a pixel. A future panel that disagrees converts in its own backend, because the declared order makes the disagreement visible rather than silent.

Transparency is carried as a one-bit mask beside the colour payload. The canonical sprites are 64 by 64 with an alpha channel, so a mask costs 512 bytes per frame and keeps the colour payload aligned and uniform. Pre-composing the sprites onto a background colour and reserving a chroma key were both considered, and the ADR records why they were not chosen. Pre-composition bakes a presentation decision into the artefact, and a chroma key removes a colour from the palette and produces fringes on the edges the canonical art already antialiases.

The pack carries no board offset, no rotation, and no panel colour order. Every board-specific fact belongs to the board profile, so a second panel changes a profile and reads the same pack.

8.4 Pack compatibility promise

The pack is generated by the same build that produces the firmware that reads it. The compatibility promise is therefore narrow and deliberate. A reader accepts only the format version it was built for, and it rejects every other version rather than interpreting it.

There is no migration, no second supported version, and no promise that a pack from one firmware image can be read by another. The format version exists so that a mismatch is detected and reported rather than misread, not so that a pack can outlive its image. This is a different promise from the save format in v0.4.0, where the artefact is user data that must outlive the process that wrote it.

8.5 Presentation and fallback policy

The frontend holds one full frame buffer for the panel and composes into it rather than streaming rows. At 135 by 240 in RGB565 a frame is 64800 bytes, which fits in internal DRAM on the classic board and stays DMA-capable, and it keeps one drawing discipline for the fallback path and the pack path instead of two. The buffer belongs to the caller, so a host test composes into an ordinary array and asserts the bytes. ADR-018 records the strategy, its memory cost, and the streaming and dirty-region alternatives.

One iteration composes the background, the pet, and any surrounding presentation into that single buffer and then transfers the whole buffer once. The pack is not copied into RAM to make this happen. Frame pixels are read out of the read-only pack while composing, so the only per-frame RAM cost is the frame buffer itself, which is allocated once at initialisation and reused for every iteration.

Composition covers the whole buffer rather than a changed region, and the transfer covers the whole panel rather than a rectangle. What the frontend does skip is the work itself. The frame selection is a deterministic function of animation time, so an iteration whose selection equals the one already on the panel composes nothing and transfers nothing, because the result would be the same bytes. The canonical animations declare frame durations between 300 and 1000 milliseconds, so the panel is fed a few times a second while the loop keeps sampling input at its own faster rate.

The transfer is the expensive half. At 40 MHz a full 64800-byte frame costs about 13 milliseconds on the bus, while composing 32400 pixels is memory-bound work of a few milliseconds. Partial updates through a sub-rectangle transfer are possible and are deliberately not taken in this release, because tracking a changed region would make the pure composition stateful in exchange for a saving the release has not shown it needs. The soak measurements decide whether a later release wants it.

The presentation role mapping is the existing one in components/presentation, so a snapshot the frontend cannot map still resolves to a calm role. Scaling and placement go through components/layout, and the two paths ask it for different rules. The procedural fallback fills the surface through pet_layout_fit, which is the rule the desktop uses. A pack frame is placed by pet_layout_fit_whole at the largest whole multiple of its own extents, so every sprite pixel covers the same square of panel pixels and the 64 by 64 canonical sprites land at 128 by 128 on this panel. A surface that cannot take one whole multiple reaches the fallback rather than a shrunk frame, which is the geometry arm of section 6.4. ADR-020 records the placement, the surround a transparent mask bit shows, and the alternatives.

The procedural fallback needs no pack, no manifest, and no image. It is the calm resting appearance that pet_assets_fallback already describes, drawn directly into the frame buffer as a letterbox, a canvas, and a body. The surround is one appearance for every role, while the body carries the role in its tone and its share of the canvas. A presentation that looked the same for every role would leave the skip rule with nothing to skip and would give a maintainer no way to see the world progressing before a pack exists.

8.6 Input

A board button is direct user input. It expresses intent in the same sense that a key press or a mouse click does, so it belongs to the frontend and host input path that the general architecture already describes.

It is not an adapter. An adapter translates an external physical or digital source, such as motion, ambient light, or a digital activity feed, and it carries source ownership, calibration, rate limiting, and validation concerns that a button does not have. Modelling a button as an adapter would open the adapter interface question that the roadmap deliberately keeps for a later release with a real source behind it.

PET_T_DISPLAY_BOARD_BUTTON_2_GPIO is GPIO 0, which is also the boot strapping pin on this board. The bring-up work records its behaviour at reset and during a run, and the release maps the safer pin to the semantic action if the strapping behaviour makes the other one unsuitable.

8.7 Serial diagnostics

Serial output is a contract rather than free text, because the release evidence quotes it.

A diagnostic line is one line of space-separated key-value pairs. It opens with the fixed prefix pet, carries event=<name> as its first pair, and carries the fields of that event after it. A key is lower case ASCII with words joined by a hyphen, and a value carries no space. A line that does not open with the prefix is not a diagnostic line, so bootloader, SDK, and panic output stays distinguishable from the project’s own output without reading it for meaning.

pet event=boot board=t-display-classic target=esp32 version=0.5.0
pet event=display-ready width=135 height=240 rotation=0 colour-order=bgr
pet event=display-failed reason=panel-init
pet event=pack-ready version=1 entries=5 bytes=44232
pet event=pack-rejected reason=version
pet event=runtime-ready tick-ms=40
pet event=input button=1 action=greet
pet event=time-clamped elapsed-ms=9120 delivered-ms=1000
pet event=heap free-bytes=142312 min-free-bytes=139880 uptime-s=7200
pet event=runtime-fault reason=panel-transfer

The event names below are the whole vocabulary for this release, and every one of them is contractual. An event is added by revising this section rather than by a firmware author choosing a name, and an existing name is never repurposed.

EventEmitted whenFields
bootthe application starts, before anything is initialisedboard, target, version
display-readythe panel has been brought upwidth, height, rotation, colour-order
display-failedthe panel could not be brought upreason
pack-readythe embedded pack has been acceptedversion, entries, bytes
pack-rejectedthe pack was refused before any lookupreason
runtime-readythe run loop is about to be enteredtick-ms
inputa filtered edge produced a semantic actionbutton, action
time-clampedan elapsed delta arrived above the bound and was clampedelapsed-ms, delivered-ms
heapa heap reading is taken, at least at the start and the end of a soakfree-bytes, min-free-bytes, uptime-s
runtime-faulta failure the run survived was reportedreason

Fields are additive. An event may gain a field in a later release, so a reader ignores a field it does not know rather than rejecting the line, and a field an event already carries is neither removed nor given a new meaning. A reason value is drawn from a small fixed set that the emitting PBI records, so a reason can be compared between runs rather than read as a sentence.

A line that carries no prefix and no event name is not part of this contract and is not release evidence. Free-form status text may exist during development, and it proves nothing at release time, because a claim quoted from prose cannot be checked against a vocabulary. The release quotes only lines that follow the format above.

A transcript proves that the firmware booted, that the expected board path was selected, that the pack was accepted or refused with its reason, that the loop stayed alive, that the heap readings were taken, and that a semantic action was produced. It proves nothing about what the panel physically showed, whether the colours were right, or whether the pet a user saw responded to the press that the input line records. Those stay maintainer observations, which section 10.6 defines.

8.8 Memory budget

The pack lives in flash as read-only bytes and is read in place, so its pixel payloads cost image space rather than RAM. The frame buffer is the largest RAM consumer at 64800 bytes and is allocated once, at initialisation, from the storage the frontend owns. The materialised PetManifest is the second consumer, and it is the price of running the same selection code as the desktop rather than a second one. Its exact size is a compile-time property of the existing manifest capacities, and for the esp32 toolchain of this release it is 13844 bytes, of which 9216 are the image paths a pack never fills. The structure belongs to the application rather than to the frontend, which holds a pointer to it, so one storage decision covers both the pack path and the fallback path.

That structure lives in static storage in the platform entry rather than in a stack frame. The main task stack is 3584 bytes, so no path on the device may hold a manifest-sized automatic, and the materialisation clears its destination in place for the same reason.

The Core continues to perform no dynamic allocation. The frontend performs no allocation after initialisation, and the firmware application performs none at all. Whatever the SDK allocates for the SPI bus and its DMA descriptors belongs to the SDK and is recorded in the evidence rather than claimed as zero.

The two-hour soak observation includes the minimum free heap at the start and at the end, because a slow leak inside a loop that never returns is exactly the failure a single boot cannot show.

8.9 Testing and verification

The hosted gate covers the debounce rule over level and clock sequences, the frame composition and clipping decisions over a caller-owned buffer, the fallback appearance, the role to pack entry resolution, the pack reader over valid and malformed packs, the converter over known inputs and rejected inputs, and the firmware lifecycle over supplied time values through a stepped run.

The ESP-IDF build covers compilation and linking of the backends, the pack embedding, and the embedded boundary check over the new sources.

Device observation covers the physical panel output, the panel offsets and colour order, the physical button behaviour including the strapping pin, the continuous run, and the heap trend. Section 10.6 defines what each observation must report. None of it is automated, and LIM-018 records that gap.

8.10 Component embedded suitability

The rule from section 8.4 of the v0.3.0 architecture continues to apply. A component is judged by its actual dependencies rather than by its directory, a hosted implementation is isolated by its own directory and a build option rather than by a preprocessor branch, and a component that cannot enter the embedded build is excluded with a recorded reason.

This release extends the same rule to a frontend for the first time. frontends/tft is one target with two selectable backends, which is the pattern components/assets already holds for its readers.

8.11 Language and documentation

Code and developer documentation use British English. Public identifiers keep the pet prefix, and the TFT frontend uses pet_frontend_tft for its public surface in the same way the SDL frontend uses pet_frontend_sdl.

8.12 Failure handling on the device

A failure is either terminal for a capability or terminal for the run, and the firmware says which.

A rejected pack costs the canonical appearance and nothing else, so the run continues on the procedural fallback and a diagnostic records the reason. A panel that fails to initialise costs all presentation, so the run continues, keeps the Core alive, and reports the fault, because a device that reboots in a loop teaches a maintainer less than one that stays up and explains itself.

The firmware never calls a reset as a recovery strategy in this release, and it never silences a failure by falling back without saying so.

9. Architecture decisions

9.1 Accepted decisions

  • v0.5.0 is the board runtime and embedded asset pipeline release
  • the classic LILYGO T-Display on esp32 is the only supported board
  • the firmware application owns a continuous non-returning run loop with a bounded elapsed delta
  • the firmware lifecycle gains a stepped form so a host test can drive it with supplied time values
  • the device supplies PET_WALL_CLOCK_UNKNOWN and computes no absence
  • frontends/tft is one frontend with common sources and selectable ESP-IDF backends
  • the frontend holds one full frame buffer rather than streaming rows
  • the button debounce rule is pure and lives beside the frontend that consumes it
  • a board button is direct user input and is not an adapter
  • the edge to semantic action mapping lives in the firmware application, as it does on the desktop
  • firmware-tft is a registered composition and is hosted build and test evidence only
  • the pack format carries asset facts only, with no board offset, no rotation, and no colour order
  • the pack carries the compiled presentation structure, so the device reuses the existing role, animation, and fallback selection rather than implementing a second one
  • the canonical manifest stays the single authored source, and the pack is its compiled form
  • the pack encodes RGB565 in a fixed declared byte order, with a one-bit transparency mask
  • a pack frame is placed at a whole multiple of its extents, and a surface that cannot take one reaches the procedural fallback
  • one iteration composes into a single frame buffer and transfers it once, reading pack pixels in place rather than copying the pack into RAM
  • the pack reader validates before it looks up and returns pointers into read-only memory
  • the pack is generated at build time into the build directory and is never committed
  • the pack is linked into the firmware image rather than flashed into a partition
  • the pack reader accepts one format version and rejects every other rather than migrating
  • the converter is a project-owned Python program using the standard library only
  • the canonical manifest grammar eventually has one reader, which is the converter, and this release defers that collapse while requiring the converter to isolate its grammar reading
  • serial diagnostics follow a documented key-value line format with a stable event vocabulary
  • the board profile records which of its values are measured and which remain vendor-documented
  • device evidence is maintainer observation and is never recorded as automated

Five of these decisions have real alternatives and outlive this release, so each receives an ADR written by the epic that commits to it.

DecisionADRWritten by
The hosted firmware-tft verification compositionADR-016E30, PBI-105
The run loop and its time source policyADR-017E32, PBI-110
The full frame buffer strategyADR-018E33, PBI-114
The pack format and the converter contractADR-019E34, PBI-117
The pack frame placement and its surroundADR-020E35, PBI-122

Identifiers continue from ADR-016, because ADR-015 was written during planning rather than by an epic. It records where the manifest grammar ends up and is the one accepted decision here whose implementation belongs to a later release. ADR-016 through ADR-020 are assigned in the table in the order their owning epics accepted them.

9.2 Open decisions

DecisionOwner
Whether the second button maps to an existing action or is only verified electricallyThis release, epic E36
Non-volatile storage on the device, its layout, and its wear behaviourLater hardware runtime release
Whether a second frontend justifies a shared frontend support layerLater release, after this frontend produces evidence
Whether a device that gains a clock should compute an absenceLater behaviour release

The first is open only until the epic that owns it measures the pin. The rest wait on a release that has the hardware, the second frontend, or the behaviour to decide them, and they are carried by LIM-015, the roadmap, and LIM-014 respectively.

9.3 Architecture deviations

No deviation from the general architecture is accepted for this release.

Any later deviation must be recorded here with its reason, impact, and tracking PBI.

10. Quality requirements

10.1 Requirement mapping

Requirementv0.5.0 tacticEvidence
SPEC-FR-008a board button produces the same semantic action a key press doesdebounce tests, firmware input tests, and device observation
SPEC-FR-009two buttons carry the supported interactionfirmware input tests and device observation
SPEC-FR-012a rejected pack and a failed panel each leave a valid worldfallback tests and failure handling review
SPEC-FR-013a monotonic platform clock supplies a bounded delta to the Corestepped lifecycle tests over supplied time values
SPEC-FR-014the pet presents its ambient activities on the panel without inputdevice observation over the continuous run
SPEC-FR-025the frontend receives a snapshot and never a world pointerfrontend contract and include boundary checks
SPEC-FR-026a 135 by 240 panel presents the same pet as a desktop windowrole mapping and layout tests
SPEC-FR-029the second frontend adds no Core, format, or behaviour changeCore diff and boundary checks
SPEC-NFR-001no ESP-IDF dependency in the Core, the components, or a hosted compositionsymbol, include, and embedded boundary checks
SPEC-NFR-002one frame buffer, a pack read in place, and no runtime allocationmemory budget and the soak heap observation
SPEC-NFR-003pure rendering, filtering, and packing decisions with supplied inputsthe hosted firmware-tft composition and the converter suite
SPEC-NFR-004a malformed pack is rejected before any lookuppack reader rejection tests
SPEC-NFR-005a bounded delta and a yielding loop keep presentation and input currentdevice observation over the continuous run
SPEC-NFR-006asset facts in the pack, board facts in the profile, effects in the backendsdependency rules and review
SPEC-NFR-007a stated and deliberately narrow pack promisesection 8.4 and the pack format ADR

10.2 Required test areas

  • the debounce rule over stable, bouncing, and held level sequences with supplied timestamps
  • an edge produced once per press rather than once per sample
  • the mapping from an edge to a semantic action, including an unmapped edge producing none
  • the stepped firmware lifecycle over supplied time values, including a clamped delta
  • role selection and pack entry resolution, including a role the pack does not carry
  • frame composition, clipping, and integer scaling into a caller-owned buffer
  • the procedural fallback appearance with no pack present
  • pack reader acceptance of a valid pack and rejection of a truncated one, a corrupted one, one with an unknown format version, and one whose entry offsets fall outside its payload
  • the manifest materialised from the pack resolving the same role, animation, frame, and duration as the manifest parsed from the canonical text, over every role the canonical set declares
  • converter determinism over repeated runs and rejection of an unsupported PNG subset
  • converter output matching a known expected pack for the canonical sources
  • continued headless and desktop SDL gate coverage

10.3 Required development checks

The strict gate continues to apply to implementation PBIs.

The release gate must cover the existing strict gate over the supported hosted compositions including firmware-tft, the include and embedded boundary checks over the new sources, the Core symbol scan, which must still find no file or allocation dependency, the converter suite alongside the existing tool suites, and the audit gate before release completion.

The ESP-IDF build is run outside the hosted gate and is required release evidence.

10.4 Release compliance

The release complies with this architecture when:

  • all v0.5.0 exit criteria are satisfied
  • all v0.5.0 PBIs are complete
  • support claims match executed evidence, and no device claim is presented as automated
  • a canonical frame reaches the panel through the pack, which a fallback-only run does not satisfy
  • no excluded capability is claimed
  • new limitations are recorded and linked to owners
  • this document reflects the implemented system

10.5 Release evidence

The release evidence is recorded in docs/releases/v0.5.0/evidence.md at release time, following the structure used by release v0.4.0 evidence.

ClaimEvidence
the supported compositions verify from a clean tree./gate.py audit --all-compositions over headless, desktop-sdl, and firmware-tft
the common TFT and firmware decisions are asserted rather than observedthe TFT frontend, firmware application, and pack reader suites
the converter is deterministic and rejects what it cannot decodethe converter suite over known inputs and rejected inputs
no ESP-IDF dependency reaches a hosted compositionthe include and embedded boundary checks and the hosted build itself
the firmware image carries the packthe ESP-IDF build report and the embedded pack size
the board runs Pet continuouslya two-hour maintainer run with the serial transcript and the heap readings
the panel presents the canonical petmaintainer observation on the classic board, with the transcript recording pack acceptance
a button produces a visible semantic responsemaintainer observation together with the input diagnostic lines
the release claims no device persistence and no automated device verificationthe support matrix, LIM-015, and LIM-018

10.6 Device evidence model

Device evidence is produced by a maintainer watching the selected classic T-Display, and by nothing else. No gate, no build output, no test result, and no agent report may stand in for it. A claim about the board that nobody watched is not recorded as unverified. It is not recorded at all.

An observation counts only when it names the board and the variant, the firmware commit, the date, what was watched, and what happened. It is written into the release evidence file in the maintainer’s own words, alongside the transcript that accompanies it. The transcript is the second half of the evidence rather than the whole of it, and section 8.7 bounds what it can carry.

The release requires three observations, and they are kept separate because they fail separately.

The panel observation. A maintainer watches the panel and reports that the pet is visible, that it is the canonical pet rather than the procedural fallback, that its colours and orientation match the canonical art, and that it is positioned inside the visible area rather than clipped by a panel offset. The accompanying transcript carries display-ready with the measured geometry and pack-ready with the accepted pack. A run whose transcript shows pack-rejected satisfies the fallback criterion and fails this one.

The button observation. A maintainer presses each button in turn and reports what the pet did. The observation covers both buttons, the behaviour of the strapping pin at reset and during a run, and whether the visible response followed the press closely enough to feel like an answer to it. The accompanying transcript carries one input line for each press that produced an action. A press that produces an input line and no visible response fails this observation, because the diagnostic proves the action reached the Core and says nothing about what the user saw.

The soak observation. A maintainer runs the board continuously for two hours and reports whether the pet kept moving through its ambient activities. Two hours is the stated duration rather than a judgement, and a shorter run is not the same evidence. The run is not reset, reflashed, or power cycled, and the button observation may be taken inside it as long as the presses are recorded with the time they happened. The run must show no unexpected reset, no watchdog abort, no stalled or frozen presentation, and no press that produced nothing. The transcript carries a heap line at the start and a heap line at the end, and the two min-free-bytes readings are recorded side by side, because a minimum free heap that fell across two hours is the failure a single boot can never show. A run that ends early is reported with the time it reached and the reason.

These three observations, together with the two-hour duration and the heap readings, are the whole device claim of the release. Everything else in the support matrix is a build or test claim.

11. Risks and technical debt

RiskImpactMitigation
the panel measurement is deferred and the pack encoding is frozen firstthe artefact is designed for a display nobody has lit and has to be regenerated with its readerkeep the E31 to E34 edge in the epic map and treat the encoding as open until the colour path is verified
board offsets or rotation leak into the pack for conveniencea second panel needs a new artefact rather than a new profilekeep the split in the format ADR and review the entry table against it
the ESP-IDF backend grows drawing decisions because it is closest to the panelthe untestable half of the frontend becomes the half that decideskeep composition in the common sources and keep the backend to bus, transfer, and error translation
the hosted composition is read as device evidencea support claim becomes falsestate the claim in the support matrix and the evidence, and keep the device rows separate
the run loop hides a slow leak that one boot never showsthe device dies after hours in a way the gate can never seerequire the two-hour observation with heap readings at both ends
the delta bound is set so low that the world runs slower than realitythe pet ages more slowly on the device than on the desktopderive the bound from the observed iteration time and record the value with its reason
GPIO 0 strapping behaviour makes one button unusablea mapped action becomes unreliable or the board fails to bootrecord the pin behaviour during bring-up and map the action to the safer pin
a fallback-only run is accepted as successLIM-013 is closed without an embedded asset pathkeep the canonical frame criterion and the fallback criterion as separate exit criteria
the converter grows into a general PNG decodera build tool becomes a maintenance burden with its own bug classrestrict the accepted subset explicitly and reject everything else with a named reason
device evidence is repeated by hand every release foreverlater releases either skip it or overstate itrecord LIM-018 and carry the observation policy that LIM-011 already established
the converter becomes a second reader of the canonical manifest grammara format change has to be made twice in two languages with nothing checking that they agreeADR-015 decides the collapse, PBI-118 isolates the grammar module so the later change moves it, and the limitation opens when the converter lands

12. Glossary

12.1 Terms

Board profile

The measured and vendor-documented facts of one board variant, including pins, geometry, panel offsets, orientation, and colour order.

Panel offset

The position of the visible area inside the controller’s addressable area. It is a board fact and never an asset fact.

Transfer orientation

The orientation the display backend applies when it sends a frame, as distinct from the panel’s native orientation.

Embedded asset pack

The generated binary artefact carrying every frame the embedded runtime can present, together with the entry table that addresses them.

Entry table

The fixed-size records at the head of a pack that name each frame and locate its payload.

Debounce rule

The pure state transition that turns a sequence of raw levels and timestamps into stable button edges.

Procedural fallback

The calm appearance the frontend draws without any asset, used whenever a pack cannot be accepted or resolved.

Soak run

A continuous device run of a stated duration, observed for resets, stalls, lost input, and heap trend.

Maintainer observation

Release evidence produced by a person watching a physical device, recorded as such and never counted as an automated check.

Hardware in the loop

An automated gate that drives and asserts a physical device. The project has none, which LIM-018 records.

12.2 References