GUI
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user