EcsControlStore
The second-round evaluation program: a closed-loop LED world under the derived schedule, with a deposited-and-collected reporting facility.
Overview
EcsControlStore is the second-round evaluation program for the Entity Component System (ECS) architecture for control systems, following the first round's EcsControlBase variants. Where the first round explored the store, the layering, and the adaptation modes across eight variants, this round built the mechanisms: declared access roles, the schedule derived from them, checked construction, the value store split into its variable and locked halves.
This document describes the program itself – its world, its Systems, its wiring, and what it does on the bench. The implementation document explains the mechanisms the program is built from; the authoritative form of every access fact below is the manifests in the code.
Scope
What the first round's programs performed by discipline and review is now mechanised: the producer and consumer roles are declarations (the manifests), the scheduling order is derived at start-up, and the construction checks run before the first tick – no check is relaxed, no exemption exists. The deposit classes and the tick monitoring are new with this round.
Still at the concept level: partitioning and second schedules, the asynchronous seam (this program drives all hardware synchronously; the first round's variants explore and demonstrate that seam), and recovery – the motivating horizon, with snapshot work ahead. The outlook document starts from these.
What the Program Does
Four LEDs under closed-loop control, and a report to a terminal.
Two leader LEDs blink, each at its own period. Two follower LEDs each track a leader – crosswise: follower 0 follows leader 1, follower 1 follows leader 0. The coupling is measurement-based: SenseSystem reads the actual pin levels back each tick, and the followers are set to what their leaders measurably do, not to what the leaders were told to do. Actuation writes a pin only on mismatch between set-point and measurement. The loop closes through the hardware, tick by tick.
The report side is the round's shared facility: HeartbeatSystem deposits a status line once per second, TickMonitorSystem deposits a report whenever the kernel recorded missed ticks – and stays silent otherwise. PrintSystem collects both and hands the text to DrainSystem, which feeds the UART.
Hardware
-
Pico2 board; four LEDs,
LED0–LED3as assigned inLEDbinding– the external hardware is four LEDs and their resistors, nothing more. -
Report UART:
UART1, TX/RX on pins 4/5, 38400 baud, 8N1. -
The console UART stays reserved for the run-time error output and the development-build construction report.
The World
The populations are two leaders, two followers, and two printer participations – all W constants. The store carries eight tokens:
| Token | Type backing | Writer | Readers |
|---|---|---|---|
LedMeasured |
leader + follower arrays | Sense |
Couple, Actuate |
LedSetpointLeader |
leader array | Blink |
Actuate |
LedSetpointFollower |
follower array | Couple |
Actuate |
BlinkConfig |
leader array | Blink (owned) |
– |
PrintCursor |
printer array | Print (owned) |
– |
PrintBuf |
printer array | Heartbeat, TickMonitor (deposit) |
Print (collector) |
HeartbeatStatus |
single record | Heartbeat (owned) |
– |
DrainBuf |
none – edge token | Print |
Drain |
LedMeasured and the two set-point tokens share one measurement story but split at the write-partition boundaries: one token per writer, one array per token where the writers differ. DrainBuf is the pure-id edge token – its payload lives in a service module's buffer.
The locked half (CL) holds the pin bindings, the crosswise follows references, the printer wiring (writer and buffer per participant), and the device handles for UART, timer, and drain buffer.
The Systems
Eight Systems, one module each:
| System | Manifest (compact) | Runs |
|---|---|---|
Sense |
P: LedMeasured | every tick |
Blink |
O: BlinkConfig; P: LedSetpointLeader | every tick |
Couple |
C: LedMeasured; P: LedSetpointFollower | every tick |
Actuate |
C: LedMeasured, LedSetpointLeader, LedSetpointFollower | every tick |
Heartbeat |
O: HeartbeatStatus; Px: PrintBuf | every 100 ticks |
TickMonitor |
Px: PrintBuf | every tick, deposits only on slip |
Print |
O: PrintCursor; P: DrainBuf; Cx: PrintBuf | every tick |
Drain |
C: DrainBuf | every tick |
Two chains flow through the store:
SenseSystem ---- LedMeasured ---------+--> CoupleSystem ---+
| | LedSetpointFollower
+--------------------+--> ActuateSystem
|
BlinkSystem ---- LedSetpointLeader ------------------------+
HeartbeatSystem ---+
+-- PrintBuf --> PrintSystem -- DrainBuf --> DrainSystem --> UART
TickMonitorSystem -+ (deposited / collected)
The Systems are registered in an arbitrary order; the schedule is derived from the manifests at start-up. The development build prints the whole derivation – manifests, dependencies, order, check results – at start-up: the listing is shown under Development and Deployment Build, below.
Wiring and Values
The deployment choices, all in W, I, and the CL wiring lines:
-
identities (
I):Leader_0,Leader_1;Printer_Heartbeat,Printer_TickMonitor; -
the crosswise coupling:
followswiredI.Leader_1andI.Leader_0respectively; -
each printer participation paired with its own text writer and print buffer;
-
one UART, one timer, one drain buffer, each behind a
Whandle.
The tuning values (V): tick period 10 ms; blink periods 400 ms and 650 ms, the heartbeat 1000 ms – all held as tick counts, converted once, in V. Report baud rate 38,400.
The full pin and handle assignments read directly off W.mod and CL.mod – a screenful together; this document does not duplicate them.
World Creation, Concretely
The main program module's body is the whole sequence – build, attest, acquire, value, propagate, then the kernel calls. In this program:
-
build – allocates the store and seven device objects (the UART, the drain buffer, two print buffers, two text writers, the timer), binds each to its
Whandle, and configures the hardware through the binding modules; -
attest – verifies every device reference wired in
CLagainst a bound device: seven checks, one per wiring; -
acquire – samples the four LED pins into the measured Components and takes the heartbeat's first timer reading;
-
value – sets the blink periods and tickers, zeroes the set-points, sequence counters, and the heartbeat count;
-
propagate – writes the set-points out to the four pins, so the hardware starts in agreement with the store.
What You Observe
The four LEDs: the leaders blink at their two rates; the followers reproduce their leaders' measured state, crosswise, one tick behind.
On the terminal, one heartbeat line per second – the System's name, its sequence count, the timer reading, and the delta to the previous beat:
Heartbeat 13 13225786 1000000
Heartbeat 14 14225786 1000000
Heartbeat 15 15225785 999999
Heartbeat 16 16225786 1000001
Heartbeat 17 17225785 999999
TickMonitor stays silent: in normal operation no tick is missed. To see it report, the program includes a deliberate test aid: in PrintSystem.mod, the blocks marked "uncomment for TickMonitor trigger" add a periodic busy-loop that overruns the tick on purpose. Rebuilt with the trigger enabled, the monitor deposits its report – elapsed ticks of the pass, cumulative missed ticks, the tick count – whenever the kernel records a slip:
Heartbeat 13 13236490 1000001
TickMonitor 9 416 1301
TickMonitor 9 424 1326
TickMonitor 9 432 1351
TickMonitor 9 440 1376
Heartbeat 14 14236490 1000000
TickMonitor 9 448 1401
TickMonitor 9 456 1426
TickMonitor 9 464 1451
TickMonitor 9 472 1476
Heartbeat 15 15236490 1000000
TickMonitor 9 480 1501
TickMonitor 9 488 1526
TickMonitor 9 496 1551
TickMonitor 9 504 1576
Heartbeat 16 16236490 1000000
TickMonitor 9 512 1601
TickMonitor 9 520 1626
TickMonitor 9 528 1651
TickMonitor 9 536 1676
Heartbeat 17 17236489 999999
TickMonitor 9 544 1701
TickMonitor 9 552 1726
TickMonitor 9 560 1751
TickMonitor 9 568 1776
The slip-and-count policy behind those numbers is the kernel's, described in the implementation document.
Development and Deployment Build
The program as published is the development build: main calls PlanView.Report between Kernel.Plan and Kernel.Commit, and the development-only modules PlanView and TokenNames are imported. A commented Kernel.Relax line documents the coverage-check waiver used while a producer's consumer does not exist yet. A deployed build drops the report call, the Relax line, and the two imports – Plan and Commit appear identically in both builds, and a rejected plan is then a hard fault with its own error code.
The report at start-up, program as published – the manifests in registration order, the dependency matrix, the derived order, the verdict:
Kernel.Plan
manifests (registration order):
0 Blink
O:
BlinkConfig
P:
LedSetpoint Leader
1 Couple
C:
LedMeasured
P:
LedSetpoint Follower
2 Actuate
C:
LedMeasured
LedSetpoint Leader
LedSetpoint Follower
3 Drain
C:
DrainBuf
4 TickMonitor
Px:
PrintBuf
5 Print
O:
PrintCursor
P:
DrainBuf
Cx:
PrintBuf
6 Sense
P:
LedMeasured
7 Heartbeat
O:
HeartbeatStatus
Px:
PrintBuf
dependencies (registration order):
0 Blink
1 Couple
Sense
2 Actuate
Blink
Couple
Sense
3 Drain
Print
4 TickMonitor
5 Print
TickMonitor
Heartbeat
6 Sense
7 Heartbeat
derived scheduling order:
0 Blink
1 TickMonitor
2 Sense
3 Couple
4 Actuate
5 Heartbeat
6 Print
7 Drain
plan checks: all pass
Three things read off directly. The registration order is visibly not the schedule: Sense registers sixth and runs second, Heartbeat registers last and runs fifth. The collector's place is derived, not stated: Print follows both of its depositors, and Drain follows Print. And the last line is the gate's evidence – plan checks: all pass is what Kernel.Commit then silently confirms.
To show the checks failing, the same program is rebuilt with two violations induced in BlinkSystem's module body – two edited manifest lines, no code changed: LedMeasured added to P, a second writer beside Sense; LedSetpoint Leader added to C, the System now consuming its own product. The report then reads:
Kernel.Plan
manifests (registration order):
0 Blink
C:
LedSetpoint Leader
O:
BlinkConfig
P:
LedMeasured
LedSetpoint Leader
1 Couple
C:
LedMeasured
P:
LedSetpoint Follower
2 Actuate
C:
LedMeasured
LedSetpoint Leader
LedSetpoint Follower
3 Drain
C:
DrainBuf
4 TickMonitor
Px:
PrintBuf
5 Print
O:
PrintCursor
P:
DrainBuf
Cx:
PrintBuf
6 Sense
P:
LedMeasured
7 Heartbeat
O:
HeartbeatStatus
Px:
PrintBuf
dependencies (registration order):
0 Blink
1 Couple
Blink
Sense
2 Actuate
Blink
Couple
Sense
3 Drain
Print
4 TickMonitor
5 Print
TickMonitor
Heartbeat
6 Sense
7 Heartbeat
derived scheduling order:
0 Blink
1 TickMonitor
2 Sense
3 Couple
4 Actuate
5 Heartbeat
6 Print
7 Drain
plan check violations:
multiple writers
LedMeasured
consumes own written token
LedSetpoint Leader
run-time error in thread mode: 54 core: 0
kernel plan check violation
Kernel.Commit addr: 100063CEH line: 196
stacked registers:
xpsr: 29000000H
pc: 100063CEH
lr: 100087C3H
r12: 96B5E400H
r3: 00000002H
r2: 00000080H
r1: 00000000H
r0: 00000009H
sp: 2003F0A8H
trace: code addr ln frame addr fsz
Kernel.Commit 100063CEH 196
EcsControlStore..init 100087BEH 164 2003F0CCH 4
Everything still happens, in order. Plan gathers and derives – note the dependency matrix now showing Couple depending on Blink, an edge created by the false producer claim – and the derived order still prints, since neither violation is a cycle. Both violations are reported by name, with their offending tokens, in the one run. Then Commit faults with the plan-check error code, and the run-time error machinery takes over, as it would for any construction fault.
The perturbation never touched executable code: two declaration lines were false, and the construction checks caught exactly them, completely, before the first tick.
The lib Directory
The example directory holds the program: the store modules, the Systems, the bindings, the main program module. Alongside it, the example's own lib directory holds everything the program needs beyond the installed framework: the kernel and its development-build modules, the service modules – buffers, writers, drivers – in their handle-based form, and the framework modules the build requires alongside them. Together the two directories are self-contained: the installed framework library (lib/v3.2) is left untouched – building the example writes nothing outside the example's tree, and every deviation from v3.2 lives visibly in the example's lib.
The division is not packaging convenience; it is the evaluation's seam. What sits in the example's lib is candidate material for the framework's next major version: the ECS kernel, and – reaching further – changes to the framework's own idioms, the handle-based device access first among them, which alters the API of modules as basic as the UART driver. Form and scope are still being worked out across the evaluation rounds; the destination is settled: changes of this kind will result in lib/v4.0 – API and conceptual changes, not additions to v3.2. As of now, programs written against v3.2 are unaffected by the evaluation, and what form v4.0 takes – including how it will affect existing programs – is itself still evaluation work.
Repository
lib/v3.2 – <repo>/examples/v3.2/rpi/pico2/EcsControlStore/EcsControlStore-v12, with the example's lib directory alongside.
See Also
- ECS for Control — Concepts
- ECS for Control — Implementation Concepts
- ECS for Control — Round 2 Lessons Learned
- ECS for Control — Round 1 Retrospective
- ECS for Control — Round 2 Outlook
- EcsControlBase (v0–v7)
Last updated: 30 July 2026