Переглянути джерело

Report RGB state for each zone

Paul Klumpp 2 тижнів тому
батько
коміт
9128ea00a3
3 змінених файлів з 305 додано та 43 видалено
  1. 108 43
      cmd/wobkey/rgb/info.go
  2. 170 0
      cmd/wobkey/rgb/info_test.go
  3. 27 0
      internal/via/protocol_test.go

+ 108 - 43
cmd/wobkey/rgb/info.go

@@ -2,67 +2,132 @@ package rgb
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
-	"os"
 
 	"github.com/spf13/cobra"
-	"github.com/wobkey/rgb/internal/rgb"
+	intrgb "github.com/wobkey/rgb/internal/rgb"
 	"github.com/wobkey/rgb/internal/via"
 )
 
+type infoGetter interface {
+	GetValue(via.LEDType, uint8) ([]byte, error)
+}
+
+type infoColor struct {
+	Hue        uint8 `json:"hue"`
+	Saturation uint8 `json:"saturation"`
+}
+
+type zoneInfo struct {
+	Zone       intrgb.Zone `json:"zone"`
+	Channel    uint8       `json:"channel"`
+	Enabled    bool        `json:"enabled"`
+	Effect     string      `json:"effect"`
+	EffectID   uint8       `json:"effectId"`
+	Brightness uint8       `json:"brightness"`
+	Speed      uint8       `json:"speed"`
+	Color      infoColor   `json:"color"`
+	Error      string      `json:"error,omitempty"`
+}
+
+type infoOutput struct {
+	Enabled    bool       `json:"enabled"`
+	Mode       string     `json:"mode"`
+	Brightness uint8      `json:"brightness"`
+	Speed      uint8      `json:"speed"`
+	Zones      []zoneInfo `json:"zones"`
+}
+
+func readInfo(proto infoGetter, zones []intrgb.Zone) (infoOutput, error) {
+	out := infoOutput{Zones: make([]zoneInfo, 0, len(zones))}
+	var queryErrors []error
+	for _, zone := range zones {
+		record, err := readZoneInfo(proto, zone)
+		out.Zones = append(out.Zones, record)
+		if err != nil {
+			queryErrors = append(queryErrors, err)
+		}
+	}
+	if len(out.Zones) > 0 {
+		first := out.Zones[0]
+		out.Enabled = first.Enabled
+		out.Mode = first.Effect
+		out.Brightness = first.Brightness
+		out.Speed = first.Speed
+	}
+	return out, errors.Join(queryErrors...)
+}
+
+func readZoneInfo(proto infoGetter, zone intrgb.Zone) (zoneInfo, error) {
+	record := zoneInfo{Zone: zone, Channel: zone.Channel()}
+	channel := via.LEDType(zone.Channel())
+
+	brightness, err := getInfoValue(proto, channel, uint8(intrgb.Brightness), 1)
+	if err != nil {
+		record.Error = err.Error()
+		return record, err
+	}
+	record.Brightness = brightness[0]
+
+	effect, err := getInfoValue(proto, channel, uint8(intrgb.EffectID), 1)
+	if err != nil {
+		record.Error = err.Error()
+		return record, err
+	}
+	record.EffectID = effect[0]
+	record.Effect = intrgb.EffectName(zone, record.EffectID)
+	record.Enabled = lightingEnabled(record.EffectID, record.Brightness)
+
+	speed, err := getInfoValue(proto, channel, uint8(intrgb.Speed), 1)
+	if err != nil {
+		record.Error = err.Error()
+		return record, err
+	}
+	record.Speed = speed[0]
+
+	color, err := getInfoValue(proto, channel, uint8(intrgb.ColorValue), 2)
+	if err != nil {
+		record.Error = err.Error()
+		return record, err
+	}
+	record.Color = infoColor{Hue: color[0], Saturation: color[1]}
+	return record, nil
+}
+
+func getInfoValue(proto infoGetter, channel via.LEDType, param uint8, size int) ([]byte, error) {
+	value, err := proto.GetValue(channel, param)
+	if err != nil {
+		return nil, err
+	}
+	if len(value) < size {
+		return nil, fmt.Errorf("short value response: got %d, want %d", len(value), size)
+	}
+	return value, nil
+}
+
 func NewInfoCmd() *cobra.Command {
 	return &cobra.Command{
 		Use:   "info",
 		Short: "Show current RGB state",
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(_ *cobra.Command, args []string) error {
+			zones, err := selectedZones()
+			if err != nil {
+				return err
+			}
 			proto, _, err := OpenDevice()
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "Error: %v\n", err)
-				os.Exit(1)
+				return err
 			}
 			defer proto.Close()
 
-			getValue := func(param uint8) (uint8, error) {
-				value, err := proto.GetValue(via.RGBLight, param)
-				if err != nil {
-					return 0, err
-				}
-				if len(value) == 0 {
-					return 0, fmt.Errorf("empty value response")
-				}
-				return value[0], nil
-			}
-
-			brightness, err := getValue(uint8(rgb.Brightness))
+			out, queryErr := readInfo(proto, zones)
+			data, err := json.MarshalIndent(out, "", "  ")
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "Error getting brightness: %v\n", err)
-				os.Exit(1)
+				return err
 			}
-			speed, err := getValue(uint8(rgb.Speed))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "Error getting speed: %v\n", err)
-				os.Exit(1)
-			}
-			mode, err := getValue(uint8(rgb.EffectID))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "Error getting mode: %v\n", err)
-				os.Exit(1)
-			}
-
-			out := struct {
-				Enabled    bool   `json:"enabled"`
-				Mode       string `json:"mode"`
-				Brightness uint8  `json:"brightness"`
-				Speed      uint8  `json:"speed"`
-			}{
-				Enabled:    lightingEnabled(mode, brightness),
-				Mode:       rgb.Impact80EffectName(mode),
-				Brightness: brightness,
-				Speed:      speed,
-			}
-
-			data, _ := json.MarshalIndent(out, "", "  ")
 			fmt.Println(string(data))
+			return queryErr
 		},
 	}
 }

+ 170 - 0
cmd/wobkey/rgb/info_test.go

@@ -0,0 +1,170 @@
+package rgb
+
+import (
+	"encoding/json"
+	"errors"
+	"reflect"
+	"testing"
+
+	intrgb "github.com/wobkey/rgb/internal/rgb"
+	"github.com/wobkey/rgb/internal/via"
+)
+
+type infoKey struct {
+	channel via.LEDType
+	param   uint8
+}
+
+type fakeInfoProtocol struct {
+	values map[infoKey][]byte
+	errors map[infoKey]error
+	calls  []infoKey
+}
+
+func (f *fakeInfoProtocol) GetValue(channel via.LEDType, param uint8) ([]byte, error) {
+	key := infoKey{channel: channel, param: param}
+	f.calls = append(f.calls, key)
+	if err := f.errors[key]; err != nil {
+		return nil, err
+	}
+	return append([]byte(nil), f.values[key]...), nil
+}
+
+func TestInfoSuccessfulRecord(t *testing.T) {
+	protocol := &fakeInfoProtocol{
+		values: map[infoKey][]byte{
+			{channel: 3, param: 1}: {160},
+			{channel: 3, param: 2}: {17},
+			{channel: 3, param: 3}: {2},
+			{channel: 3, param: 4}: {85, 255},
+			{channel: 2, param: 1}: {80},
+			{channel: 2, param: 2}: {4},
+			{channel: 2, param: 3}: {1},
+			{channel: 2, param: 4}: {0, 128},
+		},
+		errors: make(map[infoKey]error),
+	}
+	zones := []intrgb.Zone{intrgb.ZoneBacklight, intrgb.ZoneLogo}
+
+	got, err := readInfo(protocol, zones)
+	if err != nil {
+		t.Fatalf("readInfo() unexpected error: %v", err)
+	}
+	want := infoOutput{
+		Enabled:    true,
+		Mode:       "rainbow_moving_chevron",
+		Brightness: 160,
+		Speed:      2,
+		Zones: []zoneInfo{
+			{
+				Zone:       intrgb.ZoneBacklight,
+				Channel:    3,
+				Enabled:    true,
+				Effect:     "rainbow_moving_chevron",
+				EffectID:   17,
+				Brightness: 160,
+				Speed:      2,
+				Color:      infoColor{Hue: 85, Saturation: 255},
+			},
+			{
+				Zone:       intrgb.ZoneLogo,
+				Channel:    2,
+				Enabled:    true,
+				Effect:     "breathing",
+				EffectID:   4,
+				Brightness: 80,
+				Speed:      1,
+				Color:      infoColor{Hue: 0, Saturation: 128},
+			},
+		},
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("readInfo() = %+v, want %+v", got, want)
+	}
+
+	data, err := json.Marshal(got)
+	if err != nil {
+		t.Fatalf("json.Marshal() error = %v", err)
+	}
+	wantJSON := `{"enabled":true,"mode":"rainbow_moving_chevron","brightness":160,"speed":2,"zones":[{"zone":"backlight","channel":3,"enabled":true,"effect":"rainbow_moving_chevron","effectId":17,"brightness":160,"speed":2,"color":{"hue":85,"saturation":255}},{"zone":"logo","channel":2,"enabled":true,"effect":"breathing","effectId":4,"brightness":80,"speed":1,"color":{"hue":0,"saturation":128}}]}`
+	if string(data) != wantJSON {
+		t.Errorf("JSON = %s, want %s", data, wantJSON)
+	}
+}
+
+func TestInfoErrorRecord(t *testing.T) {
+	protocol := &fakeInfoProtocol{
+		values: map[infoKey][]byte{
+			{channel: 2, param: 1}: {120},
+			{channel: 2, param: 2}: {4},
+			{channel: 2, param: 3}: {3},
+			{channel: 2, param: 4}: {10, 20},
+			{channel: 3, param: 1}: {90},
+			{channel: 3, param: 2}: {5},
+			{channel: 3, param: 3}: {4},
+			{channel: 3, param: 4}: {30, 40},
+		},
+		errors: map[infoKey]error{
+			{channel: 4, param: 1}: errors.New("side unavailable"),
+		},
+	}
+	zones := []intrgb.Zone{intrgb.ZoneLogo, intrgb.ZoneSide, intrgb.ZoneBacklight}
+
+	got, err := readInfo(protocol, zones)
+	if err == nil {
+		t.Fatal("readInfo() expected error, got nil")
+	}
+	want := infoOutput{
+		Enabled:    true,
+		Mode:       "breathing",
+		Brightness: 120,
+		Speed:      3,
+		Zones: []zoneInfo{
+			{
+				Zone:       intrgb.ZoneLogo,
+				Channel:    2,
+				Enabled:    true,
+				Effect:     "breathing",
+				EffectID:   4,
+				Brightness: 120,
+				Speed:      3,
+				Color:      infoColor{Hue: 10, Saturation: 20},
+			},
+			{
+				Zone:    intrgb.ZoneSide,
+				Channel: 4,
+				Error:   "side unavailable",
+			},
+			{
+				Zone:       intrgb.ZoneBacklight,
+				Channel:    3,
+				Enabled:    true,
+				Effect:     "breathing",
+				EffectID:   5,
+				Brightness: 90,
+				Speed:      4,
+				Color:      infoColor{Hue: 30, Saturation: 40},
+			},
+		},
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("readInfo() = %+v, want %+v", got, want)
+	}
+
+	data, err := json.Marshal(got)
+	if err != nil {
+		t.Fatalf("json.Marshal() error = %v", err)
+	}
+	var envelope struct {
+		Zones []map[string]json.RawMessage `json:"zones"`
+	}
+	if err := json.Unmarshal(data, &envelope); err != nil {
+		t.Fatalf("json.Unmarshal() error = %v", err)
+	}
+	if len(envelope.Zones) != 3 {
+		t.Fatalf("zones = %d, want 3", len(envelope.Zones))
+	}
+	if _, ok := envelope.Zones[1]["error"]; !ok {
+		t.Errorf("error zone JSON = %v, want error key", envelope.Zones[1])
+	}
+}

+ 27 - 0
internal/via/protocol_test.go

@@ -226,3 +226,30 @@ func TestSetValueWritesEachImpact80EffectChannel(t *testing.T) {
 		}
 	}
 }
+
+func TestGetValueReturnsTwoByteColor(t *testing.T) {
+	response := make([]byte, 32)
+	response[0] = 0x08
+	response[1] = 0x03
+	response[2] = 0x04
+	response[3] = 0x2a
+	response[4] = 0x80
+	transport := &fakeTransport{response: response}
+	protocol := Protocol{handle: transport}
+
+	got, err := protocol.GetValue(RGBMatrix, 0x04)
+	if err != nil {
+		t.Fatalf("GetValue() error = %v", err)
+	}
+	if !bytes.Equal(got, []byte{0x2a, 0x80}) {
+		t.Errorf("GetValue() = %v, want [42 128]", got)
+	}
+
+	want := make([]byte, 32)
+	want[0] = 0x08
+	want[1] = 0x03
+	want[2] = 0x04
+	if !bytes.Equal(transport.reports[0], want) {
+		t.Errorf("request = %v, want %v", transport.reports[0], want)
+	}
+}