| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- package main
- import (
- "fmt"
- "strconv"
- "strings"
- intdevice "netdome.biz/paul/qmk-rgb/internal/device"
- intrgb "netdome.biz/paul/qmk-rgb/internal/rgb"
- "netdome.biz/paul/qmk-rgb/internal/via"
- )
- type zoneProtocol interface {
- SetValue(via.Channel, uint8, uint8) error
- SetColor(via.Channel, uint8, uint8) error
- }
- type rgbProtocol interface {
- zoneProtocol
- GetValue(via.Channel, uint8) ([]byte, error)
- DetectChannels() ([]via.Channel, error)
- Close() error
- }
- // keyboardFor is a seam for tests; it reads the optional keyboards.json.
- var keyboardFor = intdevice.KeyboardFor
- // targetDeviceData is everything a command needs before it opens the keyboard.
- type targetDeviceData struct {
- Device intdevice.Device
- Display map[uint16]string
- // Requested is nil when no --zone was given, which means every channel
- // the keyboard has.
- Requested []via.Channel
- }
- // prepareTarget resolves the keyboard, its display names and the requested
- // channels. Enumeration does not open a HID handle, so an unusable --zone value
- // is still rejected before the device is opened.
- func prepareTarget() (targetDeviceData, error) {
- devices, err := discoverAll()
- if err != nil {
- return targetDeviceData{}, fmt.Errorf("discover: %w", err)
- }
- dev, err := selectDevice(devices, targetDevice)
- if err != nil {
- return targetDeviceData{}, err
- }
- keyboard, _, err := keyboardFor(dev.VendorID, dev.ProductID)
- if err != nil {
- return targetDeviceData{}, err
- }
- requested, err := resolveZoneName(targetZone, keyboard.Channels)
- if err != nil {
- return targetDeviceData{}, err
- }
- return targetDeviceData{Device: dev, Display: keyboard.Channels, Requested: requested}, 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 nil
- }
- 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)
- })
- }
- // 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 {
- Name string
- 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.Name, r.Applied)
- }
- if len(results) > 0 {
- fmt.Fprintf(&b, " (requested %d)", results[0].Requested)
- }
- return b.String()
- }
- // readBackValues reads one value ID from every channel, in the order given.
- // Every verified set reads back through this, so a parameter that needs no new
- // read-back path reuses it.
- func readBackValues(proto rgbProtocol, channels []via.Channel, display map[uint16]string, param uint8) ([]uint8, error) {
- values := make([]uint8, 0, len(channels))
- for _, ch := range channels {
- raw, err := proto.GetValue(ch, param)
- if err != nil {
- return nil, fmt.Errorf("read back value 0x%02x for %s: %w", param, channelName(ch, display), err)
- }
- if len(raw) == 0 {
- return nil, fmt.Errorf("read back value 0x%02x for %s: empty response", param, channelName(ch, display))
- }
- values = append(values, raw[0])
- }
- return values, nil
- }
- // setValueVerified writes one value ID to every zone and reads each back, so a
- // 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
- // is not optional for any parameter the keyboard rescales.
- 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
- }
- applied, err := readBackValues(proto, channels, display, param)
- if err != nil {
- return nil, err
- }
- results := make([]zoneResult, 0, len(channels))
- for i, ch := range channels {
- results = append(results, zoneResult{Name: channelName(ch, display), Requested: value, Applied: applied[i]})
- }
- return results, nil
- }
- // effectResult records the effect a zone was asked for and the one the keyboard
- // holds afterwards. The Impact 80's logo and side channels do not take effect ID
- // 0: the firmware reads it as "lighting off" and leaves the mode register where
- // it was, so the effect that is still running has to be named rather than
- // reported as the requested one.
- type effectResult struct {
- Name string
- RequestedName string
- RequestedID uint8
- AppliedName string
- AppliedID uint8
- }
- // Mismatch reports whether the keyboard holds an effect other than the one
- // requested.
- func (r effectResult) Mismatch() bool { return r.AppliedID != r.RequestedID }
- // anyEffectMismatch reports whether any zone kept an effect of its own.
- func anyEffectMismatch(results []effectResult) bool {
- for _, r := range results {
- if r.Mismatch() {
- return true
- }
- }
- return false
- }
- // formatEffectResults names the effect each selected zone actually holds, so an
- // effect the keyboard refused is visible instead of being summarised as the one
- // that was asked for.
- func formatEffectResults(requestedName string, results []effectResult) string {
- var b strings.Builder
- b.WriteString("Effect")
- for _, r := range results {
- fmt.Fprintf(&b, " %s %q (%d)", r.Name, r.AppliedName, r.AppliedID)
- }
- if len(results) > 0 {
- fmt.Fprintf(&b, " (requested %q (%d))", requestedName, results[0].RequestedID)
- }
- return b.String()
- }
- // setEffectVerified writes one effect ID per channel and reads every channel
- // back. It cannot reuse setValueVerified, because one effect name is a
- // different index on each subsystem, so the requested value is carried per
- // target while the read-back stays shared.
- func setEffectVerified(proto rgbProtocol, targets []intrgb.EffectTarget, display map[uint16]string, catalog *intrgb.Catalog) ([]effectResult, error) {
- channels := make([]via.Channel, 0, len(targets))
- requested := make(map[via.Channel]uint8, len(targets))
- for _, t := range targets {
- if err := proto.SetValue(t.Channel, uint8(intrgb.EffectID), t.ID); err != nil {
- return nil, fmt.Errorf("set effect on %s: %w", channelName(t.Channel, display), err)
- }
- channels = append(channels, t.Channel)
- requested[t.Channel] = t.ID
- }
- applied, err := readBackValues(proto, channels, display, uint8(intrgb.EffectID))
- if err != nil {
- return nil, err
- }
- results := make([]effectResult, 0, len(channels))
- for i, ch := range channels {
- results = append(results, effectResult{
- Name: channelName(ch, display),
- RequestedName: catalog.EffectName(ch, requested[ch]),
- RequestedID: requested[ch],
- AppliedName: catalog.EffectName(ch, applied[i]),
- AppliedID: applied[i],
- })
- }
- return results, nil
- }
- // 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 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)
- }
- // setModeVerified writes a raw effect index and reads every channel back. A
- // board that does not implement the index clamps it to the highest ID it holds
- // rather than refusing it, so the index the keyboard ended up with is the only
- // honest thing to print.
- func setModeVerified(proto rgbProtocol, channels []via.Channel, display map[uint16]string, value uint8) ([]zoneResult, error) {
- return setValueVerified(proto, channels, display, uint8(intrgb.EffectID), value)
- }
- // 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
- // read back: a keyboard that stored another saturation must not be reported as
- // having taken the requested color.
- type colorResult struct {
- Name string
- RequestedHue uint8
- RequestedSaturation uint8
- Hue uint8
- Saturation uint8
- }
- // Mismatch reports whether the keyboard stored something other than the
- // requested color.
- func (r colorResult) Mismatch() bool {
- return r.Hue != r.RequestedHue || r.Saturation != r.RequestedSaturation
- }
- // setColorVerified writes one hue and saturation to every channel and reads each
- // back, for the same reason setValueVerified exists: the command must report
- // what the keyboard holds, not what it was asked for.
- 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
- }
- results := make([]colorResult, 0, len(channels))
- for _, ch := range channels {
- raw, err := proto.GetValue(ch, uint8(intrgb.ColorValue))
- if err != nil {
- return nil, fmt.Errorf("read back color for %s: %w", channelName(ch, display), err)
- }
- if len(raw) < 2 {
- 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{
- Name: channelName(ch, display),
- RequestedHue: hue,
- RequestedSaturation: saturation,
- Hue: raw[0],
- Saturation: raw[1],
- })
- }
- return results, nil
- }
- 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 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 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 proto.SetValue(ch, uint8(intrgb.Brightness), 0)
- })
- }
- 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 proto.SetValue(ch, uint8(intrgb.Brightness), 160)
- })
- }
- func setModeOnChannels(proto zoneProtocol, channels []via.Channel, value uint8) error {
- return setValueOnChannels(proto, channels, uint8(intrgb.EffectID), value)
- }
- // 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
- }
- proto, err := via.New(target.Device)
- if err != nil {
- return nil, targetDeviceData{}, nil, fmt.Errorf("open protocol: %w", err)
- }
- channels, err := resolveChannels(proto, target)
- if err != nil {
- proto.Close()
- return nil, targetDeviceData{}, nil, err
- }
- return proto, target, channels, nil
- }
- // 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 {
- 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
- }
- 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 {
- 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
- }
|