| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- package via
- import (
- "encoding/binary"
- "errors"
- "fmt"
- )
- // Vial's protocol. This is a QMK fork that keeps VIA's command set unchanged and
- // hangs its own commands off a prefix byte, and it replaces the rgb_matrix
- // lighting handler with one of its own. The numbers below are read from
- // vial-kb/vial-qmk, branch `vial`:
- //
- // quantum/via.h enum via_command_id -> id_vial_prefix
- // quantum/vial.h enum -> vial_get_keyboard_id, VIAL_PROTOCOL_VERSION
- // quantum/vialrgb.h enum -> vialrgb_get_info, vialrgb_get_supported
- //
- // None of this has been run against Vial firmware. It is written from that
- // source and tested against a scripted keyboard, which is what makes it
- // reviewable rather than what makes it right.
- const (
- // vialPrefix is Vial's command prefix. VIA's own commands are 0x01 to 0x13 and
- // untouched; this is where Vial's begin.
- vialPrefix = 0xFE
- // vialGetKeyboardID is the one command worth sending to find out whether a
- // keyboard speaks Vial at all. It answers with the protocol version and
- // nothing else, so it cannot be mistaken for state.
- vialGetKeyboardID = 0x00
- )
- // VialRGB's commands, from quantum/vialrgb.h. They are not behind vialPrefix:
- // VialRGB answers the ordinary VIA lighting command 0x08 and carries its own
- // command in the channel byte, so a request for the supported effect IDs is a
- // custom get with a channel of 0x42. That is in vialrgb_get_value, which reads
- // `uint8_t cmd = data[1]` and switches on it.
- const (
- lightingGet = 0x08
- vialrgbGetInfo = 0x40
- vialrgbGetSupported = 0x42
- )
- // vialrgbMaxPayload is how many bytes of a report the supported-ID list may
- // fill. The report is 32 bytes, the command byte and Vial's command byte take
- // two, and the list starts in the third.
- const vialrgbMaxPayload = 32 - 2
- // VialVersion asks whether the keyboard speaks Vial and, if it does, for its
- // protocol version.
- //
- // The answer is a discriminator rather than a guess because the two firmwares
- // differ in what they do with a command they do not know. Stock QMK's
- // raw_hid_receive ends its switch with
- //
- // default: { *command_id = id_unhandled; }
- //
- // so an unknown command comes back as 0xFF. Vial's default case does not set that
- // marker — it defers to the keyboard instead — and its keyboard-ID handler fills
- // bytes 1 to 4 with the version. So 0xFF in byte 0 is a keyboard that is not
- // Vial, and a version that is not zero is one that is. A keyboard implementing
- // raw_hid_receive_kb for 0xFE would defeat this, which is the one case the
- // source cannot rule out.
- func (p *Protocol) VialVersion() (uint32, bool, error) {
- report := make([]byte, 32)
- report[0] = vialPrefix
- report[1] = vialGetKeyboardID
- if _, err := p.handle.SendReport(0x00, report); err != nil {
- return 0, false, err
- }
- buf, err := p.readPrefixed(vialPrefix)
- if err != nil {
- if errors.Is(err, errUnhandled) {
- // Stock QMK marking a command it does not know is the ordinary
- // answer for a keyboard that is not Vial, not a failure.
- return 0, false, nil
- }
- return 0, false, err
- }
- // Vial's handlers take msg = &data[1], so the version lands in bytes 1 to 4.
- version := binary.LittleEndian.Uint32(buf[1:5])
- if version == 0 {
- // Byte 0 came back as the prefix and the rest as nothing, which is what a
- // keyboard that echoes the request looks like. A Vial version is never 0.
- return 0, false, nil
- }
- return version, true, nil
- }
- // VialEffectIDs returns the effect IDs the keyboard's rgb_matrix channel has
- // compiled in, as Vial numbers.
- //
- // It does not report effect 0. get_supported fills the report with the IDs
- // strictly greater than the cursor, and the first request asks from 0, so "off"
- // is implied rather than listed. A caller that wants to set 0 writes 0; nothing
- // here tells it that 0 exists.
- //
- // It is worth asking for where it answers, for two reasons. It does not write to
- // the keyboard, where reading the range by clamping does. And it is the firmware
- // reporting what it has, where the clamp is the firmware's answer to a question
- // it was not asked.
- //
- // Three things bound the answer. The list is a run of u16 with 0xFF padding, and
- // 0xFFFF is padding rather than an ID. The IDs ascend, and a list that does not
- // is not one this firmware produced. And the whole list is fetched in pages,
- // because get_supported fills one report at a time from a cursor.
- func (p *Protocol) VialEffectIDs() ([]uint16, error) {
- var ids []uint16
- cursor := uint16(0)
- // Each page must advance the cursor or the loop would never end. One page per
- // ten IDs is generous for a firmware whose largest list is under fifty, and a
- // keyboard that needs more is not one this can make sense of.
- for page := 0; page < 32; page++ {
- batch, err := p.vialSupportedPage(cursor)
- if err != nil {
- return nil, err
- }
- if len(batch) == 0 {
- break
- }
- ids = append(ids, batch...)
- if batch[len(batch)-1] <= cursor {
- return nil, fmt.Errorf("vialrgb effect list did not advance past %d", cursor)
- }
- cursor = batch[len(batch)-1]
- }
- return ascending(ids), nil
- }
- // vialSupportedPage asks for the effect IDs above a cursor and reads the run of
- // u16 the firmware wrote into the payload.
- func (p *Protocol) vialSupportedPage(cursor uint16) ([]uint16, error) {
- report := make([]byte, 32)
- report[0] = lightingGet
- report[1] = vialrgbGetSupported
- binary.LittleEndian.PutUint16(report[2:4], cursor)
- if _, err := p.handle.SendReport(0x00, report); err != nil {
- return nil, err
- }
- // The list starts at byte 2, where the request's cursor was: get_supported
- // overwrites the cursor with the first ID it found, so those bytes are data
- // and not an echo of what was sent.
- buf, err := p.readPrefixed(lightingGet)
- if err != nil {
- return nil, err
- }
- payload := buf[2 : 2+vialrgbMaxPayload]
- var out []uint16
- for i := 0; i+1 < len(payload); i += 2 {
- id := binary.LittleEndian.Uint16(payload[i : i+2])
- if id == 0xFFFF || id == 0 {
- // get_supported pads the rest of the report with 0xFF, so 0xFFFF is
- // padding and not an effect. Zero stops it as well: the list holds the
- // IDs strictly greater than a cursor that starts at 0, so a 0 in it is
- // never an effect. That is what keeps a report that is zero-filled
- // rather than 0xFF-filled from running on through fifteen zeros.
- break
- }
- out = append(out, id)
- }
- return out, nil
- }
- // ascending reports whether the IDs came out in order. A list that did not is not
- // this firmware's, and returning it would put a name on a number the firmware
- // never said was there.
- func ascending(ids []uint16) []uint16 {
- for i := 1; i < len(ids); i++ {
- if ids[i] <= ids[i-1] {
- return nil
- }
- }
- return ids
- }
- // readPrefixed reads the report from a command that does not answer in VIA's
- // shape. readResponse checks the echoed channel and value ID, and Vial's handlers
- // fill their payload from byte 1 or byte 2 without echoing either — one of them
- // carries its own command in the channel byte — so those checks would reject a
- // correct answer. The command byte is the one thing both firmwares leave in
- // place, and 0xFF is how either says it does not handle the command.
- func (p *Protocol) readPrefixed(command byte) ([]byte, error) {
- buf := make([]byte, 32)
- n, err := p.handle.Read(buf)
- if err != nil {
- return nil, fmt.Errorf("read response: %w", err)
- }
- if n != len(buf) {
- return nil, fmt.Errorf("short response: got %d bytes, want 32", n)
- }
- if buf[0] == byte(Unhandled) {
- return nil, errUnhandled
- }
- if buf[0] != command {
- return nil, fmt.Errorf("unexpected response command: 0x%02x, want 0x%02x", buf[0], command)
- }
- return buf, nil
- }
|