package relaydev import ( "context" "fmt" "strings" "sync" "go.bug.st/serial" ) // serialRelay drives the common cheap USB-serial relay boards (CH340 / LCUS-1 // style) that use the "A0" command protocol over a virtual COM port: // // [0xA0] [relay 1-based] [state 0|1] [checksum] checksum = (0xA0+relay+state) & 0xFF // // e.g. relay 1 ON = A0 01 01 A2, relay 1 OFF = A0 01 00 A1. These boards are // write-only (no state readback), so Status reflects a shadow of what we sent. // Channel count varies by board (1/2/4/8/16), so it is configurable. type serialRelay struct { portName string count int baud int mu sync.Mutex port serial.Port shadow []bool } // NewSerialRelay builds a driver for a CH340/LCUS-style USB-serial relay board on // the given COM port with `count` channels (defaults to 8). func NewSerialRelay(port string, count int) Device { if count < 1 { count = 8 } return &serialRelay{portName: strings.TrimSpace(port), count: count, baud: 9600, shadow: make([]bool, count)} } func (s *serialRelay) Count() int { return s.count } func (s *serialRelay) ensureOpen() error { if s.port != nil { return nil } if s.portName == "" { return fmt.Errorf("no COM port set for the USB relay board") } p, err := serial.Open(s.portName, &serial.Mode{BaudRate: s.baud}) if err != nil { return fmt.Errorf("open %s: %w", s.portName, err) } s.port = p return nil } func (s *serialRelay) Set(ctx context.Context, relay int, on bool) error { if relay < 1 || relay > s.count { return fmt.Errorf("relay %d out of range 1..%d", relay, s.count) } s.mu.Lock() defer s.mu.Unlock() if err := s.ensureOpen(); err != nil { return err } st := byte(0) if on { st = 1 } r := byte(relay) frame := []byte{0xA0, r, st, byte(0xA0) + r + st} // last byte = checksum if _, err := s.port.Write(frame); err != nil { // Drop the handle so the next call reopens (USB unplugged / port reset). s.port.Close() s.port = nil return fmt.Errorf("write to %s: %w", s.portName, err) } s.shadow[relay-1] = on return nil } func (s *serialRelay) Status(ctx context.Context) ([]bool, error) { s.mu.Lock() defer s.mu.Unlock() if err := s.ensureOpen(); err != nil { return nil, err } out := make([]bool, s.count) copy(out, s.shadow) return out, nil }