Explorar el Código

Plan zone-aware Impact 80 effects implementation

Paul Klumpp hace 2 semanas
padre
commit
f3edc8bbae
Se han modificado 1 ficheros con 331 adiciones y 0 borrados
  1. 331 0
      docs/superpowers/plans/2026-09-25-impact80-effects.md

+ 331 - 0
docs/superpowers/plans/2026-09-25-impact80-effects.md

@@ -0,0 +1,331 @@
+# Impact 80 Zone-Aware Lighting Effects Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Expose all official Impact 80 lighting effects with zone-aware CLI targeting, defaulting every command to Logo, Backlight, and Side.
+
+**Architecture:** Put protocol-neutral zone names, channel IDs, effect catalogs, aliases, and resolution in `internal/rgb`. Keep `internal/via` responsible only for QMK Raw HID channel/value transport. Resolve target zones in the Cobra command layer, emit warnings for unsupported default targets, and keep `rgb info` machine-readable with a per-zone array plus a compatibility summary.
+
+**Tech Stack:** Go 1.26+, Cobra, existing Linux hidraw/QMK v12 transport, table-driven Go tests, fake transport, hardware smoke tests.
+
+**Spec:** `docs/superpowers/specs/2026-09-25-impact80-effects-design.md`
+
+## Global Constraints
+
+- Target zones are `logo`, `backlight`, and `side`, mapped to VIA channels 2, 3, and 4.
+- Without `--zone`, commands target all three zones in Logo, Backlight, Side order.
+- With `--zone`, commands target exactly one zone and invalid values fail before opening HID.
+- Unknown effects fail before opening HID; explicit unsupported effects fail without a report; default unsupported effects are skipped with a stderr warning.
+- All writes use QMK v12 custom set `0x07`, 32-byte payloads, and validated acknowledgements.
+- Existing top-level `rgb info` fields remain a summary of the first selected zone.
+- No EEPROM save command, macOS/Windows backend, or Rainy 75 profile is added.
+- Follow TDD: each behavior gets a failing test, a minimal implementation, a passing test, and a commit.
+
+## Review Focus
+
+- A backlight-only effect sent without `--zone` must never be sent to Logo or Side.
+- An explicit unsupported zone/effect combination must produce zero HID writes.
+- A malformed `--zone` value must fail before device discovery.
+- `rgb info` must return valid JSON even when one selected zone cannot be queried.
+- Every canonical Backlight ID 0–45 must map to the documented name, including distinct IDs 39 `multisplash` and 41 `solid_multisplash`.
+
+---
+
+### Task 1: Build the Zone and Effect Profile Domain
+
+**Files:**
+- Create: `internal/rgb/impact80.go`
+- Create: `internal/rgb/impact80_test.go`
+- Modify: `internal/rgb/effects.go` (remove the partial `Impact80EffectName`/`ParseImpact80Effect` implementation after the new API owns it)
+- Modify: `internal/rgb/effects_test.go` (retain HSV/color/QMK value tests; remove `TestImpact80EffectName` and `TestParseImpact80Effect`, which move to `impact80_test.go`)
+
+**Interfaces:**
+- Produces `type Zone string`, `AllZones() []Zone`, `ParseZone(string) (Zone, error)`, `(Zone).Channel() uint8`.
+- Produces `type EffectTarget struct { Zone Zone; ID uint8 }`.
+- Produces `ResolveEffect(name string, zones []Zone) ([]EffectTarget, []Zone, error)`, `EffectName(zone Zone, id uint8) string`, and `DefaultEffect(zone Zone) uint8`.
+- The resolver returns supported targets in the caller's zone order, skipped zones separately, and an error for unknown names. A one-zone request is treated as explicit and errors when unsupported; a three-zone request is treated as the default and skips unsupported zones.
+
+- [ ] **Step 1: Write the failing zone/catalog tests**
+
+Create table-driven tests with literal expectations:
+
+```go
+func TestAllImpact80Zones(t *testing.T) {
+	want := []Zone{ZoneLogo, ZoneBacklight, ZoneSide}
+	got := AllZones()
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("AllZones() = %v, want %v", got, want)
+	}
+}
+
+func TestImpact80ZoneChannels(t *testing.T) {
+	cases := []struct { zone Zone; want uint8 }{
+		{ZoneLogo, 2}, {ZoneBacklight, 3}, {ZoneSide, 4},
+	}
+	for _, tc := range cases {
+		if got := tc.zone.Channel(); got != tc.want {
+			t.Errorf("%s.Channel() = %d, want %d", tc.zone, got, tc.want)
+		}
+	}
+}
+
+func TestResolveBacklightOnlyEffect(t *testing.T) {
+	targets, skipped, err := ResolveEffect("rainbow_moving_chevron", AllZones())
+	if err != nil { t.Fatal(err) }
+	if len(targets) != 1 || targets[0] != (EffectTarget{ZoneBacklight, 17}) { ... }
+	if !reflect.DeepEqual(skipped, []Zone{ZoneLogo, ZoneSide}) { ... }
+}
+```
+
+Add a complete 0–45 Backlight table test, the seven Logo/Side values, aliases (`off`, `breathe`, `rainbow`, `rainbow_wave`, `solid`), explicit unsupported behavior, and `EffectName`/`DefaultEffect` tests.
+
+- [ ] **Step 2: Run the focused tests and verify RED**
+
+Run: `go test ./internal/rgb -run 'Test(AllImpact80|Impact80|Resolve)' -count=1`
+Expected: compile failure because `Zone`, `ResolveEffect`, and related functions do not exist yet.
+
+- [ ] **Step 3: Implement the typed profile and resolver**
+
+Implement `Zone`, channel mapping, canonical profile tables, alias resolution, and `ResolveEffect`. Use a table for every Backlight ID 0–45; do not derive names from numeric ranges. `ResolveEffect` must return zero targets plus an error for an unknown name, and must return an error for an explicit zone that does not support the requested name.
+
+- [ ] **Step 4: Run focused tests and verify GREEN**
+
+Run: `go test ./internal/rgb -run 'Test(AllImpact80|Impact80|Resolve)' -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit the domain profile**
+
+```bash
+git add internal/rgb/impact80.go internal/rgb/impact80_test.go internal/rgb/effects.go internal/rgb/effects_test.go
+git commit -m "Add Impact 80 zone effect profiles"
+```
+
+### Task 2: Add Zone Selection to the RGB Command Layer
+
+**Files:**
+- Modify: `cmd/wobkey/rgb/rgb.go:12-73`
+- Modify: `cmd/wobkey/rgb/rgb_test.go`
+- Create: `cmd/wobkey/rgb/zones.go` (small target resolver helper)
+
+**Interfaces:**
+- Adds persistent `--zone` flag with values `logo`, `backlight`, and `side`.
+- Produces `selectedZones() ([]intrgb.Zone, error)` and `zoneChannels([]intrgb.Zone) []via.LEDType` for all commands.
+- Validation occurs before `OpenDevice()`.
+
+- [ ] **Step 1: Write failing target-selection tests**
+
+```go
+func TestSelectedZonesDefaultsToAll(t *testing.T) {
+	old := targetZone
+	defer func() { targetZone = old }()
+	targetZone = ""
+	got, err := selectedZones()
+	// assert Logo, Backlight, Side
+}
+
+func TestSelectedZonesRejectsUnknownZone(t *testing.T) {
+	targetZone = "matrix"
+	if _, err := selectedZones(); err == nil { t.Fatal(...) }
+}
+```
+
+Also test explicit Logo/Backlight/Side and channel conversion `[2,3,4]`.
+
+- [ ] **Step 2: Run the focused tests and verify RED**
+
+Run: `go test ./cmd/wobkey/rgb -run 'TestSelectedZones|TestZone' -count=1`
+Expected: compile failure because `selectedZones` and `zoneChannels` do not exist.
+
+- [ ] **Step 3: Implement the persistent flag and resolver**
+
+Add `targetZone`, register `PersistentFlags().StringVar`, parse through `intrgb.ParseZone`, return `intrgb.AllZones()` for an empty value, and convert channel IDs to `via.LEDType` without importing CLI policy into `internal/via`.
+
+- [ ] **Step 4: Run focused tests and verify GREEN**
+
+Run: `go test ./cmd/wobkey/rgb -run 'TestSelectedZones|TestZone|TestDeviceFlag' -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit zone selection**
+
+```bash
+git add cmd/wobkey/rgb/rgb.go cmd/wobkey/rgb/zones.go cmd/wobkey/rgb/rgb_test.go
+git commit -m "Add RGB zone selection"
+```
+
+### Task 3: Make Effect Resolution Zone-Aware
+
+**Files:**
+- Modify: `cmd/wobkey/rgb/effect.go`
+- Create: `cmd/wobkey/rgb/effect_test.go`
+- Modify: `internal/via/protocol_test.go`
+
+**Interfaces:**
+- Consumes `selectedZones`, `intrgb.ResolveEffect`, and `via.SetValue`.
+- Produces `resolveEffectTargets(name string, zones []intrgb.Zone) ([]intrgb.EffectTarget, []intrgb.Zone, error)` for command-policy tests and the Cobra command.
+- Produces one Effect report for each supported target; default unsupported zones generate a warning; explicit unsupported zones return an error before opening HID.
+
+- [ ] **Step 1: Write failing command-policy tests**
+
+Test the pure command policy with a fake resolver or extracted helper, not a real device. Assert:
+
+```go
+targets, skipped, err := resolveEffectTargets("rainbow_moving_chevron", intrgb.AllZones())
+// target channel 3/id 17, skipped Logo and Side
+```
+
+Also assert `breathing` targets IDs 4, 5, 4 and that explicit `backlight` rejects a Logo-only effect.
+
+- [ ] **Step 2: Run the focused tests and verify RED**
+
+Run: `go test ./cmd/wobkey/rgb -run 'TestResolveEffectTargets' -count=1`
+Expected: compile failure because the command policy helper is missing.
+
+- [ ] **Step 3: Implement effect command policy**
+
+Validate zones and effect names before `OpenDevice`. Print a warning such as `effect not supported on zone(s): logo, side` to stderr when a default target is skipped. For each `EffectTarget`, call `proto.SetValue(via.LEDType(target.Zone.Channel()), uint8(intrgb.EffectID), target.ID)`. Preserve the success message only after all writes succeed.
+
+- [ ] **Step 4: Run focused tests and verify GREEN**
+
+Run: `go test ./cmd/wobkey/rgb -run 'TestResolveEffectTargets' -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit effect policy**
+
+```bash
+git add cmd/wobkey/rgb/effect.go cmd/wobkey/rgb/effect_test.go
+git commit -m "Apply effects per lighting zone"
+```
+
+### Task 4: Route All RGB Commands Through the Selected Zones
+
+**Files:**
+- Modify: `cmd/wobkey/rgb/brightness.go`
+- Modify: `cmd/wobkey/rgb/speed.go`
+- Modify: `cmd/wobkey/rgb/color.go`
+- Modify: `cmd/wobkey/rgb/disable.go`
+- Modify: `cmd/wobkey/rgb/enable.go`
+- Modify: `cmd/wobkey/rgb/mode.go`
+- Modify: `cmd/wobkey/rgb/rgb.go` (shared iteration helper)
+- Create: `cmd/wobkey/rgb/commands_test.go`
+
+**Interfaces:**
+- `brightness`, `speed`, and `color` send to every selected channel.
+- `disable` sends Effect `0` then Brightness `0` to every selected channel.
+- `enable` sends `intrgb.DefaultEffect(zone)` then Brightness `160` to every selected channel.
+- `mode <index>` sends the raw numeric ID to every selected channel.
+
+- [ ] **Step 1: Write failing selected-channel tests**
+
+Use the existing fake transport pattern to assert that a default operation produces reports for channels 2, 3, and 4, while `--zone side` produces only channel 4. Cover brightness, speed, color, disable, enable, and mode with table-driven expectations.
+
+- [ ] **Step 2: Run the focused tests and verify RED**
+
+Run: `go test ./cmd/wobkey/rgb -run 'TestSelectedChannel' -count=1`
+Expected: failure because commands still hard-code `RGBLight` or the test helper is absent.
+
+- [ ] **Step 3: Implement selected-zone iteration**
+
+Replace hard-coded channel calls with `zoneChannels(selectedZones)` and a small helper that stops on the first error. Do not duplicate the channel list in each command.
+
+- [ ] **Step 4: Run focused tests and verify GREEN**
+
+Run: `go test ./cmd/wobkey/rgb -run 'TestSelectedChannel' -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit command routing**
+
+```bash
+git add cmd/wobkey/rgb/brightness.go cmd/wobkey/rgb/speed.go cmd/wobkey/rgb/color.go cmd/wobkey/rgb/disable.go cmd/wobkey/rgb/enable.go cmd/wobkey/rgb/mode.go cmd/wobkey/rgb/rgb.go cmd/wobkey/rgb/commands_test.go
+git commit -m "Target all RGB commands by zone"
+```
+
+### Task 5: Expand `rgb info` to Per-Zone State
+
+**Files:**
+- Modify: `cmd/wobkey/rgb/info.go`
+- Modify: `cmd/wobkey/rgb/info_test.go` (new)
+- Modify: `internal/via/protocol_test.go` (add `TestGetValueReturnsTwoByteColor`)
+
+**Interfaces:**
+- Produces JSON with `zones` entries containing `zone`, `channel`, `enabled`, `effect`, `effectId`, `brightness`, `speed`, and `color`.
+- Retains top-level `enabled`, `mode`, `brightness`, and `speed` from the first selected zone.
+- Emits an `error` field for a zone that cannot be queried and exits non-zero after serializing available results.
+
+- [ ] **Step 1: Write failing JSON-shape tests**
+
+Test a successful zone record and an error record with hand-written expected structs/JSON keys. Assert the top-level summary comes from the first selected zone, not an implicit hard-coded channel.
+
+- [ ] **Step 2: Run the focused tests and verify RED**
+
+Run: `go test ./cmd/wobkey/rgb -run 'TestInfo' -count=1`
+Expected: compile failure because the zone info model/helper does not exist.
+
+- [ ] **Step 3: Implement per-zone queries**
+
+For each selected channel, query Brightness, Effect, Speed, and Color (`0x01`, `0x02`, `0x03`, `0x04`). Use `intrgb.EffectName(zone, effectID)` and `lightingEnabled(effectID, brightness)`. Preserve valid JSON when one zone fails; collect errors, print JSON, then exit non-zero.
+
+- [ ] **Step 4: Run focused tests and verify GREEN**
+
+Run: `go test ./cmd/wobkey/rgb -run 'TestInfo' -count=1`
+Expected: PASS.
+
+- [ ] **Step 5: Commit info output**
+
+```bash
+git add cmd/wobkey/rgb/info.go cmd/wobkey/rgb/info_test.go internal/via/protocol_test.go
+git commit -m "Report RGB state for each zone"
+```
+
+### Task 6: Documentation, Full Verification, and Hardware Acceptance
+
+**Files:**
+- Modify: `README.md`
+- Modify: `PLAN.md`
+- Modify: `AGENTS.md`
+- No generated graph files are staged
+
+**Interfaces:**
+- Documents the three zones, default-all behavior, explicit-zone behavior, unsupported-effect warning semantics, and the complete effect-name families.
+
+- [ ] **Step 1: Update documentation examples**
+
+Document `rgb effect breathing`, `rgb effect rainbow_moving_chevron`, `rgb effect rainbow_moving_chevron --zone backlight`, and the default-all behavior. Remove any example that implies a generic effect ID is valid on every zone.
+
+- [ ] **Step 2: Run all automated checks**
+
+Run:
+
+```bash
+gofmt -w .
+go test -count=1 ./...
+go vet ./...
+go build -o /tmp/wobkey ./cmd/wobkey/
+```
+
+Expected: all commands exit 0 and `git diff --check` is empty.
+
+- [ ] **Step 3: Run hardware smoke tests**
+
+Run:
+
+```bash
+/tmp/wobkey keyboard info
+/tmp/wobkey rgb effect breathing
+/tmp/wobkey rgb effect rainbow_moving_chevron
+/tmp/wobkey rgb effect rainbow_moving_chevron --zone backlight
+/tmp/wobkey rgb info
+/tmp/wobkey rgb color 00ff00
+/tmp/wobkey rgb disable
+/tmp/wobkey rgb enable
+```
+
+Expected: the Impact 80 raw interface is selected, unsupported default targets produce warnings only, explicit Backlight works, JSON contains three zones, and the final state is enabled.
+
+- [ ] **Step 4: Commit documentation and final verified implementation**
+
+```bash
+git add README.md PLAN.md AGENTS.md
+git commit -m "Document zone-aware RGB commands"
+```
+
+Do not stage `graphify-out/` unless the user explicitly requests generated graph artifacts.