2026-09-25-impact80-effects.md 15 KB

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:

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

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
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:

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
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
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
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:

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:

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

Execution Handoff — 2026-09-25

  • Tasks 1–5 and the documentation/automated-verification portions of Task 6 are implemented in feature/impact80-effects through commit 62852e7.
  • All task reviews and the final whole-branch code review are approved.
  • go test -count=1 ./..., go vet ./..., go build, gofmt, and git diff --check pass.
  • Task 6’s live hardware acceptance remains open. The last run discovered the Impact 80 at /dev/hidraw7, but the device was root-owned mode 600; it later disappeared, so the next operator must discover the current path.
  • Continue with the hardware procedure in PLAN.md, record the result, and merge the feature branch into main only after the three-zone JSON and final enabled state have been verified.