package localuart import ( "bytes" "io" "strings" "time" "go.bug.st/serial" ) const ( bufferSize = 4096 readTimeout = 50 * time.Millisecond idle = 200 * time.Millisecond silenceTimeout = 2 * time.Second // если вообще нет ответа — не виснуть ) // Port — открытый напрямую USB-UART порт (без демона). type Port struct { name string port serial.Port } // Open открывает порт по имени на заданной скорости. func Open(name string, baud int) (*Port, error) { mode := &serial.Mode{BaudRate: baud} p, err := serial.Open(name, mode) if err != nil { return nil, err } if err := p.SetReadTimeout(readTimeout); err != nil { _ = p.Close() return nil, err } return &Port{name: name, port: p}, nil } // Name возвращает имя порта (например /dev/ttyUSB0 или COM3). func (p *Port) Name() string { return p.name } // Write отправляет команду устройству (как в internal/uart.UART). func (p *Port) Write(cmd string) error { _, err := p.port.Write([]byte(strings.TrimSpace(cmd) + "\r")) return err } // ReadResponse читает ответ до паузы в потоке данных (idle), либо // возвращает ошибку, если устройство вообще не ответило за silenceTimeout — // это важно для standalone-режима: кнопка проверки не должна виснуть // навсегда, если выбранный порт "не тот". func (p *Port) ReadResponse() (string, error) { buf := make([]byte, bufferSize) var out bytes.Buffer received := false lastData := time.Now() started := time.Now() for { n, err := p.port.Read(buf) if err != nil && err != io.EOF { return "", err } if n > 0 { received = true lastData = time.Now() out.Write(buf[:n]) } if received && time.Since(lastData) >= idle { return strings.TrimSpace(out.String()), nil } if !received && time.Since(started) >= silenceTimeout { return "", io.ErrNoProgress } } } // Exec отправляет команду и возвращает ответ устройства. func (p *Port) Exec(cmd string) (string, error) { if err := p.Write(cmd); err != nil { return "", err } return p.ReadResponse() } // Close закрывает порт. func (p *Port) Close() error { return p.port.Close() }