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) } }