Просмотр исходного кода

Add Impact 80 zone effect profiles

Paul Klumpp 2 недель назад
Родитель
Сommit
e65b3fcb3d
4 измененных файлов с 531 добавлено и 92 удалено
  1. 0 42
      internal/rgb/effects.go
  2. 0 50
      internal/rgb/effects_test.go
  3. 226 0
      internal/rgb/impact80.go
  4. 305 0
      internal/rgb/impact80_test.go

+ 0 - 42
internal/rgb/effects.go

@@ -110,48 +110,6 @@ func ParseEffect(name string) (Effect, error) {
 	}
 }
 
-func Impact80EffectName(value uint8) string {
-	switch value {
-	case 0:
-		return "none"
-	case 1:
-		return "wave"
-	case 2:
-		return "fixed_wave"
-	case 3:
-		return "spectrum"
-	case 4:
-		return "breathing"
-	case 5:
-		return "light"
-	case 6:
-		return "shutdown"
-	default:
-		return "unknown"
-	}
-}
-
-func ParseImpact80Effect(name string) (uint8, error) {
-	switch name {
-	case "none", "off":
-		return 0, nil
-	case "wave", "rainbow_wave":
-		return 1, nil
-	case "fixed_wave":
-		return 2, nil
-	case "spectrum", "rainbow":
-		return 3, nil
-	case "breathing", "breathe":
-		return 4, nil
-	case "light", "static", "solid":
-		return 5, nil
-	case "shutdown":
-		return 6, nil
-	default:
-		return 0, fmt.Errorf("unknown Impact 80 effect: %s", name)
-	}
-}
-
 // LEDParam defines QMK rgblight parameter indices.
 type LEDParam uint8
 

+ 0 - 50
internal/rgb/effects_test.go

@@ -4,56 +4,6 @@ import (
 	"testing"
 )
 
