vial.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. package via
  2. import (
  3. "encoding/binary"
  4. "errors"
  5. "fmt"
  6. )
  7. // Vial's protocol. This is a QMK fork that keeps VIA's command set unchanged and
  8. // hangs its own commands off a prefix byte, and it replaces the rgb_matrix
  9. // lighting handler with one of its own. The numbers below are read from
  10. // vial-kb/vial-qmk, branch `vial`:
  11. //
  12. // quantum/via.h enum via_command_id -> id_vial_prefix
  13. // quantum/vial.h enum -> vial_get_keyboard_id, VIAL_PROTOCOL_VERSION
  14. // quantum/vialrgb.h enum -> vialrgb_get_info, vialrgb_get_supported
  15. //
  16. // None of this has been run against Vial firmware. It is written from that
  17. // source and tested against a scripted keyboard, which is what makes it
  18. // reviewable rather than what makes it right.
  19. const (
  20. // vialPrefix is Vial's command prefix. VIA's own commands are 0x01 to 0x13 and
  21. // untouched; this is where Vial's begin.
  22. vialPrefix = 0xFE
  23. // vialGetKeyboardID is the one command worth sending to find out whether a
  24. // keyboard speaks Vial at all. It answers with the protocol version and
  25. // nothing else, so it cannot be mistaken for state.
  26. vialGetKeyboardID = 0x00
  27. )
  28. // VialRGB's commands, from quantum/vialrgb.h. They are not behind vialPrefix:
  29. // VialRGB answers the ordinary VIA lighting command 0x08 and carries its own
  30. // command in the channel byte, so a request for the supported effect IDs is a
  31. // custom get with a channel of 0x42. That is in vialrgb_get_value, which reads
  32. // `uint8_t cmd = data[1]` and switches on it.
  33. const (
  34. lightingGet = 0x08
  35. vialrgbGetInfo = 0x40
  36. vialrgbGetSupported = 0x42
  37. )
  38. // vialrgbMaxPayload is how many bytes of a report the supported-ID list may
  39. // fill. The report is 32 bytes, the command byte and Vial's command byte take
  40. // two, and the list starts in the third.
  41. const vialrgbMaxPayload = 32 - 2
  42. // VialVersion asks whether the keyboard speaks Vial and, if it does, for its
  43. // protocol version.
  44. //
  45. // The answer is a discriminator rather than a guess because the two firmwares
  46. // differ in what they do with a command they do not know. Stock QMK's
  47. // raw_hid_receive ends its switch with
  48. //
  49. // default: { *command_id = id_unhandled; }
  50. //
  51. // so an unknown command comes back as 0xFF. Vial's default case does not set that
  52. // marker — it defers to the keyboard instead — and its keyboard-ID handler fills
  53. // bytes 1 to 4 with the version. So 0xFF in byte 0 is a keyboard that is not
  54. // Vial, and a version that is not zero is one that is. A keyboard implementing
  55. // raw_hid_receive_kb for 0xFE would defeat this, which is the one case the
  56. // source cannot rule out.
  57. func (p *Protocol) VialVersion() (uint32, bool, error) {
  58. report := make([]byte, 32)
  59. report[0] = vialPrefix
  60. report[1] = vialGetKeyboardID
  61. if _, err := p.handle.SendReport(0x00, report); err != nil {
  62. return 0, false, err
  63. }
  64. buf, err := p.readPrefixed(vialPrefix)
  65. if err != nil {
  66. if errors.Is(err, errUnhandled) {
  67. // Stock QMK marking a command it does not know is the ordinary
  68. // answer for a keyboard that is not Vial, not a failure.
  69. return 0, false, nil
  70. }
  71. return 0, false, err
  72. }
  73. // Vial's handlers take msg = &data[1], so the version lands in bytes 1 to 4.
  74. version := binary.LittleEndian.Uint32(buf[1:5])
  75. if version == 0 {
  76. // Byte 0 came back as the prefix and the rest as nothing, which is what a
  77. // keyboard that echoes the request looks like. A Vial version is never 0.
  78. return 0, false, nil
  79. }
  80. return version, true, nil
  81. }
  82. // VialEffectIDs returns the effect IDs the keyboard's rgb_matrix channel has
  83. // compiled in, as Vial numbers.
  84. //
  85. // It does not report effect 0. get_supported fills the report with the IDs
  86. // strictly greater than the cursor, and the first request asks from 0, so "off"
  87. // is implied rather than listed. A caller that wants to set 0 writes 0; nothing
  88. // here tells it that 0 exists.
  89. //
  90. // It is worth asking for where it answers, for two reasons. It does not write to
  91. // the keyboard, where reading the range by clamping does. And it is the firmware
  92. // reporting what it has, where the clamp is the firmware's answer to a question
  93. // it was not asked.
  94. //
  95. // Three things bound the answer. The list is a run of u16 with 0xFF padding, and
  96. // 0xFFFF is padding rather than an ID. The IDs ascend, and a list that does not
  97. // is not one this firmware produced. And the whole list is fetched in pages,
  98. // because get_supported fills one report at a time from a cursor.
  99. func (p *Protocol) VialEffectIDs() ([]uint16, error) {
  100. var ids []uint16
  101. cursor := uint16(0)
  102. // Each page must advance the cursor or the loop would never end. One page per
  103. // ten IDs is generous for a firmware whose largest list is under fifty, and a
  104. // keyboard that needs more is not one this can make sense of.
  105. for page := 0; page < 32; page++ {
  106. batch, err := p.vialSupportedPage(cursor)
  107. if err != nil {
  108. return nil, err
  109. }
  110. if len(batch) == 0 {
  111. break
  112. }
  113. ids = append(ids, batch...)
  114. if batch[len(batch)-1] <= cursor {
  115. return nil, fmt.Errorf("vialrgb effect list did not advance past %d", cursor)
  116. }
  117. cursor = batch[len(batch)-1]
  118. }
  119. return ascending(ids), nil
  120. }
  121. // vialSupportedPage asks for the effect IDs above a cursor and reads the run of
  122. // u16 the firmware wrote into the payload.
  123. func (p *Protocol) vialSupportedPage(cursor uint16) ([]uint16, error) {
  124. report := make([]byte, 32)
  125. report[0] = lightingGet
  126. report[1] = vialrgbGetSupported
  127. binary.LittleEndian.PutUint16(report[2:4], cursor)
  128. if _, err := p.handle.SendReport(0x00, report); err != nil {
  129. return nil, err
  130. }
  131. // The list starts at byte 2, where the request's cursor was: get_supported
  132. // overwrites the cursor with the first ID it found, so those bytes are data
  133. // and not an echo of what was sent.
  134. buf, err := p.readPrefixed(lightingGet)
  135. if err != nil {
  136. return nil, err
  137. }
  138. payload := buf[2 : 2+vialrgbMaxPayload]
  139. var out []uint16
  140. for i := 0; i+1 < len(payload); i += 2 {
  141. id := binary.LittleEndian.Uint16(payload[i : i+2])
  142. if id == 0xFFFF || id == 0 {
  143. // get_supported pads the rest of the report with 0xFF, so 0xFFFF is
  144. // padding and not an effect. Zero stops it as well: the list holds the
  145. // IDs strictly greater than a cursor that starts at 0, so a 0 in it is
  146. // never an effect. That is what keeps a report that is zero-filled
  147. // rather than 0xFF-filled from running on through fifteen zeros.
  148. break
  149. }
  150. out = append(out, id)
  151. }
  152. return out, nil
  153. }
  154. // ascending reports whether the IDs came out in order. A list that did not is not
  155. // this firmware's, and returning it would put a name on a number the firmware
  156. // never said was there.
  157. func ascending(ids []uint16) []uint16 {
  158. for i := 1; i < len(ids); i++ {
  159. if ids[i] <= ids[i-1] {
  160. return nil
  161. }
  162. }
  163. return ids
  164. }
  165. // readPrefixed reads the report from a command that does not answer in VIA's
  166. // shape. readResponse checks the echoed channel and value ID, and Vial's handlers
  167. // fill their payload from byte 1 or byte 2 without echoing either — one of them
  168. // carries its own command in the channel byte — so those checks would reject a
  169. // correct answer. The command byte is the one thing both firmwares leave in
  170. // place, and 0xFF is how either says it does not handle the command.
  171. func (p *Protocol) readPrefixed(command byte) ([]byte, error) {
  172. buf := make([]byte, 32)
  173. n, err := p.handle.Read(buf)
  174. if err != nil {
  175. return nil, fmt.Errorf("read response: %w", err)
  176. }
  177. if n != len(buf) {
  178. return nil, fmt.Errorf("short response: got %d bytes, want 32", n)
  179. }
  180. if buf[0] == byte(Unhandled) {
  181. return nil, errUnhandled
  182. }
  183. if buf[0] != command {
  184. return nil, fmt.Errorf("unexpected response command: 0x%02x, want 0x%02x", buf[0], command)
  185. }
  186. return buf, nil
  187. }