package device import ( "context" "fmt" "time" "github.com/google/gousb" "galahad2lcd/internal/protocol" ) // Lian Li Galahad II LCD USB layout (from the reference driver and // protocol reversals). const ( interfaceNumber = 1 endpointNumber = 2 configNumber = 1 writeTimeout = time.Second ) // USBDevice is the gousb-backed implementation of Device. type USBDevice struct { gctx *gousb.Context dev *gousb.Device intf *gousb.Interface ep *gousb.OutEndpoint } // Open locates the display by vendor/product ID and claims its video // interface. The kernel driver is detached automatically and reattached on // Close. func Open(ctx context.Context, vendorID, productID int) (*USBDevice, error) { gctx := gousb.NewContext() dev, err := gctx.OpenDeviceWithVIDPID(gousb.ID(vendorID), gousb.ID(productID)) if err != nil { gctx.Close() return nil, fmt.Errorf("open USB device %04x:%04x: %w", vendorID, productID, err) } if dev == nil { gctx.Close() return nil, fmt.Errorf("USB device %04x:%04x: %w", vendorID, productID, ErrNotFound) } // Auto-detach releases any kernel driver (e.g. usbhid) so we can claim // the interface, and reattaches it when we are done. if err := dev.SetAutoDetach(true); err != nil { _ = dev.Close() gctx.Close() return nil, fmt.Errorf("enable auto-detach: %w", err) } cfg, err := dev.Config(configNumber) if err != nil { _ = dev.Close() gctx.Close() return nil, fmt.Errorf("select USB config %d: %w", configNumber, err) } intf, err := cfg.Interface(interfaceNumber, 0) if err != nil { _ = cfg.Close() _ = dev.Close() gctx.Close() return nil, fmt.Errorf("claim interface %d: %w", interfaceNumber, err) } ep, err := intf.OutEndpoint(endpointNumber) if err != nil { intf.Close() _ = dev.Close() gctx.Close() return nil, fmt.Errorf("open out endpoint 0x0%x: %w", endpointNumber, err) } return &USBDevice{gctx: gctx, dev: dev, intf: intf, ep: ep}, nil } // OpenWithRetry keeps trying to open the device until ctx is done or // timeout elapses, with exponential backoff. This lets the service start // before the USB device has finished enumerating at boot. func OpenWithRetry(ctx context.Context, vendorID, productID int, timeout time.Duration) (*USBDevice, error) { deadline := time.Now().Add(timeout) backoff := 500 * time.Millisecond var lastErr error for { d, err := Open(ctx, vendorID, productID) if err == nil { return d, nil } if ctx.Err() != nil { return nil, ctx.Err() } lastErr = err if time.Now().After(deadline) { return nil, fmt.Errorf("device %04x:%04x unavailable for %s: %w", vendorID, productID, timeout, lastErr) } select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(backoff): if backoff < 5*time.Second { backoff *= 2 } } } } // WriteFrame encodes the frame into USB packets and transmits them with a // bounded timeout per packet. func (d *USBDevice) WriteFrame(ctx context.Context, frame []byte) error { packets, err := protocol.EncodePackets(frame) if err != nil { return fmt.Errorf("encode frame: %w", err) } for _, pkt := range packets { writeCtx, cancel := context.WithTimeout(ctx, writeTimeout) n, err := d.ep.WriteContext(writeCtx, pkt) cancel() if err != nil { return fmt.Errorf("USB write: %w", err) } if n != len(pkt) { return fmt.Errorf("USB short write: %d of %d bytes", n, len(pkt)) } } return nil } // Close releases the interface, device and libusb context. func (d *USBDevice) Close() error { var firstErr error if d.intf != nil { d.intf.Close() } if d.dev != nil { if err := d.dev.Close(); err != nil && firstErr == nil { firstErr = err } } if d.gctx != nil { if err := d.gctx.Close(); err != nil && firstErr == nil { firstErr = err } } return firstErr }