package via import ( "errors" "fmt" ) // Channel is a VIA lighting channel. The values are QMK's id_qmk_*_channel // constants from quantum/via.h. type Channel uint8 const ( ChannelBacklight Channel = 1 ChannelRgblight Channel = 2 ChannelRgbMatrix Channel = 3 ChannelAudio Channel = 4 ChannelLedMatrix Channel = 5 ) // AssignedChannelMax is the highest channel QMK currently assigns. The probe // asks beyond it on purpose, see probeChannelMax. const AssignedChannelMax = ChannelLedMatrix // probeChannelMax is the highest channel the probe asks about. The QMK enum // stops at 5; the extra reads cost one round trip each and let a future QMK // that claims a higher channel be found without a code change. const probeChannelMax = 15 // probeValueID is the brightness value ID. It is one byte and valid on every // lighting subsystem, so one request per channel finds any kind of channel. const probeValueID = 0x01 // Subsystem returns the QMK name of the channel's lighting subsystem. The name // follows from the channel number, so no board file stores it and it is always // available for a channel the keyboard has. // LightingChannels lists the QMK lighting channels in channel order, which is // the order the tool reports them in. var LightingChannels = []Channel{ChannelBacklight, ChannelRgblight, ChannelRgbMatrix, ChannelAudio, ChannelLedMatrix} func (c Channel) Subsystem() string { switch c { case ChannelBacklight: return "backlight" case ChannelRgblight: return "rgblight" case ChannelRgbMatrix: return "rgb_matrix" case ChannelAudio: return "audio" case ChannelLedMatrix: return "led_matrix" default: return "" } } // DetectChannels asks the keyboard which lighting channels it has, in ascending // order. // // The 0xFF answer is the discriminator, not the payload: QMK rejects a channel // it does not compile in with id_unhandled, whereas an unknown value ID on a // known channel is mirrored back with a zero payload. A read that fails for any // other reason aborts the probe, because a channel list missing entries is // indistinguishable from a complete one and would silently under-report. func (p *Protocol) DetectChannels() ([]Channel, error) { var present []Channel for c := Channel(1); c <= probeChannelMax; c++ { if _, err := p.GetValue(c, probeValueID); err != nil { if errors.Is(err, errUnhandled) { continue } return nil, fmt.Errorf("probe channel %d: %w", c, err) } present = append(present, c) } return present, nil } // effectValueID is the effect parameter's value ID. It is the same byte on every // lighting subsystem, like brightness. const effectValueID = 0x02 // effectProbeValue is what EffectTop writes to find the top. It is above every // effect ID any firmware has, and it is not 0: QMK's VIA handler reads a 0 as // "turn this channel off", so a probe that wrote it would switch the lighting // off rather than ask a question. const effectProbeValue = 0xff // EffectTop returns the highest effect ID the keyboard's firmware accepts on a // channel, which is the number of effect slots the channel has minus one. // // It is found by writing above the top and reading back what the firmware kept. // QMK clamps rather than rejects: quantum/rgb_matrix/rgb_matrix.c maps a mode at // or above RGB_MATRIX_EFFECT_MAX down to EFFECT_MAX - 1, so a single write above // the top returns the top itself. A firmware that ignores the write instead // returns the value it already held, and that is the same answer the clamp gives // when the channel already sat on its last effect, so the two need not be told // apart. A returned value of 255 would mean the channel really does take it. // // The channel is left as it was found, because the probe otherwise leaves it // running the last effect. What it cannot restore is a channel that was off: VIA // enables the channel for any nonzero effect and exposes no enabled bit to read, // so a channel that was disabled comes back enabled. `disable` turns it off // again. func (p *Protocol) EffectTop(ch Channel) (int, error) { previous, err := p.GetValue(ch, effectValueID) if err != nil { return 0, fmt.Errorf("read the effect on %s: %w", ch.Subsystem(), err) } if err := p.SetValue(ch, effectValueID, effectProbeValue); err != nil { return 0, fmt.Errorf("probe the effect range on %s: %w", ch.Subsystem(), err) } top, readErr := p.GetValue(ch, effectValueID) // The keyboard is holding the probe value until the original goes back, so // the restore runs whether the read succeeded or not. restoreErr := p.SetValue(ch, effectValueID, previous[0]) if readErr != nil { return 0, fmt.Errorf("read back the effect range on %s: %w", ch.Subsystem(), readErr) } if restoreErr != nil { return 0, fmt.Errorf("restore the effect on %s, which is left on %d: %w", ch.Subsystem(), top[0], restoreErr) } return int(top[0]), nil }