-func TestImpact80EffectName(t *testing.T) {
-	cases := []struct {
-		value uint8
-		want  string
-	}{
-		{0, "none"},
-		{1, "wave"},
-		{2, "fixed_wave"},
-		{3, "spectrum"},
-		{4, "breathing"},
-		{5, "light"},
-		{6, "shutdown"},
-		{7, "unknown"},
-	}
-
-	for _, tc := range cases {
-		if got := Impact80EffectName(tc.value); got != tc.want {
-			t.Errorf("Impact80EffectName(%d) = %q, want %q", tc.value, got, tc.want)
-		}
-	}
-}
-
-func TestParseImpact80Effect(t *testing.T) {
-	cases := []struct {
-		input string
-		want  uint8
-	}{
-		{"none", 0},
-		{"wave", 1},
-		{"fixed_wave", 2},
-		{"spectrum", 3},
-		{"breathing", 4},
-		{"light", 5},
-		{"shutdown", 6},
-		{"rainbow", 3},
-		{"static", 5},
-	}
-
-	for _, tc := range cases {
-		got, err := ParseImpact80Effect(tc.input)
-		if err != nil {
-			t.Errorf("ParseImpact80Effect(%q) unexpected error: %v", tc.input, err)
-			continue
-		}
-		if got != tc.want {
-			t.Errorf("ParseImpact80Effect(%q) = %d, want %d", tc.input, got, tc.want)
-		}
-	}
-}
-
 func TestQMKValueIDs(t *testing.T) {
 	if Brightness != 1 {
 		t.Errorf("Brightness = %d, want 1", Brightness)

+ 226 - 0
internal/rgb/impact80.go

@@ -0,0 +1,226 @@
+package rgb
+
+import "fmt"
+
+type Zone string
+
+const (
+	ZoneLogo      Zone = "logo"
+	ZoneBacklight Zone = "backlight"
+	ZoneSide      Zone = "side"
+)
+
+type EffectTarget struct {
+	Zone Zone
+	ID   uint8
+}
+
+var impact80BacklightEffects = [...]string{
+	"none",
+	"solid_color",
+	"alphas_mods",
+	"gradient_up_down",
+	"gradient_left_right",
+	"breathing",
+	"band_sat",
+	"band_val",
+	"band_pinwheel_sat",
+	"band_pinwheel_val",
+	"band_spiral_sat",
+	"band_spiral_val",
+	"cycle_all",
+	"cycle_left_right",
+	"cycle_up_down",
+	"cycle_out_in",
+	"cycle_out_in_dual",
+	"rainbow_moving_chevron",
+	"cycle_pinwheel",
+	"cycle_spiral",
+	"dual_beacon",
+	"rainbow_beacon",
+	"rainbow_pinwheels",
+	"flower_blooming",
+	"raindrops",
+	"jellybean_raindrops",
+	"hue_breathing",
+	"hue_pendulum",
+	"hue_wave",
+	"pixel_flow",
+	"digital_rain",
+	"solid_reactive",
+	"solid_reactive_wide",
+	"solid_reactive_multiwide",
+	"solid_reactive_cross",
+	"solid_reactive_multicross",
+	"solid_reactive_nexus",
+	"solid_reactive_multinexus",
+	"splash",
+	"multisplash",
+	"solid_splash",
+	"solid_multisplash",
+	"starlight",
+	"starlight_dual_hue",
+	"starlight_dual_sat",
+	"riverflow",
+}
+
+var impact80LogoEffects = [...]string{
+	"none",
+	"wave",
+	"fixed_wave",
+	"spectrum",
+	"breathing",
+	"light",
+	"shutdown",
+}
+
+var impact80SideEffects = [...]string{
+	"none",
+	"wave",
+	"fixed_wave",
+	"spectrum",
+	"breathing",
+	"light",
+	"shutdown",
+}
+
+var impact80EffectAliases = map[Zone]map[string]string{
+	ZoneLogo: {
+		"off":          "none",
+		"breathe":      "breathing",
+		"rainbow":      "spectrum",
+		"rainbow_wave": "wave",
+		"solid":        "light",
+	},
+	ZoneBacklight: {
+		"off":     "none",
+		"breathe": "breathing",
+		"rainbow": "rainbow_moving_chevron",
+		"solid":   "solid_color",
+	},
+	ZoneSide: {
+		"off":          "none",
+		"breathe":      "breathing",
+		"rainbow":      "spectrum",
+		"rainbow_wave": "wave",
+		"solid":        "light",
+	},
+}
+
+func AllZones() []Zone {
+	return []Zone{ZoneLogo, ZoneBacklight, ZoneSide}
+}
+
+func ParseZone(value string) (Zone, error) {
+	zone := Zone(value)
+	switch zone {
+	case ZoneLogo, ZoneBacklight, ZoneSide:
+		return zone, nil
+	default:
+		return "", fmt.Errorf("unknown zone: %s", value)
+	}
+}
+
+func (z Zone) Channel() uint8 {
+	switch z {
+	case ZoneLogo:
+		return 2
+	case ZoneBacklight:
+		return 3
+	case ZoneSide:
+		return 4
+	default:
+		return 0
+	}
+}
+
+func EffectName(zone Zone, id uint8) string {
+	names := impact80EffectNames(zone)
+	if int(id) >= len(names) {
+		return "unknown"
+	}
+	return names[id]
+}
+
+func DefaultEffect(zone Zone) uint8 {
+	switch zone {
+	case ZoneLogo, ZoneSide:
+		return 4
+	case ZoneBacklight:
+		return 5
+	default:
+		return 0
+	}
+}
+
+func ResolveEffect(name string, zones []Zone) ([]EffectTarget, []Zone, error) {
+	for _, zone := range zones {
+		if impact80EffectNames(zone) == nil {
+			return nil, nil, fmt.Errorf("unknown zone: %s", zone)
+		}
+	}
+
+	known := false
+	for _, zone := range AllZones() {
+		if _, ok := resolveEffectID(zone, name); ok {
+			known = true
+			break
+		}
+	}
+	if !known {
+		return nil, nil, fmt.Errorf("unknown effect: %s", name)
+	}
+
+	explicit := len(zones) == 1
+	var targets []EffectTarget
+	var skipped []Zone
+	for _, zone := range zones {
+		id, ok := resolveEffectID(zone, name)
+		if !ok {
+			if explicit {
+				return nil, nil, fmt.Errorf("effect %s is not supported on zone %s", name, zone)
+			}
+			skipped = append(skipped, zone)
+			continue
+		}
+		targets = append(targets, EffectTarget{Zone: zone, ID: id})
+	}
+	return targets, skipped, nil
+}
+
+func impact80EffectNames(zone Zone) []string {
+	switch zone {
+	case ZoneLogo:
+		return impact80LogoEffects[:]
+	case ZoneBacklight:
+		return impact80BacklightEffects[:]
+	case ZoneSide:
+		return impact80SideEffects[:]
+	default:
+		return nil
+	}
+}
+
+func resolveEffectID(zone Zone, name string) (uint8, bool) {
+	for id, effectName := range impact80EffectNames(zone) {
+		if effectName == name {
+			return uint8(id), true
+		}
+	}
+	if canonical, ok := impact80EffectAliases[zone][name]; ok {
+		return resolveEffectID(zone, canonical)
+	}
+	return 0, false
+}
+
+func Impact80EffectName(value uint8) string {
+	return EffectName(ZoneLogo, value)
+}
+
+func ParseImpact80Effect(name string) (uint8, error) {
+	targets, _, err := ResolveEffect(name, []Zone{ZoneLogo})
+	if err != nil {
+		return 0, err
+	}
+	return targets[0].ID, nil
+}

+ 305 - 0
internal/rgb/impact80_test.go

@@ -0,0 +1,305 @@
+package rgb
+
+import (
+	"reflect"
+	"testing"
+)
+
+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 TestImpact80ParseZone(t *testing.T) {
+	cases := []struct {
+		input string
+		want  Zone
+		valid bool
+	}{
+		{"logo", ZoneLogo, true},
+		{"backlight", ZoneBacklight, true},
+		{"side", ZoneSide, true},
+		{"matrix", "", false},
+		{"", "", false},
+	}
+	for _, tc := range cases {
+		got, err := ParseZone(tc.input)
+		if tc.valid {
+			if err != nil {
+				t.Errorf("ParseZone(%q) unexpected error: %v", tc.input, err)
+				continue
+			}
+			if got != tc.want {
+				t.Errorf("ParseZone(%q) = %q, want %q", tc.input, got, tc.want)
+			}
+			continue
+		}
+		if err == nil {
+			t.Errorf("ParseZone(%q) expected error, got nil", tc.input)
+		}
+	}
+}
+
+func TestImpact80BacklightCatalog(t *testing.T) {
+	want := []string{
+		"none",
+		"solid_color",
+		"alphas_mods",
+		"gradient_up_down",
+		"gradient_left_right",
+		"breathing",
+		"band_sat",
+		"band_val",
+		"band_pinwheel_sat",
+		"band_pinwheel_val",
+		"band_spiral_sat",
+		"band_spiral_val",
+		"cycle_all",
+		"cycle_left_right",
+		"cycle_up_down",
+		"cycle_out_in",
+		"cycle_out_in_dual",
+		"rainbow_moving_chevron",
+		"cycle_pinwheel",
+		"cycle_spiral",
+		"dual_beacon",
+		"rainbow_beacon",
+		"rainbow_pinwheels",
+		"flower_blooming",
+		"raindrops",
+		"jellybean_raindrops",
+		"hue_breathing",
+		"hue_pendulum",
+		"hue_wave",
+		"pixel_flow",
+		"digital_rain",
+		"solid_reactive",
+		"solid_reactive_wide",
+		"solid_reactive_multiwide",
+		"solid_reactive_cross",
+		"solid_reactive_multicross",
+		"solid_reactive_nexus",
+		"solid_reactive_multinexus",
+		"splash",
+		"multisplash",
+		"solid_splash",
+		"solid_multisplash",
+		"starlight",
+		"starlight_dual_hue",
+		"starlight_dual_sat",
+		"riverflow",
+	}
+	for id, name := range want {
+		if id > 45 {
+			t.Fatalf("test catalog has %d entries", len(want))
+		}
+		if got := EffectName(ZoneBacklight, uint8(id)); got != name {
+			t.Errorf("EffectName(%q, %d) = %q, want %q", ZoneBacklight, id, got, name)
+		}
+	}
+	if got := EffectName(ZoneBacklight, 46); got != "unknown" {
+		t.Errorf("EffectName(%q, 46) = %q, want %q", ZoneBacklight, got, "unknown")
+	}
+}
+
+func TestImpact80LogoAndSideCatalog(t *testing.T) {
+	want := []string{
+		"none",
+		"wave",
+		"fixed_wave",
+		"spectrum",
+		"breathing",
+		"light",
+		"shutdown",
+	}
+	for _, zone := range []Zone{ZoneLogo, ZoneSide} {
+		for id, name := range want {
+			if got := EffectName(zone, uint8(id)); got != name {
+				t.Errorf("EffectName(%q, %d) = %q, want %q", zone, id, got, name)
+			}
+		}
+		if got := EffectName(zone, 7); got != "unknown" {
+			t.Errorf("EffectName(%q, 7) = %q, want %q", zone, got, "unknown")
+		}
+	}
+	if got := EffectName(Zone("matrix"), 0); got != "unknown" {
+		t.Errorf("EffectName(%q, 0) = %q, want %q", "matrix", got, "unknown")
+	}
+}
+
+func TestImpact80DefaultEffects(t *testing.T) {
+	cases := []struct {
+		zone Zone
+		want uint8
+	}{
+		{ZoneLogo, 4},
+		{ZoneBacklight, 5},
+		{ZoneSide, 4},
+		{Zone("matrix"), 0},
+	}
+	for _, tc := range cases {
+		if got := DefaultEffect(tc.zone); got != tc.want {
+			t.Errorf("DefaultEffect(%q) = %d, want %d", tc.zone, got, tc.want)
+		}
+	}
+}
+
+func TestImpact80ResolveAliases(t *testing.T) {
+	cases := []struct {
+		name    string
+		targets []EffectTarget
+		skipped []Zone
+	}{
+		{
+			name: "off",
+			targets: []EffectTarget{
+				{ZoneLogo, 0},
+				{ZoneBacklight, 0},
+				{ZoneSide, 0},
+			},
+		},
+		{
+			name: "breathe",
+			targets: []EffectTarget{
+				{ZoneLogo, 4},
+				{ZoneBacklight, 5},
+				{ZoneSide, 4},
+			},
+		},
+		{
+			name: "rainbow",
+			targets: []EffectTarget{
+				{ZoneLogo, 3},
+				{ZoneBacklight, 17},
+				{ZoneSide, 3},
+			},
+		},
+		{
+			name: "rainbow_wave",
+			targets: []EffectTarget{
+				{ZoneLogo, 1},
+				{ZoneSide, 1},
+			},
+			skipped: []Zone{ZoneBacklight},
+		},
+		{
+			name: "solid",
+			targets: []EffectTarget{
+				{ZoneLogo, 5},
+				{ZoneBacklight, 1},
+				{ZoneSide, 5},
+			},
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			targets, skipped, err := ResolveEffect(tc.name, AllZones())
+			if err != nil {
+				t.Fatalf("ResolveEffect(%q) unexpected error: %v", tc.name, err)
+			}
+			if !reflect.DeepEqual(targets, tc.targets) {
+				t.Errorf("ResolveEffect(%q) targets = %v, want %v", tc.name, targets, tc.targets)
+			}
+			if !reflect.DeepEqual(skipped, tc.skipped) {
+				t.Errorf("ResolveEffect(%q) skipped = %v, want %v", tc.name, skipped, tc.skipped)
+			}
+		})
+	}
+}
+
+func TestResolveBacklightOnlyEffect(t *testing.T) {
+	targets, skipped, err := ResolveEffect("rainbow_moving_chevron", AllZones())
+	if err != nil {
+		t.Fatal(err)
+	}
+	wantTargets := []EffectTarget{{ZoneBacklight, 17}}
+	if !reflect.DeepEqual(targets, wantTargets) {
+		t.Fatalf("ResolveEffect() targets = %v, want %v", targets, wantTargets)
+	}
+	wantSkipped := []Zone{ZoneLogo, ZoneSide}
+	if !reflect.DeepEqual(skipped, wantSkipped) {
+		t.Fatalf("ResolveEffect() skipped = %v, want %v", skipped, wantSkipped)
+	}
+}
+
+func TestResolveEffectPreservesZoneOrder(t *testing.T) {
+	zones := []Zone{ZoneSide, ZoneBacklight, ZoneLogo}
+	want := []EffectTarget{
+		{ZoneSide, 4},
+		{ZoneBacklight, 5},
+		{ZoneLogo, 4},
+	}
+	targets, skipped, err := ResolveEffect("breathing", zones)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !reflect.DeepEqual(targets, want) {
+		t.Fatalf("ResolveEffect() targets = %v, want %v", targets, want)
+	}
+	if len(skipped) != 0 {
+		t.Fatalf("ResolveEffect() skipped = %v, want none", skipped)
+	}
+}
+
+func TestResolveEffectExplicitUnsupported(t *testing.T) {
+	cases := []struct {
+		name  string
+		zones []Zone
+	}{
+		{"rainbow_moving_chevron", []Zone{ZoneLogo}},
+		{"rainbow_wave", []Zone{ZoneBacklight}},
+		{"splash", []Zone{ZoneSide}},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name+string(tc.zones[0]), func(t *testing.T) {
+			targets, skipped, err := ResolveEffect(tc.name, tc.zones)
+			if err == nil {
+				t.Fatal("ResolveEffect() expected error, got nil")
+			}
+			if len(targets) != 0 {
+				t.Errorf("ResolveEffect() targets = %v, want none", targets)
+			}
+			if len(skipped) != 0 {
+				t.Errorf("ResolveEffect() skipped = %v, want none", skipped)
+			}
+		})
+	}
+}
+
+func TestResolveEffectUnknownName(t *testing.T) {
+	targets, skipped, err := ResolveEffect("not_an_effect", AllZones())
+	if err == nil {
+		t.Fatal("ResolveEffect() expected error, got nil")
+	}
+	if len(targets) != 0 {
+		t.Errorf("ResolveEffect() targets = %v, want none", targets)
+	}
+	if len(skipped) != 0 {
+		t.Errorf("ResolveEffect() skipped = %v, want none", skipped)
+	}
+}
+
+func TestResolveEffectInvalidZone(t *testing.T) {
+	if _, _, err := ResolveEffect("breathing", []Zone{Zone("matrix")}); err == nil {
+		t.Fatal("ResolveEffect() expected error, got nil")
+	}
+}