Add virtual outputs and output state as a bind source #144

Merged
Claude merged 5 commits from feat/yn-27-virtual-endpoints into feat/yaml-nodes 2026-10-17 20:38:29 +00:00
Collaborator

virtual: true on a light, fan, load, dimmable_light or variable_fan gives an endpoint with no hardware that holds its state, reports, takes commands and can be bound to. A bind may now follow or toggle from any such output, so one touch can drive a virtual "Main lights" and the real lights follow it.

Runtime (shared paths, for review):

  • SwitchEndpoint takes ctrl::noPin and skips the GPIO calls; hand-written nodes are unchanged. LevelEndpoint skips LEDC set-up and writes when it has no pins, so a virtual dimmer cannot reconfigure a real channel.
  • Two more source decoders in the array: StateSwitch and StateLevel, on from the state, no freshPress. The generator maps toggle from an output to ToggleOnChange.
  • class_filter.h now folds as many low class bits as it takes until every wanted class fits, so Event plus State plus the required classes always fit in four MCP2515 slots. A remote output source adds State to the interests.

Generator: virtual field (rejects pin, pins, invert, frequency), virtual PWM endpoints use no PWM channel, output types are accepted as bind sources, a bind cannot take its own endpoint as source. Schema, README (field rows and a Virtual endpoints section), spec 9.5 and 12, and the LOLIN32 fixture (virtual light toggled by the touch pad, the ceiling dimmer following it) are updated. dimming_curve is not rejected on virtual endpoints yet because it does not exist on this base.

Tested: native 647 -> 652, Python 140 -> 146; pio run and the LOLIN32 fixture build pass.

YN-27

`virtual: true` on a `light`, `fan`, `load`, `dimmable_light` or `variable_fan` gives an endpoint with no hardware that holds its state, reports, takes commands and can be bound to. A bind may now follow or toggle from any such output, so one touch can drive a virtual "Main lights" and the real lights follow it. Runtime (shared paths, for review): - `SwitchEndpoint` takes `ctrl::noPin` and skips the GPIO calls; hand-written nodes are unchanged. `LevelEndpoint` skips LEDC set-up and writes when it has no pins, so a virtual dimmer cannot reconfigure a real channel. - Two more source decoders in the array: `StateSwitch` and `StateLevel`, `on` from the state, no `freshPress`. The generator maps `toggle` from an output to `ToggleOnChange`. - `class_filter.h` now folds as many low class bits as it takes until every wanted class fits, so Event plus State plus the required classes always fit in four MCP2515 slots. A remote output source adds `State` to the interests. Generator: `virtual` field (rejects `pin`, `pins`, `invert`, `frequency`), virtual PWM endpoints use no PWM channel, output types are accepted as bind sources, a bind cannot take its own endpoint as source. Schema, README (field rows and a Virtual endpoints section), spec 9.5 and 12, and the LOLIN32 fixture (virtual light toggled by the touch pad, the ceiling dimmer following it) are updated. `dimming_curve` is not rejected on virtual endpoints yet because it does not exist on this base. Tested: native 647 -> 652, Python 140 -> 146; `pio run` and the LOLIN32 fixture build pass. YN-27
Add virtual outputs and output state as a YAML bind source
Some checks failed
Build and test / build (pull_request) Has been cancelled
Build and test / release (pull_request) Has been cancelled
d6347adb2c
Claude left a comment

Two findings.

  1. scripts/yaml_node.py resolve_bind: only a self-source is rejected. Two outputs on one node that toggle from each other (A toggle B, B toggle A) become two ToggleOnChange binds. Each state report flips the other, and markDirty re-reports, so they oscillate for ever and flood the bus. Reject a cycle of toggle binds at generation time (a follow-only cycle settles, so it can stay). Add a test.

  2. test/test_class_filter: test_event_and_state_together_fit_with_every_required_class passes on the old one-bit fold, so it can't fail if the new loop is reverted. Event+State needs {0,1,7,4,3}; folding one bit gives {0,6,4,2}, which fits. Drop it. The Command/Event/Diag/Bulk test is the one that pins the change.

Runtime checks (noPin, empty-PinList LevelEndpoint, fold termination and masks, decoder cross-trigger, first-report sync) found no defects. Schema tests are skipped on my machine (28 skipped), so the if/then/else was not run.

Two findings. 1. scripts/yaml_node.py `resolve_bind`: only a self-source is rejected. Two outputs on one node that toggle from each other (A `toggle` B, B `toggle` A) become two `ToggleOnChange` binds. Each state report flips the other, and `markDirty` re-reports, so they oscillate for ever and flood the bus. Reject a cycle of toggle binds at generation time (a follow-only cycle settles, so it can stay). Add a test. 2. test/test_class_filter: `test_event_and_state_together_fit_with_every_required_class` passes on the old one-bit fold, so it can't fail if the new loop is reverted. Event+State needs {0,1,7,4,3}; folding one bit gives {0,6,4,2}, which fits. Drop it. The Command/Event/Diag/Bulk test is the one that pins the change. Runtime checks (noPin, empty-PinList LevelEndpoint, fold termination and masks, decoder cross-trigger, first-report sync) found no defects. Schema tests are skipped on my machine (28 skipped), so the if/then/else was not run.
Reject output toggle loops and drop a test that cannot fail
Some checks failed
Build and test / build (pull_request) Has been cancelled
Build and test / release (pull_request) Has been cancelled
8a85eb772b
rob force-pushed feat/yn-27-virtual-endpoints from 8a85eb772b
Some checks failed
Build and test / build (pull_request) Has been cancelled
Build and test / release (pull_request) Has been cancelled
to 408f867fb0
All checks were successful
Build and test / build (pull_request) Successful in 2m39s
Build and test / release (pull_request) Has been skipped
2026-10-17 20:16:36 +00:00
Compare
rob approved these changes 2026-10-17 20:33:21 +00:00
rob force-pushed feat/yn-27-virtual-endpoints from 408f867fb0
All checks were successful
Build and test / build (pull_request) Successful in 2m39s
Build and test / release (pull_request) Has been skipped
to e67585deb3
All checks were successful
Build and test / build (pull_request) Successful in 2m13s
Build and test / release (pull_request) Has been skipped
2026-10-17 20:34:42 +00:00
Compare
Claude merged commit 647dffeda3 into feat/yaml-nodes 2026-10-17 20:38:29 +00:00
Sign in to join this conversation.
No reviewers
rob
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
CamperCan/CampervanControlSystems!144
No description provided.