This commit is contained in:
2026-09-03 13:08:01 +03:00
parent d5783423d6
commit 946c1beabf
41 changed files with 3558 additions and 103 deletions
+70
View File
@@ -0,0 +1,70 @@
// Package localuart находит и открывает USB-UART адаптер напрямую, без
// демона — для standalone-версии приложения (internal/localctl).
package localuart
import (
"fmt"
"sort"
"strings"
"go.bug.st/serial/enumerator"
)
// knownVID — VID распространённых чипов USB-UART мостов. Порты с таким VID
// считаются вероятным переходником "UART to USB" и предлагаются первыми.
var knownVID = map[string]bool{
"0403": true, // FTDI
"1A86": true, // WCH CH340 / CH341
"10C4": true, // Silicon Labs CP210x
"067B": true, // Prolific PL2303
}
// Candidate — один найденный USB-serial порт.
type Candidate struct {
Name string
VID string
PID string
}
// ListCandidates возвращает USB-serial порты, обнаруженные в системе;
// порты с известным VID переходника идут первыми.
func ListCandidates() ([]Candidate, error) {
ports, err := enumerator.GetDetailedPortsList()
if err != nil {
return nil, err
}
var known, other []Candidate
for _, p := range ports {
if !p.IsUSB {
continue
}
c := Candidate{
Name: p.Name,
VID: strings.ToUpper(p.VID),
PID: strings.ToUpper(p.PID),
}
if knownVID[c.VID] {
known = append(known, c)
} else {
other = append(other, c)
}
}
sort.Slice(known, func(i, j int) bool { return known[i].Name < known[j].Name })
sort.Slice(other, func(i, j int) bool { return other[i].Name < other[j].Name })
return append(known, other...), nil
}
// PickPort возвращает наиболее вероятный порт UART-to-USB переходника.
func PickPort() (string, error) {
candidates, err := ListCandidates()
if err != nil {
return "", fmt.Errorf("не удалось получить список портов: %w", err)
}
if len(candidates) == 0 {
return "", fmt.Errorf("USB-UART адаптер не найден")
}
return candidates[0].Name, nil
}
+91
View File
@@ -0,0 +1,91 @@
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()
}