package ezcoo import ( "bufio" "fmt" "log/slog" "strings" "time" "go.bug.st/serial" ) func (d *Device) GetStatus() State { d.mu.RLock() defer d.mu.RUnlock() return d.current } func (d *Device) SetOutput(out, in int) { cmd := fmt.Sprintf("EZS OUT%d VS IN%d", out, in) select { case d.cmds <- cmd: default: slog.Warn("command queue full, dropping", "cmd", cmd) } d.poll() time.AfterFunc(time.Second, d.poll) } func (d *Device) poll() { select { case d.cmds <- pollCmd: default: } } func (d *Device) Run(onState func(State), onAvailable func(bool), done <-chan struct{}) { if d.cfg.PollInterval > 0 { go d.pollLoop(done) } for { port, err := Open(d.cfg) if err != nil { slog.Error("open serial", "err", err, "retry_in", "5s") select { case <-done: return case <-time.After(5 * time.Second): continue } } slog.Info("serial connected", "port", d.cfg.Port) onAvailable(true) d.poll() d.runPort(port, onState, done) onAvailable(false) select { case <-done: return default: slog.Warn("serial disconnected, reconnecting in 5s") time.Sleep(5 * time.Second) } } } func (d *Device) runPort(port serial.Port, onState func(State), done <-chan struct{}) { defer port.Close() reader := bufio.NewReader(port) lineCh := make(chan string, 32) go func() { for { line, err := reader.ReadString('\n') line = strings.TrimSpace(line) if err != nil { slog.Error("serial read", "err", err) close(lineCh) return } if line == "" { continue } select { case lineCh <- line: case <-done: close(lineCh) return } } }() collectUntil := time.Time{} for { var collectCh <-chan time.Time if !collectUntil.IsZero() { collectCh = time.After(time.Until(collectUntil)) } select { case <-done: return case cmd, ok := <-d.cmds: if !ok { return } slog.Debug("serial tx", "cmd", cmd) if _, err := port.Write([]byte(cmd + "\r\n")); err != nil { slog.Error("serial write", "err", err) return } if strings.HasPrefix(cmd, "EZG OUT") { collectUntil = time.Now().Add(500 * time.Millisecond) } case line, ok := <-lineCh: if !ok { slog.Error("serial reader closed") return } d.applyLine(line) case <-collectCh: collectUntil = time.Time{} onState(d.GetStatus()) } } } func (d *Device) applyLine(line string) { slog.Debug("serial rx", "line", line) m := reOutVS.FindStringSubmatch(line) if m == nil { return } out, _ := parseInt(m[1]) in, _ := parseInt(m[2]) if out >= 1 && out <= 2 && in >= 1 && in <= 4 { d.mu.Lock() d.current[out-1] = in d.mu.Unlock() } } func (d *Device) pollLoop(done <-chan struct{}) { t := time.NewTicker(d.cfg.PollInterval) defer t.Stop() for { select { case <-done: return case <-t.C: d.poll() } } }