Skip to content

feat: device value semantics — Slice 1 (single-authority ranges, clamp-not-wrap, per-device serialization)#27

Merged
gbradham merged 5 commits into
mainfrom
claude/device-value-semantics-slice1
Jul 12, 2026
Merged

feat: device value semantics — Slice 1 (single-authority ranges, clamp-not-wrap, per-device serialization)#27
gbradham merged 5 commits into
mainfrom
claude/device-value-semantics-slice1

Conversation

@gbradham

Copy link
Copy Markdown
Member

Slice 1 of the Device Value Semantics & Data-Driven Runner Controls feature (plan in docs/features/device-value-semantics/, local). This is the foundation the other two slices read from, and it stands alone as a fix for two reproducible-today defects.

What's here (5 commits, TDD)

  1. ActionValueSpec + clamp_to_spec (hal/value_spec.py) — a frozen per-action value object (kind/min/max/step/unit/label) with save-time validation, and a clamp that rejects NaN/±inf/non-numeric before range handling and clamps rather than wraps.
  2. BaseDevice.value_spec() accessor + per-device command lock (D13) — one authority resolves declared-or-fallback; an asyncio.Lock held across the whole execute_action body serializes commands from every source so their hardware steps never interleave (e-stop stays exempt).
  3. Declarative device value semantics + clamp-not-wrap — an optional per-action value block overrides an op-inferred fallback (= today's ranges, so old .gdevice files load unchanged); _execute clamps before dispatch and the byte/word ops clamp to wire width instead of masking.
  4. Built-in devices expose value_spec() — digital→switch, PWM→0..2^res−1, analog read→0..2^res−1, servo→instance angle bounds.
  5. Nodes read range from the bound device — PWMWrite/Output/AnalogRead drop their hardcoded 0-255 / 10-bit; a 12-bit PWM device now reaches 4095.

Bugs fixed (verified by tests)

  • Over-range writes wrapped to an unrelated number (70000 & 0xFFFF == 4464) → now clamp to the ceiling.
  • NaN crashed the write path (int-cast) → now rejected before anything reaches hardware.

Tests & regression

  • ~54 new unit tests across value_spec, the command lock (serialization proven), declarative value semantics, built-in specs, and node de-hardcoding.
  • Full unit suite green: 1252 passed, 1 skipped. The high-churn base_device.py refactor broke no existing HAL/node tests.

Deliberately carried forward

  • The D11 out-of-range-at-load flag is carried to Slice 2, where its display surface (the persistent runner status strip) is built — detecting it now with nowhere visible to show it would be premature.
  • Slice 2 (generated runner controls) and Slice 3 (function buttons) build on the value_spec() / execute_action-lock seams this slice publishes.

gbradham added 5 commits July 11, 2026 21:14
…lice 1 foundation)

Per-action value semantics as data: kind/min/max/step/unit/label with save-time
validation, range-containment for out-of-range read marking, and clamp_to_spec
that rejects NaN/inf/non-numeric BEFORE range handling and clamps rather than
wraps (fixes the reproducible 70000 & 0xFFFF -> 4464 aliasing and the NaN
int-cast crash). Pure module, 30 unit tests. No consumers wired yet.

Implements docs/features/device-value-semantics plan decisions D-1, D-3 (core).
BaseDevice.value_spec(action) resolves declared-or-fallback so every consumer
reads an action's range/unit through one authority (declared spec wins; base
returns None until subclasses provide op-inferred defaults). Add a per-device
asyncio.Lock held across the whole execute_action body so commands from any
source (flow, function, manual) never interleave their hardware steps; the
non-yielding guards keep acquisition order == call order (FIFO). Emergency stop
stays exempt (does not acquire it).

Tests: serialization proven (no interleave), lock released, accessor precedence.
HAL regression baseline stays green. Plan D-1, D-5.
DeclarativeDevice now resolves value_spec() for its actions: an optional per-
action 'value' block (kind/min/max/step/unit/label) overrides an op-inferred
fallback (write_pwm -> board pwm_resolution, write_byte -> 0xFF, write_word ->
0xFFFF) equal to today's ranges, so existing .gdevice files load unchanged.

_execute clamps a value op to its declared range before dispatch and the
write_byte/write_word ops clamp to their wire width instead of masking (& 0xFF/
& 0xFFFF) -- fixing the 70000 -> 4464 aliasing. clamp_to_spec rejects NaN/inf
before any hardware call; clamp warnings are rate-limited per action.
validate_definition rejects value blocks on no-value/multi-arg ops and malformed
ranges. 12 tests; full HAL suite green. Plan D-2, D-3, D-16.
Each built-in provides its action ranges through the shared accessor so nodes
and the runner read one authority: DigitalOutput 'set' -> switch; PWMOutput
'set' -> 0..2^pwm_resolution-1 and 'set_percent' -> 0..100%; AnalogInput 'read'
-> 0..2^analog_resolution-1; Servo 'set_angle' -> instance min/max angle in deg
(declared, since the bounds are per-instance). 5 tests; HAL suite green (226).
Plan D-1, D-7 (built-in half of the value_spec story).
PWMWriteNode and the generic OutputNode clamp to the bound device's value_spec
range instead of a literal 0-255, so a 12-bit PWM device reaches 4095;
AnalogReadNode computes voltage from the device's declared read range instead of
a hardcoded 10-bit assumption. Nodes query value_spec() lazily at use, keeping
the device the single authority. Unbound nodes fall back to neutral defaults.

The D11 out-of-range-at-load *flag* is carried to Slice 2, where its display
surface (the persistent runner status strip) is built -- logging it to nowhere
visible now would be premature. 2 tests; node + HAL suites green (333).
@gbradham gbradham merged commit ffc6fd2 into main Jul 12, 2026
4 checks passed
@gbradham gbradham deleted the claude/device-value-semantics-slice1 branch July 12, 2026 03:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant