|
@@ -18,13 +18,10 @@ type zoneProtocol interface {
|
|
|
type rgbProtocol interface {
|
|
type rgbProtocol interface {
|
|
|
zoneProtocol
|
|
zoneProtocol
|
|
|
GetValue(via.Channel, uint8) ([]byte, error)
|
|
GetValue(via.Channel, uint8) ([]byte, error)
|
|
|
|
|
+ DetectChannels() ([]via.Channel, error)
|
|
|
Close() error
|
|
Close() error
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-var openRGBProtocol = func() (rgbProtocol, error) {
|
|
|
|
|
- return OpenDevice()
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// keyboardFor is a seam for tests; it reads the optional keyboards.json.
|
|
// keyboardFor is a seam for tests; it reads the optional keyboards.json.
|
|
|
var keyboardFor = intdevice.KeyboardFor
|
|
var keyboardFor = intdevice.KeyboardFor
|
|
|
|
|
|
|
@@ -64,32 +61,27 @@ func prepareTarget() (targetDeviceData, error) {
|
|
|
return targetDeviceData{Device: dev, Display: keyboard.Channels, Requested: requested}, nil
|
|
return targetDeviceData{Device: dev, Display: keyboard.Channels, Requested: requested}, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func forEachSelectedZone(zones []intrgb.Zone, fn func(intrgb.Zone, via.Channel) error) error {
|
|
|
|
|
- channels := zoneChannels(zones)
|
|
|
|
|
- for i, zone := range zones {
|
|
|
|
|
- if err := fn(zone, channels[i]); err != nil {
|
|
|
|
|
|
|
+func forEachChannel(channels []via.Channel, fn func(via.Channel) error) error {
|
|
|
|
|
+ for _, ch := range channels {
|
|
|
|
|
+ if err := fn(ch); err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func setValueOnZones(proto zoneProtocol, zones []intrgb.Zone, param, value uint8) error {
|
|
|
|
|
- return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.Channel) error {
|
|
|
|
|
- return proto.SetValue(channel, param, value)
|
|
|
|
|
|
|
+func setValueOnChannels(proto zoneProtocol, channels []via.Channel, param, value uint8) error {
|
|
|
|
|
+ return forEachChannel(channels, func(ch via.Channel) error {
|
|
|
|
|
+ return proto.SetValue(ch, param, value)
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func setBrightnessOnZones(proto zoneProtocol, zones []intrgb.Zone, value uint8) error {
|
|
|
|
|
- return setValueOnZones(proto, zones, uint8(intrgb.Brightness), value)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// zoneResult records what was requested for a zone and what the keyboard
|
|
// zoneResult records what was requested for a zone and what the keyboard
|
|
|
// reported afterwards. The two differ on real hardware: the Impact 80 clamps
|
|
// reported afterwards. The two differ on real hardware: the Impact 80 clamps
|
|
|
// brightness at 160 on the logo and side channels and scales it up to 255 on
|
|
// brightness at 160 on the logo and side channels and scales it up to 255 on
|
|
|
// the backlight channel.
|
|
// the backlight channel.
|
|
|
type zoneResult struct {
|
|
type zoneResult struct {
|
|
|
- Zone intrgb.Zone
|
|
|
|
|
|
|
+ Name string
|
|
|
Requested uint8
|
|
Requested uint8
|
|
|
Applied uint8
|
|
Applied uint8
|
|
|
}
|
|
}
|
|
@@ -115,7 +107,7 @@ func formatResults(label string, results []zoneResult) string {
|
|
|
var b strings.Builder
|
|
var b strings.Builder
|
|
|
b.WriteString(label)
|
|
b.WriteString(label)
|
|
|
for _, r := range results {
|
|
for _, r := range results {
|
|
|
- fmt.Fprintf(&b, " %s %d", r.Zone, r.Applied)
|
|
|
|
|
|
|
+ fmt.Fprintf(&b, " %s %d", r.Name, r.Applied)
|
|
|
}
|
|
}
|
|
|
if len(results) > 0 {
|
|
if len(results) > 0 {
|
|
|
fmt.Fprintf(&b, " (requested %d)", results[0].Requested)
|
|
fmt.Fprintf(&b, " (requested %d)", results[0].Requested)
|
|
@@ -127,41 +119,41 @@ func formatResults(label string, results []zoneResult) string {
|
|
|
// clamped or rescaled value is reported instead of silently claimed as set.
|
|
// clamped or rescaled value is reported instead of silently claimed as set.
|
|
|
// The firmware transform differs per channel and per value ID, so the read-back
|
|
// The firmware transform differs per channel and per value ID, so the read-back
|
|
|
// is not optional for any parameter the keyboard rescales.
|
|
// is not optional for any parameter the keyboard rescales.
|
|
|
-func setValueVerified(proto rgbProtocol, zones []intrgb.Zone, param, value uint8) ([]zoneResult, error) {
|
|
|
|
|
- if err := setValueOnZones(proto, zones, param, value); err != nil {
|
|
|
|
|
|
|
+func setValueVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, param, value uint8) ([]zoneResult, error) {
|
|
|
|
|
+ if err := setValueOnChannels(proto, channels, param, value); err != nil {
|
|
|
return nil, err
|
|
return nil, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- results := make([]zoneResult, 0, len(zones))
|
|
|
|
|
- for _, zone := range zones {
|
|
|
|
|
- raw, err := proto.GetValue(via.Channel(zone.Channel()), param)
|
|
|
|
|
|
|
+ results := make([]zoneResult, 0, len(channels))
|
|
|
|
|
+ for _, ch := range channels {
|
|
|
|
|
+ raw, err := proto.GetValue(ch, param)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("read back value 0x%02x for %s: %w", param, zone, err)
|
|
|
|
|
|
|
+ return nil, fmt.Errorf("read back value 0x%02x for %s: %w", param, channelName(ch, display), err)
|
|
|
}
|
|
}
|
|
|
if len(raw) == 0 {
|
|
if len(raw) == 0 {
|
|
|
- return nil, fmt.Errorf("read back value 0x%02x for %s: empty response", param, zone)
|
|
|
|
|
|
|
+ return nil, fmt.Errorf("read back value 0x%02x for %s: empty response", param, channelName(ch, display))
|
|
|
}
|
|
}
|
|
|
- results = append(results, zoneResult{Zone: zone, Requested: value, Applied: raw[0]})
|
|
|
|
|
|
|
+ results = append(results, zoneResult{Name: channelName(ch, display), Requested: value, Applied: raw[0]})
|
|
|
}
|
|
}
|
|
|
return results, nil
|
|
return results, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// setBrightnessVerified writes the brightness and reads every zone back.
|
|
|
|
|
-func setBrightnessVerified(proto rgbProtocol, zones []intrgb.Zone, value uint8) ([]zoneResult, error) {
|
|
|
|
|
- return setValueVerified(proto, zones, uint8(intrgb.Brightness), value)
|
|
|
|
|
|
|
+// setBrightnessVerified writes the brightness and reads every channel back.
|
|
|
|
|
+func setBrightnessVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, value uint8) ([]zoneResult, error) {
|
|
|
|
|
+ return setValueVerified(proto, channels, display, uint8(intrgb.Brightness), value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// setSpeedVerified writes the speed and reads every zone back.
|
|
|
|
|
-func setSpeedVerified(proto rgbProtocol, zones []intrgb.Zone, value uint8) ([]zoneResult, error) {
|
|
|
|
|
- return setValueVerified(proto, zones, uint8(intrgb.Speed), value)
|
|
|
|
|
|
|
+// setSpeedVerified writes the speed and reads every channel back.
|
|
|
|
|
+func setSpeedVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, value uint8) ([]zoneResult, error) {
|
|
|
|
|
+ return setValueVerified(proto, channels, display, uint8(intrgb.Speed), value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// colorResult records the hue and saturation one zone holds after a color was
|
|
|
|
|
|
|
+// colorResult records the hue and saturation one channel holds after a color was
|
|
|
// written to it. The color value ID carries two bytes, so both components are
|
|
// written to it. The color value ID carries two bytes, so both components are
|
|
|
// read back: a keyboard that stored another saturation must not be reported as
|
|
// read back: a keyboard that stored another saturation must not be reported as
|
|
|
// having taken the requested color.
|
|
// having taken the requested color.
|
|
|
type colorResult struct {
|
|
type colorResult struct {
|
|
|
- Zone intrgb.Zone
|
|
|
|
|
|
|
+ Name string
|
|
|
RequestedHue uint8
|
|
RequestedHue uint8
|
|
|
RequestedSaturation uint8
|
|
RequestedSaturation uint8
|
|
|
Hue uint8
|
|
Hue uint8
|
|
@@ -174,25 +166,25 @@ func (r colorResult) Mismatch() bool {
|
|
|
return r.Hue != r.RequestedHue || r.Saturation != r.RequestedSaturation
|
|
return r.Hue != r.RequestedHue || r.Saturation != r.RequestedSaturation
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// setColorVerified writes one hue and saturation to every zone and reads each
|
|
|
|
|
|
|
+// setColorVerified writes one hue and saturation to every channel and reads each
|
|
|
// back, for the same reason setValueVerified exists: the command must report
|
|
// back, for the same reason setValueVerified exists: the command must report
|
|
|
// what the keyboard holds, not what it was asked for.
|
|
// what the keyboard holds, not what it was asked for.
|
|
|
-func setColorVerified(proto rgbProtocol, zones []intrgb.Zone, hue, saturation uint8) ([]colorResult, error) {
|
|
|
|
|
- if err := setColorOnZones(proto, zones, hue, saturation); err != nil {
|
|
|
|
|
|
|
+func setColorVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, hue, saturation uint8) ([]colorResult, error) {
|
|
|
|
|
+ if err := setColorOnChannels(proto, channels, hue, saturation); err != nil {
|
|
|
return nil, err
|
|
return nil, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- results := make([]colorResult, 0, len(zones))
|
|
|
|
|
- for _, zone := range zones {
|
|
|
|
|
- raw, err := proto.GetValue(via.Channel(zone.Channel()), uint8(intrgb.ColorValue))
|
|
|
|
|
|
|
+ results := make([]colorResult, 0, len(channels))
|
|
|
|
|
+ for _, ch := range channels {
|
|
|
|
|
+ raw, err := proto.GetValue(ch, uint8(intrgb.ColorValue))
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("read back color for %s: %w", zone, err)
|
|
|
|
|
|
|
+ return nil, fmt.Errorf("read back color for %s: %w", channelName(ch, display), err)
|
|
|
}
|
|
}
|
|
|
if len(raw) < 2 {
|
|
if len(raw) < 2 {
|
|
|
- return nil, fmt.Errorf("read back color for %s: got %d bytes, want hue and saturation", zone, len(raw))
|
|
|
|
|
|
|
+ return nil, fmt.Errorf("read back color for %s: got %d bytes, want hue and saturation", channelName(ch, display), len(raw))
|
|
|
}
|
|
}
|
|
|
results = append(results, colorResult{
|
|
results = append(results, colorResult{
|
|
|
- Zone: zone,
|
|
|
|
|
|
|
+ Name: channelName(ch, display),
|
|
|
RequestedHue: hue,
|
|
RequestedHue: hue,
|
|
|
RequestedSaturation: saturation,
|
|
RequestedSaturation: saturation,
|
|
|
Hue: raw[0],
|
|
Hue: raw[0],
|
|
@@ -202,68 +194,109 @@ func setColorVerified(proto rgbProtocol, zones []intrgb.Zone, hue, saturation ui
|
|
|
return results, nil
|
|
return results, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func setSpeedOnZones(proto zoneProtocol, zones []intrgb.Zone, value uint8) error {
|
|
|
|
|
- return setValueOnZones(proto, zones, uint8(intrgb.Speed), value)
|
|
|
|
|
|
|
+func setColorOnChannels(proto zoneProtocol, channels []via.Channel, hue, saturation uint8) error {
|
|
|
|
|
+ return forEachChannel(channels, func(ch via.Channel) error {
|
|
|
|
|
+ return proto.SetColor(ch, hue, saturation)
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func setColorOnZones(proto zoneProtocol, zones []intrgb.Zone, hue, saturation uint8) error {
|
|
|
|
|
- return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.Channel) error {
|
|
|
|
|
- return proto.SetColor(channel, hue, saturation)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+func setBrightnessOnChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
|
|
|
|
|
+ return setValueOnChannels(proto, channels, uint8(intrgb.Brightness), value)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func setSpeedChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
|
|
|
|
|
+ return setValueOnChannels(proto, channels, uint8(intrgb.Speed), value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func disableLightingOnZones(proto zoneProtocol, zones []intrgb.Zone) error {
|
|
|
|
|
- return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.Channel) error {
|
|
|
|
|
- if err := proto.SetValue(channel, uint8(intrgb.EffectID), 0); err != nil {
|
|
|
|
|
|
|
+func disableLightingOnChannels(proto zoneProtocol, channels []via.Channel) error {
|
|
|
|
|
+ return forEachChannel(channels, func(ch via.Channel) error {
|
|
|
|
|
+ if err := proto.SetValue(ch, uint8(intrgb.EffectID), 0); err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
- return proto.SetValue(channel, uint8(intrgb.Brightness), 0)
|
|
|
|
|
|
|
+ return proto.SetValue(ch, uint8(intrgb.Brightness), 0)
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func enableLightingOnZones(proto zoneProtocol, zones []intrgb.Zone) error {
|
|
|
|
|
- return forEachSelectedZone(zones, func(zone intrgb.Zone, channel via.Channel) error {
|
|
|
|
|
- if err := proto.SetValue(channel, uint8(intrgb.EffectID), intrgb.DefaultEffect(zone)); err != nil {
|
|
|
|
|
|
|
+func enableLightingOnChannels(proto zoneProtocol, channels []via.Channel, catalog *intrgb.Catalog) error {
|
|
|
|
|
+ return forEachChannel(channels, func(ch via.Channel) error {
|
|
|
|
|
+ effect, ok := catalog.DefaultEffect(ch)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ return fmt.Errorf("no default effect for %s", channelName(ch, nil))
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := proto.SetValue(ch, uint8(intrgb.EffectID), effect); err != nil {
|
|
|
return err
|
|
return err
|
|
|
}
|
|
}
|
|
|
- return proto.SetValue(channel, uint8(intrgb.Brightness), 160)
|
|
|
|
|
|
|
+ return proto.SetValue(ch, uint8(intrgb.Brightness), 160)
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func setModeOnZones(proto zoneProtocol, zones []intrgb.Zone, value uint8) error {
|
|
|
|
|
- return setValueOnZones(proto, zones, uint8(intrgb.EffectID), value)
|
|
|
|
|
|
|
+func setModeOnChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
|
|
|
|
|
+ return setValueOnChannels(proto, channels, uint8(intrgb.EffectID), value)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// OpenDevice discovers the connected QMK keyboards and opens the one selected
|
|
|
|
|
-// by --device. Without --device it only proceeds when exactly one keyboard is
|
|
|
|
|
-// connected, so a command can never hit an unintended keyboard.
|
|
|
|
|
-func OpenDevice() (*via.Protocol, error) {
|
|
|
|
|
- if _, err := selectedZones(); err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
|
|
+// openTarget opens the keyboard and resolves the requested channels against the
|
|
|
|
|
+// ones it actually has. It is a seam because a command needs all three: the
|
|
|
|
|
+// handle it writes to, the names it reports with, and the channels it may touch.
|
|
|
|
|
+var openTarget = func() (rgbProtocol, targetDeviceData, []via.Channel, error) {
|
|
|
|
|
+ target, err := prepareTarget()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, targetDeviceData{}, nil, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- devices, err := intdevice.DiscoverAll()
|
|
|
|
|
|
|
+ proto, err := via.New(target.Device)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("discover: %w", err)
|
|
|
|
|
|
|
+ return nil, targetDeviceData{}, nil, fmt.Errorf("open protocol: %w", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- dev, err := selectDevice(devices, targetDevice)
|
|
|
|
|
|
|
+ channels, err := resolveChannels(proto, target)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, err
|
|
|
|
|
|
|
+ proto.Close()
|
|
|
|
|
+ return nil, targetDeviceData{}, nil, err
|
|
|
}
|
|
}
|
|
|
|
|
+ return proto, target, channels, nil
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- proto, err := via.New(dev)
|
|
|
|
|
|
|
+// resolveChannels intersects the requested channels with the detected ones, and
|
|
|
|
|
+// refuses a name that resolves to a channel this keyboard does not have.
|
|
|
|
|
+func resolveChannels(proto rgbProtocol, target targetDeviceData) ([]via.Channel, error) {
|
|
|
|
|
+ present, err := proto.DetectChannels()
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("open protocol: %w", err)
|
|
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if err := displayNameConflicts(target.Display, present); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if target.Requested == nil {
|
|
|
|
|
+ if len(present) == 0 {
|
|
|
|
|
+ return nil, fmt.Errorf("this keyboard exposes no VIA lighting channels")
|
|
|
|
|
+ }
|
|
|
|
|
+ return present, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return proto, nil
|
|
|
|
|
|
|
+ presentSet := make(map[via.Channel]bool, len(present))
|
|
|
|
|
+ for _, ch := range present {
|
|
|
|
|
+ presentSet[ch] = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var found []via.Channel
|
|
|
|
|
+ for _, ch := range target.Requested {
|
|
|
|
|
+ if presentSet[ch] {
|
|
|
|
|
+ found = append(found, ch)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(found) == 0 {
|
|
|
|
|
+ return nil, fmt.Errorf("channel %s is not present on this keyboard", target.Requested[0].Subsystem())
|
|
|
|
|
+ }
|
|
|
|
|
+ return found, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// lightingEnabled reports whether a channel is lit: a mode other than none and a
|
|
|
|
|
+// brightness above zero.
|
|
|
func lightingEnabled(mode, brightness uint8) bool {
|
|
func lightingEnabled(mode, brightness uint8) bool {
|
|
|
return mode != 0 && brightness > 0
|
|
return mode != 0 && brightness > 0
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
func ParseUint8(s string) (uint8, error) {
|
|
func ParseUint8(s string) (uint8, error) {
|
|
|
v, err := strconv.ParseUint(s, 10, 8)
|
|
v, err := strconv.ParseUint(s, 10, 8)
|
|
|
if err != nil {
|
|
if err != nil {
|