package via import ( "errors" "fmt" "netdome.biz/paul/qmk-rgb/internal/device" "netdome.biz/paul/qmk-rgb/internal/hid" ) type transport interface { SendReport(reportID byte, report []byte) (int, error) Read(buf []byte) (int, error) Close() error } // Protocol implements the VIA protocol for communication with a keyboard. type Protocol struct { handle transport } // New creates a new VIA protocol handler for a connected device. func New(dev device.Device) (*Protocol, error) { h, err := hid.OpenPath(dev.Path) if err != nil { return nil, fmt.Errorf("open device: %w", err) } return &Protocol{handle: h}, nil } // Close releases the device handle. func (p *Protocol) Close() error { if p.handle != nil { return p.handle.Close() } return nil } // Message defines VIA message types. type Message uint8 const ( CustomSet Message = 0x07 CustomGet Message = 0x08 Unhandled Message = 0xff ) // errUnhandled reports a channel or value ID the firmware does not implement. // It is a sentinel so a caller can tell "this does not exist here" from a // transport failure, which the two are otherwise indistinguishable in. var errUnhandled = errors.New("unhandled response") // SetValue sends a set value command for the given channel and parameter. func (p *Protocol) SetValue(ch Channel, param uint8, value uint8) error { report := make([]byte, 32) report[0] = byte(CustomSet) report[1] = byte(ch) report[2] = param report[3] = value if _, err := p.handle.SendReport(0x00, report); err != nil { return err } _, err := p.readResponse(CustomSet, byte(ch), param) return err } func (p *Protocol) SetColor(ch Channel, hue uint8, saturation uint8) error { report := make([]byte, 32) report[0] = byte(CustomSet) report[1] = byte(ch) report[2] = 0x04 report[3] = hue report[4] = saturation if _, err := p.handle.SendReport(0x00, report); err != nil { return err } _, err := p.readResponse(CustomSet, byte(ch), 0x04) return err } // GetValue sends a get value request and reads the response. func (p *Protocol) GetValue(ch Channel, param uint8) ([]byte, error) { report := make([]byte, 32) report[0] = byte(CustomGet) report[1] = byte(ch) report[2] = param _, err := p.handle.SendReport(0x00, report) if err != nil { return nil, fmt.Errorf("send get request: %w", err) } buf, err := p.readResponse(CustomGet, byte(ch), param) if err != nil { return nil, err } valueSize := 1 if param == 0x04 { valueSize = 2 } return append([]byte(nil), buf[3:3+valueSize]...), nil } func (p *Protocol) readResponse(command Message, ch, param 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) { // A 0xFF for a different channel is not absence, it is a stale answer // still sitting in the read buffer. QMK echoes the channel it answers, // so the mismatch is what tells the two apart. if buf[1] != ch { return nil, fmt.Errorf("stale response for channel 0x%02x while reading channel 0x%02x", buf[1], ch) } return nil, errUnhandled } if buf[0] != byte(command) { return nil, fmt.Errorf("unexpected response command: 0x%02x", buf[0]) } if buf[1] != ch || buf[2] != param { return nil, fmt.Errorf("response value mismatch: got channel 0x%02x value 0x%02x", buf[1], buf[2]) } return buf, nil }