channel.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package via
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. // Channel is a VIA lighting channel. The values are QMK's id_qmk_*_channel
  7. // constants from quantum/via.h.
  8. type Channel uint8
  9. const (
  10. ChannelBacklight Channel = 1
  11. ChannelRgblight Channel = 2
  12. ChannelRgbMatrix Channel = 3
  13. ChannelAudio Channel = 4
  14. ChannelLedMatrix Channel = 5
  15. )
  16. // AssignedChannelMax is the highest channel QMK currently assigns. The probe
  17. // asks beyond it on purpose, see probeChannelMax.
  18. const AssignedChannelMax = ChannelLedMatrix
  19. // probeChannelMax is the highest channel the probe asks about. The QMK enum
  20. // stops at 5; the extra reads cost one round trip each and let a future QMK
  21. // that claims a higher channel be found without a code change.
  22. const probeChannelMax = 15
  23. // probeValueID is the brightness value ID. It is one byte and valid on every
  24. // lighting subsystem, so one request per channel finds any kind of channel.
  25. const probeValueID = 0x01
  26. // Subsystem returns the QMK name of the channel's lighting subsystem. The name
  27. // follows from the channel number, so no board file stores it and it is always
  28. // available for a channel the keyboard has.
  29. // LightingChannels lists the QMK lighting channels in channel order, which is
  30. // the order the tool reports them in.
  31. var LightingChannels = []Channel{ChannelBacklight, ChannelRgblight, ChannelRgbMatrix, ChannelAudio, ChannelLedMatrix}
  32. func (c Channel) Subsystem() string {
  33. switch c {
  34. case ChannelBacklight:
  35. return "backlight"
  36. case ChannelRgblight:
  37. return "rgblight"
  38. case ChannelRgbMatrix:
  39. return "rgb_matrix"
  40. case ChannelAudio:
  41. return "audio"
  42. case ChannelLedMatrix:
  43. return "led_matrix"
  44. default:
  45. return ""
  46. }
  47. }
  48. // DetectChannels asks the keyboard which lighting channels it has, in ascending
  49. // order.
  50. //
  51. // The 0xFF answer is the discriminator, not the payload: QMK rejects a channel
  52. // it does not compile in with id_unhandled, whereas an unknown value ID on a
  53. // known channel is mirrored back with a zero payload. A read that fails for any
  54. // other reason aborts the probe, because a channel list missing entries is
  55. // indistinguishable from a complete one and would silently under-report.
  56. func (p *Protocol) DetectChannels() ([]Channel, error) {
  57. var present []Channel
  58. for c := Channel(1); c <= probeChannelMax; c++ {
  59. if _, err := p.GetValue(c, probeValueID); err != nil {
  60. if errors.Is(err, errUnhandled) {
  61. continue
  62. }
  63. return nil, fmt.Errorf("probe channel %d: %w", c, err)
  64. }
  65. present = append(present, c)
  66. }
  67. return present, nil
  68. }
  69. // effectValueID is the effect parameter's value ID. It is the same byte on every
  70. // lighting subsystem, like brightness.
  71. const effectValueID = 0x02
  72. // effectProbeValue is what EffectTop writes to find the top. It is above every
  73. // effect ID any firmware has, and it is not 0: QMK's VIA handler reads a 0 as
  74. // "turn this channel off", so a probe that wrote it would switch the lighting
  75. // off rather than ask a question.
  76. const effectProbeValue = 0xff
  77. // EffectTop returns the highest effect ID the keyboard's firmware accepts on a
  78. // channel, which is the number of effect slots the channel has minus one.
  79. //
  80. // It is found by writing above the top and reading back what the firmware kept.
  81. // QMK clamps rather than rejects: quantum/rgb_matrix/rgb_matrix.c maps a mode at
  82. // or above RGB_MATRIX_EFFECT_MAX down to EFFECT_MAX - 1, so a single write above
  83. // the top returns the top itself. A firmware that ignores the write instead
  84. // returns the value it already held, and that is the same answer the clamp gives
  85. // when the channel already sat on its last effect, so the two need not be told
  86. // apart. A returned value of 255 would mean the channel really does take it.
  87. //
  88. // The channel is left as it was found, because the probe otherwise leaves it
  89. // running the last effect. What it cannot restore is a channel that was off: VIA
  90. // enables the channel for any nonzero effect and exposes no enabled bit to read,
  91. // so a channel that was disabled comes back enabled. `disable` turns it off
  92. // again.
  93. func (p *Protocol) EffectTop(ch Channel) (int, error) {
  94. previous, err := p.GetValue(ch, effectValueID)
  95. if err != nil {
  96. return 0, fmt.Errorf("read the effect on %s: %w", ch.Subsystem(), err)
  97. }
  98. if err := p.SetValue(ch, effectValueID, effectProbeValue); err != nil {
  99. return 0, fmt.Errorf("probe the effect range on %s: %w", ch.Subsystem(), err)
  100. }
  101. top, readErr := p.GetValue(ch, effectValueID)
  102. // The keyboard is holding the probe value until the original goes back, so
  103. // the restore runs whether the read succeeded or not.
  104. restoreErr := p.SetValue(ch, effectValueID, previous[0])
  105. if readErr != nil {
  106. return 0, fmt.Errorf("read back the effect range on %s: %w", ch.Subsystem(), readErr)
  107. }
  108. if restoreErr != nil {
  109. return 0, fmt.Errorf("restore the effect on %s, which is left on %d: %w",
  110. ch.Subsystem(), top[0], restoreErr)
  111. }
  112. return int(top[0]), nil
  113. }
  114. // ChannelFromSubsystem returns the channel a QMK lighting subsystem name stands
  115. // for, and whether the name is one of them. It is the read side of LightingChannels
  116. // and lives beside it so that the vocabulary is written down once: a name a file
  117. // or a command carries is turned into a channel here and nowhere else.
  118. func ChannelFromSubsystem(name string) (Channel, bool) {
  119. for _, ch := range LightingChannels {
  120. if ch.Subsystem() == name {
  121. return ch, true
  122. }
  123. }
  124. return 0, false
  125. }