|
@@ -0,0 +1,222 @@
|
|
|
|
|
+package via
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "encoding/binary"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// emptyVialPage is what get_supported returns once the list runs out: the whole
|
|
|
|
|
+// report is 0xFF padding and no ID is greater than the cursor. A list ends
|
|
|
|
|
+// because the firmware says so, not because the host ran out of answers.
|
|
|
|
|
+func emptyVialPage() []byte {
|
|
|
|
|
+ buf := make([]byte, 32)
|
|
|
|
|
+ buf[0] = lightingGet
|
|
|
|
|
+ buf[1] = vialrgbGetSupported
|
|
|
|
|
+ for i := 2; i < len(buf); i++ {
|
|
|
|
|
+ buf[i] = 0xFF
|
|
|
|
|
+ }
|
|
|
|
|
+ return buf
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// vialVersionResponse is a Vial keyboard-ID answer: the prefix stays in byte 0
|
|
|
|
|
+// and the version fills bytes 1 to 4, because Vial's handlers take msg = &data[1].
|
|
|
|
|
+func vialVersionResponse(version uint32) []byte {
|
|
|
|
|
+ buf := make([]byte, 32)
|
|
|
|
|
+ buf[0] = vialPrefix
|
|
|
|
|
+ binary.LittleEndian.PutUint32(buf[1:5], version)
|
|
|
|
|
+ return buf
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// unhandledResponse is stock QMK's answer to a command its switch does not know:
|
|
|
|
|
+// raw_hid_receive ends with `default: { *command_id = id_unhandled; }`.
|
|
|
|
|
+func unhandledResponse() []byte {
|
|
|
|
|
+ buf := make([]byte, 32)
|
|
|
|
|
+ buf[0] = byte(Unhandled)
|
|
|
|
|
+ return buf
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// echoResponse is a keyboard that hands the request straight back. Nothing in
|
|
|
|
|
+// Vial's or QMK's source rules it out, and it is the answer that would make a
|
|
|
|
|
+// prefix probe believe anything, so it has to read as "not Vial".
|
|
|
|
|
+func echoResponse() []byte {
|
|
|
|
|
+ buf := make([]byte, 32)
|
|
|
|
|
+ buf[0] = vialPrefix
|
|
|
|
|
+ buf[1] = vialGetKeyboardID
|
|
|
|
|
+ return buf
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func TestVialVersionReadsTheVersionOffAVialKeyboard(t *testing.T) {
|
|
|
|
|
+ transport := &fakeTransport{queue: [][]byte{vialVersionResponse(0x00000006)}}
|
|
|
|
|
+ protocol := Protocol{handle: transport}
|
|
|
|
|
+
|
|
|
|
|
+ version, isVial, err := protocol.VialVersion()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("VialVersion() error = %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !isVial {
|
|
|
|
|
+ t.Error("VialVersion() = not Vial, want a keyboard that answered with a version")
|
|
|
|
|
+ }
|
|
|
|
|
+ if version != 6 {
|
|
|
|
|
+ t.Errorf("VialVersion() = %d, want 6", version)
|
|
|
|
|
+ }
|
|
|
|
|
+ // The probe has to be the prefixed command, and the rest of the report zeroed,
|
|
|
|
|
+ // so that an echo cannot look like an answer.
|
|
|
|
|
+ sent := transport.reports[0]
|
|
|
|
|
+ if sent[0] != vialPrefix || sent[1] != vialGetKeyboardID {
|
|
|
|
|
+ t.Errorf("request = %v, want the Vial prefix and the keyboard-ID command", sent[:2])
|
|
|
|
|
+ }
|
|
|
|
|
+ for i := 2; i < len(sent); i++ {
|
|
|
|
|
+ if sent[i] != 0 {
|
|
|
|
|
+ t.Errorf("request byte %d = %d, want 0 so that an echo cannot pass for a version", i, sent[i])
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Stock QMK marks a command it does not know, and that is a keyboard that is not
|
|
|
|
|
+// Vial rather than a failure to talk to it.
|
|
|
|
|
+func TestVialVersionTreatsAnUnhandledCommandAsNotVial(t *testing.T) {
|
|
|
|
|
+ transport := &fakeTransport{queue: [][]byte{unhandledResponse()}}
|
|
|
|
|
+ protocol := Protocol{handle: transport}
|
|
|
|
|
+
|
|
|
|
|
+ version, isVial, err := protocol.VialVersion()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("VialVersion() error = %v, want the unhandled marker read as an answer", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if isVial || version != 0 {
|
|
|
|
|
+ t.Errorf("VialVersion() = %d, %t, want 0, false", version, isVial)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A keyboard that echoes the request answers with a prefix and a zero. A version
|
|
|
|
|
+// is never zero, so this must not read as Vial.
|
|
|
|
|
+func TestVialVersionDoesNotTrustAnEcho(t *testing.T) {
|
|
|
|
|
+ transport := &fakeTransport{queue: [][]byte{echoResponse()}}
|
|
|
|
|
+ protocol := Protocol{handle: transport}
|
|
|
|
|
+
|
|
|
|
|
+ _, isVial, err := protocol.VialVersion()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("VialVersion() error = %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if isVial {
|
|
|
|
|
+ t.Error("VialVersion() = Vial on an echoed request, which carries no version")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// The supported list is a run of u16 from byte 2, padded to the end of the
|
|
|
|
|
+// report with 0xFF. Reading the padding as an ID would put a name on a number
|
|
|
|
|
+// the firmware never said was there.
|
|
|
|
|
+func TestVialEffectIDsStopsAtTheFirmwarePadding(t *testing.T) {
|
|
|
|
|
+ page := make([]byte, 32)
|
|
|
|
|
+ page[0] = lightingGet
|
|
|
|
|
+ page[1] = vialrgbGetSupported
|
|
|
|
|
+ for i, id := range []uint16{1, 4, 5} {
|
|
|
|
|
+ binary.LittleEndian.PutUint16(page[2+i*2:], id)
|
|
|
|
|
+ }
|
|
|
|
|
+ // get_supported memsets the rest of the report to 0xFF, which is the padding
|
|
|
|
|
+ // the reader has to stop at.
|
|
|
|
|
+ for i := 8; i < len(page); i++ {
|
|
|
|
|
+ page[i] = 0xFF
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ transport := &fakeTransport{queue: [][]byte{page, emptyVialPage()}}
|
|
|
|
|
+ protocol := Protocol{handle: transport}
|
|
|
|
|
+
|
|
|
|
|
+ ids, err := protocol.VialEffectIDs()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("VialEffectIDs() error = %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ want := []uint16{1, 4, 5}
|
|
|
|
|
+ if len(ids) != len(want) {
|
|
|
|
|
+ t.Fatalf("VialEffectIDs() = %v, want %v", ids, want)
|
|
|
|
|
+ }
|
|
|
|
|
+ for i := range want {
|
|
|
|
|
+ if ids[i] != want[i] {
|
|
|
|
|
+ t.Errorf("VialEffectIDs()[%d] = %d, want %d", i, ids[i], want[i])
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// get_supported fills one report at a time from a cursor the request carries, so
|
|
|
|
|
+// a list longer than one page takes more than one round trip.
|
|
|
|
|
+func TestVialEffectIDsFollowsTheCursor(t *testing.T) {
|
|
|
|
|
+ first := make([]byte, 32)
|
|
|
|
|
+ first[0] = lightingGet
|
|
|
|
|
+ first[1] = vialrgbGetSupported
|
|
|
|
|
+ binary.LittleEndian.PutUint16(first[2:], 40)
|
|
|
|
|
+
|
|
|
|
|
+ second := make([]byte, 32)
|
|
|
|
|
+ second[0] = lightingGet
|
|
|
|
|
+ second[1] = vialrgbGetSupported
|
|
|
|
|
+ binary.LittleEndian.PutUint16(second[2:], 45)
|
|
|
|
|
+ for i := 4; i < len(second); i++ {
|
|
|
|
|
+ second[i] = 0xFF
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ transport := &fakeTransport{queue: [][]byte{first, second, emptyVialPage()}}
|
|
|
|
|
+ protocol := Protocol{handle: transport}
|
|
|
|
|
+
|
|
|
|
|
+ ids, err := protocol.VialEffectIDs()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("VialEffectIDs() error = %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(ids) != 2 || ids[0] != 40 || ids[1] != 45 {
|
|
|
|
|
+ t.Errorf("VialEffectIDs() = %v, want [40 45]", ids)
|
|
|
|
|
+ }
|
|
|
|
|
+ // The second request has to carry what the first page ended on, or the
|
|
|
|
|
+ // firmware would send the same page again.
|
|
|
|
|
+ cursor := binary.LittleEndian.Uint16(transport.reports[1][2:4])
|
|
|
|
|
+ if cursor != 40 {
|
|
|
|
|
+ t.Errorf("second request cursor = %d, want 40", cursor)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Stock QMK's rgb_matrix get has no default case, so a value ID it does not know
|
|
|
|
|
+// leaves the report unmarked rather than answering 0xFF. That is why the effect
|
|
|
|
|
+// IDs must not be asked for before the firmware is known to be Vial: on a stock
|
|
|
|
|
+// board the answer is an echo, and an echo of this request is bytes that can be
|
|
|
|
|
+// read as a list.
|
|
|
|
|
+func TestVialEffectIDsRefusesAListThatDoesNotAscend(t *testing.T) {
|
|
|
|
|
+ page := make([]byte, 32)
|
|
|
|
|
+ page[0] = lightingGet
|
|
|
|
|
+ page[1] = vialrgbGetSupported
|
|
|
|
|
+ binary.LittleEndian.PutUint16(page[2:], 7)
|
|
|
|
|
+ binary.LittleEndian.PutUint16(page[4:], 3)
|
|
|
|
|
+
|
|
|
|
|
+ transport := &fakeTransport{queue: [][]byte{page, emptyVialPage()}}
|
|
|
|
|
+ protocol := Protocol{handle: transport}
|
|
|
|
|
+
|
|
|
|
|
+ ids, err := protocol.VialEffectIDs()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("VialEffectIDs() error = %v, want a list read and then rejected", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if ids != nil {
|
|
|
|
|
+ t.Errorf("VialEffectIDs() = %v, want nothing from a list that does not ascend", ids)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A firmware that echoes the request answers the second page with the cursor it
|
|
|
|
|
+// was given, and a cursor that cannot advance would loop forever. That is the case
|
|
|
|
|
+// the guard is for: an echoing keyboard is not a Vial keyboard, and the loop has
|
|
|
|
|
+// to end rather than keep asking.
|
|
|
|
|
+func TestVialEffectIDsStopsWhenTheFirmwareEchoesTheCursor(t *testing.T) {
|
|
|
|
|
+ page := make([]byte, 32)
|
|
|
|
|
+ page[0] = lightingGet
|
|
|
|
|
+ page[1] = vialrgbGetSupported
|
|
|
|
|
+ binary.LittleEndian.PutUint16(page[2:], 1)
|
|
|
|
|
+ binary.LittleEndian.PutUint16(page[4:], 4)
|
|
|
|
|
+ for i := 6; i < len(page); i++ {
|
|
|
|
|
+ page[i] = 0xFF
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ transport := &fakeTransport{queue: [][]byte{page}}
|
|
|
|
|
+ protocol := Protocol{handle: transport}
|
|
|
|
|
+
|
|
|
|
|
+ _, err := protocol.VialEffectIDs()
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ t.Fatal("VialEffectIDs() = nil error, want a failure when the cursor cannot advance")
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(err.Error(), "advance") {
|
|
|
|
|
+ t.Errorf("error = %q, want it to say the list did not advance", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|