package main import ( "fmt" "strconv" "strings" intdevice "netdome.biz/paul/impact-80/internal/device" intrgb "netdome.biz/paul/impact-80/internal/rgb" "netdome.biz/paul/impact-80/internal/via" ) type zoneProtocol interface { SetValue(via.LEDType, uint8, uint8) error SetColor(via.LEDType, uint8, uint8) error } type rgbProtocol interface { zoneProtocol GetValue(via.LEDType, uint8) ([]byte, error) Close() error } var openRGBProtocol = func() (rgbProtocol, error) { return OpenDevice() } func forEachSelectedZone(zones []intrgb.Zone, fn func(intrgb.Zone, via.LEDType) error) error { channels := zoneChannels(zones) for i, zone := range zones { if err := fn(zone, channels[i]); err != nil { return err } } return nil } func setValueOnZones(proto zoneProtocol, zones []intrgb.Zone, param, value uint8) error { return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.LEDType) error { return proto.SetValue(channel, 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 // 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 // the backlight channel. type zoneResult struct { Zone intrgb.Zone Requested uint8 Applied uint8 } // Mismatch reports whether the keyboard applied something other than the // requested value. func (r zoneResult) Mismatch() bool { return r.Applied != r.Requested } // anyMismatch reports whether any zone deviated from the request. func anyMismatch(results []zoneResult) bool { for _, r := range results { if r.Mismatch() { return true } } return false } // formatResults renders one line stating what each selected zone actually // holds, so a partial application is visible instead of being summarised as // the value that was asked for. func formatResults(label string, results []zoneResult) string { var b strings.Builder b.WriteString(label) for _, r := range results { fmt.Fprintf(&b, " %s %d", r.Zone, r.Applied) } if len(results) > 0 { fmt.Fprintf(&b, " (requested %d)", results[0].Requested) } return b.String() } // setBrightnessVerified writes the brightness and reads every zone back, so a // clamped or rescaled value is reported instead of silently claimed as set. func setBrightnessVerified(proto rgbProtocol, zones []intrgb.Zone, value uint8) ([]zoneResult, error) { if err := setBrightnessOnZones(proto, zones, value); err != nil { return nil, err } results := make([]zoneResult, 0, len(zones)) for _, zone := range zones { raw, err := proto.GetValue(via.LEDType(zone.Channel()), uint8(intrgb.Brightness)) if err != nil { return nil, fmt.Errorf("read back brightness for %s: %w", zone, err) } if len(raw) == 0 { return nil, fmt.Errorf("read back brightness for %s: empty response", zone) } results = append(results, zoneResult{Zone: zone, Requested: value, Applied: raw[0]}) } return results, nil } func setSpeedOnZones(proto zoneProtocol, zones []intrgb.Zone, value uint8) error { return setValueOnZones(proto, zones, uint8(intrgb.Speed), value) } func setColorOnZones(proto zoneProtocol, zones []intrgb.Zone, hue, saturation uint8) error { return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.LEDType) error { return proto.SetColor(channel, hue, saturation) }) } func disableLightingOnZones(proto zoneProtocol, zones []intrgb.Zone) error { return forEachSelectedZone(zones, func(_ intrgb.Zone, channel via.LEDType) error { if err := proto.SetValue(channel, uint8(intrgb.EffectID), 0); err != nil { return err } return proto.SetValue(channel, uint8(intrgb.Brightness), 0) }) } func enableLightingOnZones(proto zoneProtocol, zones []intrgb.Zone) error { return forEachSelectedZone(zones, func(zone intrgb.Zone, channel via.LEDType) error { if err := proto.SetValue(channel, uint8(intrgb.EffectID), intrgb.DefaultEffect(zone)); err != nil { return err } return proto.SetValue(channel, uint8(intrgb.Brightness), 160) }) } func setModeOnZones(proto zoneProtocol, zones []intrgb.Zone, value uint8) error { return setValueOnZones(proto, zones, 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 } devices, err := intdevice.DiscoverAll() if err != nil { return nil, fmt.Errorf("discover: %w", err) } dev, err := selectDevice(devices, targetDevice) if err != nil { return nil, err } proto, err := via.New(dev) if err != nil { return nil, fmt.Errorf("open protocol: %w", err) } return proto, nil } func lightingEnabled(mode, brightness uint8) bool { return mode != 0 && brightness > 0 } func ParseUint8(s string) (uint8, error) { v, err := strconv.ParseUint(s, 10, 8) if err != nil { return 0, fmt.Errorf("invalid value: %w", err) } return uint8(v), nil }