Pet architecture for v0.5.0
| Document version | Date | Summary |
|---|---|---|
| v1 | 2026-08-05 | Initial architecture for the board runtime and embedded asset pipeline release |
| v2 | 2026-08-05 | Accept the architecture for the board runtime and embedded asset pipeline release |
| v3 | 2026-08-06 | Record ADR-015 and the deferred single-source manifest grammar |
| v4 | 2026-08-06 | Record the diagnostic contract, the device evidence model, and ADR-016 |
| v5 | 2026-08-07 | Record ADR-019 and the embedded asset pack contract |
| v6 | 2026-08-18 | Record 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-tftcomposition 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
petprefix - 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
TODOcomment 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
- keep the panel first, so the pixel encoding is chosen against a measured display
- keep board facts in the board profile and asset facts in the pack, with no overlap
- generate the pack at build time and never commit it, so the canonical sources stay canonical
- link the pack into the image, so no partition, no filesystem, and no location policy is needed
- keep the run loop non-returning and its delta bounded, so a slow frame cannot become a time jump
- register the firmware and TFT composition, so the hosted gate covers what the device cannot assert
- keep ESP-IDF inside frontend-local backends selected by a build option, as the desktop reader already is
- treat a button as direct user input, so no adapter interface is opened by accident
- keep the fallback presentation reachable at every stage, so a failed pack degrades calmly
- 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.txtFiles 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
- the Core depends on no app, frontend, component, board, or platform target
- the common TFT sources depend on components and on plain board constants, never on ESP-IDF
- the ESP-IDF backends depend on the SDK and on the board profile, and nothing depends on them
components/assetsdepends on no frontend and no board- the firmware app depends on the public Core API and on the TFT frontend contract
- no component, app, or frontend reimplements the pack format
- 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 directoryThe 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
| Environment | Evidence target | Required for release |
|---|---|---|
| Linux x86-64 desktop, GCC | Build tested and runtime tested over headless, desktop-sdl, and firmware-tft | Yes |
| Linux x86-64 desktop, Clang | Build tested and runtime tested over the same three compositions | Yes |
firmware-tft hosted composition | Build and test evidence for the firmware orchestration boundary and the common TFT sources, not a device claim | Yes |
| Converter and pack reader on the host | Build tested and runtime tested over known inputs and rejected inputs | Yes |
T-Display classic variant, ESP-IDF v6.0.2 for esp32 | Cross-compile tested, including the backends and the embedded pack | Yes |
| Selected classic T-Display hardware | Runtime tested by maintainer observation, covering the panel, both buttons, and a two-hour continuous run | Yes |
| Any other T-Display variant | Not selected, unverified | No |
| Any other ESP32 board or embedded target | Designed, not implemented, unverified | No |
| MSVC on Windows | Portable by design, unverified | No |
| Clang and Ninja on Windows | Portable by design, unverified | No |
| Non-volatile storage on any target | Not implemented, unverified | No |
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:
firmwareandtftentries inconfig/compositions.json, resolving to thefirmware-tftcompositionPET_FRONTEND_TFT_ESP_IDFas the backend build selection, defaulting off and set on only by the ESP-IDF buildPET_ASSETS_PACK_READERas the pack reader build selection, followingPET_ASSETS_STDIO_READER- a pack generation target that runs
tools/assets/pack.pyand 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.pyfor the converter, joining the existing tool suites in the gate./pet.py flashand./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-compositions7.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-transferThe 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.
| Event | Emitted when | Fields |
|---|---|---|
boot | the application starts, before anything is initialised | board, target, version |
display-ready | the panel has been brought up | width, height, rotation, colour-order |
display-failed | the panel could not be brought up | reason |
pack-ready | the embedded pack has been accepted | version, entries, bytes |
pack-rejected | the pack was refused before any lookup | reason |
runtime-ready | the run loop is about to be entered | tick-ms |
input | a filtered edge produced a semantic action | button, action |
time-clamped | an elapsed delta arrived above the bound and was clamped | elapsed-ms, delivered-ms |
heap | a heap reading is taken, at least at the start and the end of a soak | free-bytes, min-free-bytes, uptime-s |
runtime-fault | a failure the run survived was reported | reason |
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.0is the board runtime and embedded asset pipeline release- the classic LILYGO T-Display on
esp32is 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_UNKNOWNand computes no absence frontends/tftis 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-tftis 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.
| Decision | ADR | Written by |
|---|---|---|
The hosted firmware-tft verification composition | ADR-016 | E30, PBI-105 |
| The run loop and its time source policy | ADR-017 | E32, PBI-110 |
| The full frame buffer strategy | ADR-018 | E33, PBI-114 |
| The pack format and the converter contract | ADR-019 | E34, PBI-117 |
| The pack frame placement and its surround | ADR-020 | E35, 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
| Decision | Owner |
|---|---|
| Whether the second button maps to an existing action or is only verified electrically | This release, epic E36 |
| Non-volatile storage on the device, its layout, and its wear behaviour | Later hardware runtime release |
| Whether a second frontend justifies a shared frontend support layer | Later release, after this frontend produces evidence |
| Whether a device that gains a clock should compute an absence | Later 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
| Requirement | v0.5.0 tactic | Evidence |
|---|---|---|
| SPEC-FR-008 | a board button produces the same semantic action a key press does | debounce tests, firmware input tests, and device observation |
| SPEC-FR-009 | two buttons carry the supported interaction | firmware input tests and device observation |
| SPEC-FR-012 | a rejected pack and a failed panel each leave a valid world | fallback tests and failure handling review |
| SPEC-FR-013 | a monotonic platform clock supplies a bounded delta to the Core | stepped lifecycle tests over supplied time values |
| SPEC-FR-014 | the pet presents its ambient activities on the panel without input | device observation over the continuous run |
| SPEC-FR-025 | the frontend receives a snapshot and never a world pointer | frontend contract and include boundary checks |
| SPEC-FR-026 | a 135 by 240 panel presents the same pet as a desktop window | role mapping and layout tests |
| SPEC-FR-029 | the second frontend adds no Core, format, or behaviour change | Core diff and boundary checks |
| SPEC-NFR-001 | no ESP-IDF dependency in the Core, the components, or a hosted composition | symbol, include, and embedded boundary checks |
| SPEC-NFR-002 | one frame buffer, a pack read in place, and no runtime allocation | memory budget and the soak heap observation |
| SPEC-NFR-003 | pure rendering, filtering, and packing decisions with supplied inputs | the hosted firmware-tft composition and the converter suite |
| SPEC-NFR-004 | a malformed pack is rejected before any lookup | pack reader rejection tests |
| SPEC-NFR-005 | a bounded delta and a yielding loop keep presentation and input current | device observation over the continuous run |
| SPEC-NFR-006 | asset facts in the pack, board facts in the profile, effects in the backends | dependency rules and review |
| SPEC-NFR-007 | a stated and deliberately narrow pack promise | section 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.
| Claim | Evidence |
|---|---|
| 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 observed | the TFT frontend, firmware application, and pack reader suites |
| the converter is deterministic and rejects what it cannot decode | the converter suite over known inputs and rejected inputs |
| no ESP-IDF dependency reaches a hosted composition | the include and embedded boundary checks and the hosted build itself |
| the firmware image carries the pack | the ESP-IDF build report and the embedded pack size |
| the board runs Pet continuously | a two-hour maintainer run with the serial transcript and the heap readings |
| the panel presents the canonical pet | maintainer observation on the classic board, with the transcript recording pack acceptance |
| a button produces a visible semantic response | maintainer observation together with the input diagnostic lines |
| the release claims no device persistence and no automated device verification | the 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
| Risk | Impact | Mitigation |
|---|---|---|
| the panel measurement is deferred and the pack encoding is frozen first | the artefact is designed for a display nobody has lit and has to be regenerated with its reader | keep 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 convenience | a second panel needs a new artefact rather than a new profile | keep 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 panel | the untestable half of the frontend becomes the half that decides | keep composition in the common sources and keep the backend to bus, transfer, and error translation |
| the hosted composition is read as device evidence | a support claim becomes false | state 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 shows | the device dies after hours in a way the gate can never see | require the two-hour observation with heap readings at both ends |
| the delta bound is set so low that the world runs slower than reality | the pet ages more slowly on the device than on the desktop | derive the bound from the observed iteration time and record the value with its reason |
GPIO 0 strapping behaviour makes one button unusable | a mapped action becomes unreliable or the board fails to boot | record the pin behaviour during bring-up and map the action to the safer pin |
| a fallback-only run is accepted as success | LIM-013 is closed without an embedded asset path | keep the canonical frame criterion and the fallback criterion as separate exit criteria |
| the converter grows into a general PNG decoder | a build tool becomes a maintenance burden with its own bug class | restrict the accepted subset explicitly and reject everything else with a named reason |
| device evidence is repeated by hand every release forever | later releases either skip it or overstate it | record LIM-018 and carry the observation policy that LIM-011 already established |
| the converter becomes a second reader of the canonical manifest grammar | a format change has to be made twice in two languages with nothing checking that they agree | ADR-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.