|
|
@@ -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.
|