feat(tools): add cross-platform serial hardware tool

This commit is contained in:
SiYue-ZO
2026-04-26 12:43:50 +08:00
parent ed687d62ae
commit 0f52076762
9 changed files with 943 additions and 2 deletions
+3
View File
@@ -9,6 +9,9 @@ func TestFacadeConstructorsRemainAvailable(t *testing.T) {
if NewSPITool() == nil {
t.Fatal("NewSPITool should return a tool")
}
if NewSerialTool() == nil {
t.Fatal("NewSerialTool should return a tool")
}
if NewMessageTool() == nil {
t.Fatal("NewMessageTool should return a tool")
}
+332
View File
@@ -0,0 +1,332 @@
package hardwaretools
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"unicode/utf8"
)
const (
defaultSerialBaud = 115200
defaultSerialDataBits = 8
defaultSerialStopBits = 1
defaultSerialTimeoutMS = 1000
maxSerialPayloadBytes = 4096
maxSerialReadBytes = 4096
)
type SerialTool struct{}
type serialPortInfo struct {
Name string `json:"name"`
Path string `json:"path"`
}
type serialConfig struct {
Port string
Baud int
DataBits int
Parity string
StopBits int
}
func NewSerialTool() *SerialTool {
return &SerialTool{}
}
func (t *SerialTool) Name() string {
return "serial"
}
func (t *SerialTool) Description() string {
return "Interact with host serial ports. Actions: list (enumerate ports), read (receive bytes), write (send bytes with explicit confirmation). Supports Linux, macOS, and Windows."
}
func (t *SerialTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"enum": []string{"list", "read", "write"},
"description": "Action to perform: list available serial ports, read bytes from a port, or write bytes to a port.",
},
"port": map[string]any{
"type": "string",
"description": "Serial port path or name, for example /dev/ttyUSB0, /dev/cu.usbserial-0001, or COM3. Required for read/write.",
},
"baud": map[string]any{
"type": "integer",
"description": "Baud rate. Default: 115200.",
},
"data_bits": map[string]any{
"type": "integer",
"description": "Data bits. Supported values: 5, 6, 7, 8. Default: 8.",
},
"parity": map[string]any{
"type": "string",
"enum": []string{"none", "even", "odd"},
"description": "Parity mode. Default: none.",
},
"stop_bits": map[string]any{
"type": "integer",
"description": "Stop bits. Supported values: 1, 2. Default: 1.",
},
"timeout_ms": map[string]any{
"type": "integer",
"description": "Read/write timeout in milliseconds. Default: 1000.",
},
"length": map[string]any{
"type": "integer",
"description": "Number of bytes to read. Required for read. Range: 1-4096.",
},
"data": map[string]any{
"type": "array",
"items": map[string]any{"type": "integer"},
"description": "Bytes to write, each in range 0-255. Required for write unless text is provided.",
},
"text": map[string]any{
"type": "string",
"description": "UTF-8 text to write. Required for write if data is omitted.",
},
"confirm": map[string]any{
"type": "boolean",
"description": "Must be true for write operations.",
},
},
"required": []string{"action"},
}
}
func (t *SerialTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
action, ok := args["action"].(string)
if !ok || strings.TrimSpace(action) == "" {
return ErrorResult("action is required")
}
switch action {
case "list":
return t.list()
case "read":
return t.read(args)
case "write":
return t.write(args)
default:
return ErrorResult(fmt.Sprintf("unknown action: %s (valid: list, read, write)", action))
}
}
func (t *SerialTool) list() *ToolResult {
ports, err := serialListPorts()
if err != nil {
return ErrorResult(fmt.Sprintf("failed to list serial ports: %v", err))
}
if len(ports) == 0 {
return SilentResult("No serial ports found on this host.")
}
result, _ := json.MarshalIndent(map[string]any{
"ports": ports,
"count": len(ports),
}, "", " ")
return SilentResult(string(result))
}
func (t *SerialTool) read(args map[string]any) *ToolResult {
cfg, errResult := parseSerialConfig(args)
if errResult != nil {
return errResult
}
length := 0
if v, ok := args["length"].(float64); ok {
length = int(v)
}
if length < 1 || length > maxSerialReadBytes {
return ErrorResult(fmt.Sprintf("length is required for read (1-%d)", maxSerialReadBytes))
}
timeout, errResult := parseSerialTimeout(args)
if errResult != nil {
return errResult
}
data, err := serialRead(cfg, length, timeout)
if err != nil {
return ErrorResult(fmt.Sprintf("serial read failed on %s: %v", cfg.Port, err))
}
return SilentResult(formatSerialPayload("read", cfg, data, timeout))
}
func (t *SerialTool) write(args map[string]any) *ToolResult {
confirm, _ := args["confirm"].(bool)
if !confirm {
return ErrorResult(
"write operations require confirm: true. Please confirm with the user before sending bytes to a serial device.",
)
}
cfg, errResult := parseSerialConfig(args)
if errResult != nil {
return errResult
}
timeout, errResult := parseSerialTimeout(args)
if errResult != nil {
return errResult
}
payload, errResult := parseSerialWritePayload(args)
if errResult != nil {
return errResult
}
written, err := serialWrite(cfg, payload, timeout)
if err != nil {
return ErrorResult(fmt.Sprintf("serial write failed on %s: %v", cfg.Port, err))
}
result, _ := json.MarshalIndent(map[string]any{
"action": "write",
"port": cfg.Port,
"baud": cfg.Baud,
"data_bits": cfg.DataBits,
"parity": cfg.Parity,
"stop_bits": cfg.StopBits,
"timeout_ms": timeout.Milliseconds(),
"written": written,
"payload": serialPayloadSummary(payload),
}, "", " ")
return SilentResult(string(result))
}
func parseSerialConfig(args map[string]any) (serialConfig, *ToolResult) {
port, ok := args["port"].(string)
port = strings.TrimSpace(port)
if !ok || port == "" {
return serialConfig{}, ErrorResult("port is required (for example /dev/ttyUSB0, /dev/cu.usbserial-0001, or COM3)")
}
cfg := serialConfig{
Port: port,
Baud: defaultSerialBaud,
DataBits: defaultSerialDataBits,
Parity: "none",
StopBits: defaultSerialStopBits,
}
if v, ok := args["baud"].(float64); ok {
cfg.Baud = int(v)
}
if cfg.Baud < 50 || cfg.Baud > 4000000 {
return serialConfig{}, ErrorResult("baud must be between 50 and 4000000")
}
if v, ok := args["data_bits"].(float64); ok {
cfg.DataBits = int(v)
}
switch cfg.DataBits {
case 5, 6, 7, 8:
default:
return serialConfig{}, ErrorResult("data_bits must be one of 5, 6, 7, or 8")
}
if v, ok := args["parity"].(string); ok && strings.TrimSpace(v) != "" {
cfg.Parity = strings.ToLower(strings.TrimSpace(v))
}
switch cfg.Parity {
case "none", "even", "odd":
default:
return serialConfig{}, ErrorResult(`parity must be one of "none", "even", or "odd"`)
}
if v, ok := args["stop_bits"].(float64); ok {
cfg.StopBits = int(v)
}
if cfg.StopBits != 1 && cfg.StopBits != 2 {
return serialConfig{}, ErrorResult("stop_bits must be 1 or 2")
}
return cfg, nil
}
func parseSerialTimeout(args map[string]any) (time.Duration, *ToolResult) {
timeoutMS := defaultSerialTimeoutMS
if v, ok := args["timeout_ms"].(float64); ok {
timeoutMS = int(v)
}
if timeoutMS < 1 || timeoutMS > 60000 {
return 0, ErrorResult("timeout_ms must be between 1 and 60000")
}
return time.Duration(timeoutMS) * time.Millisecond, nil
}
func parseSerialWritePayload(args map[string]any) ([]byte, *ToolResult) {
if text, ok := args["text"].(string); ok && text != "" {
if !utf8.ValidString(text) {
return nil, ErrorResult("text must be valid UTF-8")
}
if len(text) > maxSerialPayloadBytes {
return nil, ErrorResult(fmt.Sprintf("text payload too large: maximum %d bytes", maxSerialPayloadBytes))
}
return []byte(text), nil
}
dataRaw, ok := args["data"].([]any)
if !ok || len(dataRaw) == 0 {
return nil, ErrorResult("write requires either text or data")
}
if len(dataRaw) > maxSerialPayloadBytes {
return nil, ErrorResult(fmt.Sprintf("data too long: maximum %d bytes", maxSerialPayloadBytes))
}
data := make([]byte, len(dataRaw))
for i, v := range dataRaw {
f, ok := v.(float64)
if !ok {
return nil, ErrorResult(fmt.Sprintf("data[%d] is not a valid byte value", i))
}
b := int(f)
if b < 0 || b > 255 {
return nil, ErrorResult(fmt.Sprintf("data[%d] = %d is out of byte range (0-255)", i, b))
}
data[i] = byte(b)
}
return data, nil
}
func formatSerialPayload(action string, cfg serialConfig, data []byte, timeout time.Duration) string {
result, _ := json.MarshalIndent(map[string]any{
"action": action,
"port": cfg.Port,
"baud": cfg.Baud,
"data_bits": cfg.DataBits,
"parity": cfg.Parity,
"stop_bits": cfg.StopBits,
"timeout_ms": timeout.Milliseconds(),
"payload": serialPayloadSummary(data),
}, "", " ")
return string(result)
}
func serialPayloadSummary(data []byte) map[string]any {
hexValues := make([]string, len(data))
intValues := make([]int, len(data))
for i, b := range data {
hexValues[i] = fmt.Sprintf("0x%02x", b)
intValues[i] = int(b)
}
summary := map[string]any{
"length": len(data),
"bytes": intValues,
"hex": hexValues,
}
if utf8.Valid(data) {
summary["text"] = string(data)
}
return summary
}
+19
View File
@@ -0,0 +1,19 @@
//go:build darwin
package hardwaretools
import "golang.org/x/sys/unix"
func serialGetTermios(fd int) (*unix.Termios, error) {
return unix.IoctlGetTermios(fd, unix.TIOCGETA)
}
func serialSetSpeed(tio *unix.Termios, speed uint32) error {
tio.Ispeed = speed
tio.Ospeed = speed
return nil
}
func serialSetTermios(fd int, tio *unix.Termios) error {
return unix.IoctlSetTermios(fd, unix.TIOCSETA, tio)
}
+19
View File
@@ -0,0 +1,19 @@
//go:build linux
package hardwaretools
import "golang.org/x/sys/unix"
func serialGetTermios(fd int) (*unix.Termios, error) {
return unix.IoctlGetTermios(fd, unix.TCGETS)
}
func serialSetSpeed(tio *unix.Termios, speed uint32) error {
tio.Ispeed = speed
tio.Ospeed = speed
return nil
}
func serialSetTermios(fd int, tio *unix.Termios) error {
return unix.IoctlSetTermios(fd, unix.TCSETS, tio)
}
+20
View File
@@ -0,0 +1,20 @@
//go:build !linux && !darwin && !windows
package hardwaretools
import (
"fmt"
"time"
)
func serialListPorts() ([]serialPortInfo, error) {
return nil, nil
}
func serialRead(cfg serialConfig, length int, timeout time.Duration) ([]byte, error) {
return nil, fmt.Errorf("serial is not supported on this platform")
}
func serialWrite(cfg serialConfig, data []byte, timeout time.Duration) (int, error) {
return 0, fmt.Errorf("serial is not supported on this platform")
}
+66
View File
@@ -0,0 +1,66 @@
package hardwaretools
import (
"testing"
"time"
)
func TestParseSerialConfig(t *testing.T) {
cfg, errResult := parseSerialConfig(map[string]any{
"port": "COM3",
"baud": float64(9600),
"data_bits": float64(7),
"parity": "even",
"stop_bits": float64(2),
})
if errResult != nil {
t.Fatalf("parseSerialConfig() unexpected error = %v", errResult.ForLLM)
}
if cfg.Port != "COM3" || cfg.Baud != 9600 || cfg.DataBits != 7 || cfg.Parity != "even" || cfg.StopBits != 2 {
t.Fatalf("parseSerialConfig() = %#v", cfg)
}
}
func TestParseSerialConfigRejectsInvalidParity(t *testing.T) {
_, errResult := parseSerialConfig(map[string]any{
"port": "/dev/ttyUSB0",
"parity": "mark",
})
if errResult == nil {
t.Fatal("expected invalid parity to fail")
}
}
func TestParseSerialTimeout(t *testing.T) {
timeout, errResult := parseSerialTimeout(map[string]any{
"timeout_ms": float64(2500),
})
if errResult != nil {
t.Fatalf("parseSerialTimeout() unexpected error = %v", errResult.ForLLM)
}
if timeout != 2500*time.Millisecond {
t.Fatalf("timeout = %v, want 2500ms", timeout)
}
}
func TestParseSerialWritePayloadSupportsText(t *testing.T) {
data, errResult := parseSerialWritePayload(map[string]any{
"text": "AT\r\n",
})
if errResult != nil {
t.Fatalf("parseSerialWritePayload() unexpected error = %v", errResult.ForLLM)
}
if string(data) != "AT\r\n" {
t.Fatalf("payload = %q, want %q", string(data), "AT\r\n")
}
}
func TestParseSerialWritePayloadRejectsOutOfRangeByte(t *testing.T) {
_, errResult := parseSerialWritePayload(map[string]any{
"data": []any{float64(256)},
})
if errResult == nil {
t.Fatal("expected payload validation failure")
}
}
+263
View File
@@ -0,0 +1,263 @@
//go:build linux || darwin
package hardwaretools
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"golang.org/x/sys/unix"
)
func serialListPorts() ([]serialPortInfo, error) {
patterns := []string{
"/dev/ttyS*",
"/dev/ttyUSB*",
"/dev/ttyACM*",
"/dev/ttyAMA*",
"/dev/rfcomm*",
"/dev/tty.*",
"/dev/cu.*",
}
seen := make(map[string]struct{})
ports := make([]serialPortInfo, 0)
for _, pattern := range patterns {
matches, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
for _, match := range matches {
if _, ok := seen[match]; ok {
continue
}
info, err := os.Stat(match)
if err != nil || info.IsDir() {
continue
}
seen[match] = struct{}{}
ports = append(ports, serialPortInfo{
Name: filepath.Base(match),
Path: match,
})
}
}
sort.Slice(ports, func(i, j int) bool {
return ports[i].Path < ports[j].Path
})
return ports, nil
}
func serialRead(cfg serialConfig, length int, timeout time.Duration) ([]byte, error) {
fd, err := openAndConfigureSerialPort(cfg)
if err != nil {
return nil, err
}
defer unix.Close(fd)
buf := make([]byte, length)
total := 0
deadline := time.Now().Add(timeout)
for total < length {
remaining := time.Until(deadline)
if remaining <= 0 {
break
}
n, err := pollRead(fd, buf[total:], remaining)
if err != nil {
return nil, err
}
if n == 0 {
break
}
total += n
}
return buf[:total], nil
}
func serialWrite(cfg serialConfig, data []byte, timeout time.Duration) (int, error) {
fd, err := openAndConfigureSerialPort(cfg)
if err != nil {
return 0, err
}
defer unix.Close(fd)
total := 0
deadline := time.Now().Add(timeout)
for total < len(data) {
remaining := time.Until(deadline)
if remaining <= 0 {
return total, fmt.Errorf("timeout while writing serial data")
}
n, err := pollWrite(fd, data[total:], remaining)
if err != nil {
return total, err
}
if n == 0 {
return total, fmt.Errorf("serial port accepted zero bytes")
}
total += n
}
return total, nil
}
func openAndConfigureSerialPort(cfg serialConfig) (int, error) {
fd, err := unix.Open(normalizeUnixSerialPath(cfg.Port), unix.O_RDWR|unix.O_NOCTTY|unix.O_NONBLOCK, 0)
if err != nil {
return -1, err
}
if err := unix.SetNonblock(fd, false); err != nil {
unix.Close(fd)
return -1, err
}
if err := configureUnixSerialPort(fd, cfg); err != nil {
unix.Close(fd)
return -1, err
}
return fd, nil
}
func configureUnixSerialPort(fd int, cfg serialConfig) error {
tio, err := serialGetTermios(fd)
if err != nil {
return err
}
tio.Iflag = 0
tio.Oflag = 0
tio.Lflag = 0
tio.Cflag = unix.CREAD | unix.CLOCAL
tio.Cc[unix.VMIN] = 0
tio.Cc[unix.VTIME] = 0
switch cfg.DataBits {
case 5:
tio.Cflag |= unix.CS5
case 6:
tio.Cflag |= unix.CS6
case 7:
tio.Cflag |= unix.CS7
default:
tio.Cflag |= unix.CS8
}
switch cfg.Parity {
case "even":
tio.Cflag |= unix.PARENB
case "odd":
tio.Cflag |= unix.PARENB | unix.PARODD
}
if cfg.StopBits == 2 {
tio.Cflag |= unix.CSTOPB
}
speed, err := serialBaudToUnix(cfg.Baud)
if err != nil {
return err
}
if err := serialSetSpeed(tio, speed); err != nil {
return err
}
return serialSetTermios(fd, tio)
}
func serialBaudToUnix(baud int) (uint32, error) {
switch baud {
case 50:
return unix.B50, nil
case 75:
return unix.B75, nil
case 110:
return unix.B110, nil
case 134:
return unix.B134, nil
case 150:
return unix.B150, nil
case 200:
return unix.B200, nil
case 300:
return unix.B300, nil
case 600:
return unix.B600, nil
case 1200:
return unix.B1200, nil
case 1800:
return unix.B1800, nil
case 2400:
return unix.B2400, nil
case 4800:
return unix.B4800, nil
case 9600:
return unix.B9600, nil
case 19200:
return unix.B19200, nil
case 38400:
return unix.B38400, nil
case 57600:
return unix.B57600, nil
case 115200:
return unix.B115200, nil
case 230400:
return unix.B230400, nil
default:
return 0, fmt.Errorf("unsupported baud rate on this platform: %d", baud)
}
}
func pollRead(fd int, dst []byte, timeout time.Duration) (int, error) {
pfd := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
n, err := unix.Poll(pfd, durationToPollTimeout(timeout))
if err != nil {
return 0, err
}
if n == 0 {
return 0, nil
}
return unix.Read(fd, dst)
}
func pollWrite(fd int, src []byte, timeout time.Duration) (int, error) {
pfd := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLOUT}}
n, err := unix.Poll(pfd, durationToPollTimeout(timeout))
if err != nil {
return 0, err
}
if n == 0 {
return 0, nil
}
return unix.Write(fd, src)
}
func durationToPollTimeout(timeout time.Duration) int {
if timeout <= 0 {
return 0
}
ms := int(timeout / time.Millisecond)
if ms == 0 {
return 1
}
return ms
}
func normalizeUnixSerialPath(port string) string {
trimmed := strings.TrimSpace(port)
if strings.HasPrefix(trimmed, "/dev/") {
return trimmed
}
return "/dev/" + trimmed
}
+214
View File
@@ -0,0 +1,214 @@
//go:build windows
package hardwaretools
import (
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"unsafe"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procGetCommState = kernel32.NewProc("GetCommState")
procSetCommState = kernel32.NewProc("SetCommState")
procSetCommTimeouts = kernel32.NewProc("SetCommTimeouts")
procPurgeComm = kernel32.NewProc("PurgeComm")
)
const (
purgeTxClear = 0x0004
purgeRxClear = 0x0008
)
type dcb struct {
DCBlength uint32
BaudRate uint32
Flags uint32
Reserved uint16
XonLim uint16
XoffLim uint16
ByteSize byte
Parity byte
StopBits byte
XonChar byte
XoffChar byte
ErrorChar byte
EofChar byte
EvtChar byte
wReserved1 uint16
}
type commTimeouts struct {
ReadIntervalTimeout uint32
ReadTotalTimeoutMultiplier uint32
ReadTotalTimeoutConstant uint32
WriteTotalTimeoutMultiplier uint32
WriteTotalTimeoutConstant uint32
}
func serialListPorts() ([]serialPortInfo, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `HARDWARE\DEVICEMAP\SERIALCOMM`, registry.QUERY_VALUE)
if err != nil {
if err == registry.ErrNotExist {
return nil, nil
}
return nil, err
}
defer key.Close()
names, err := key.ReadValueNames(-1)
if err != nil {
return nil, err
}
ports := make([]serialPortInfo, 0, len(names))
seen := make(map[string]struct{})
for _, name := range names {
value, _, err := key.GetStringValue(name)
if err != nil {
continue
}
portName := strings.TrimSpace(value)
if portName == "" {
continue
}
normalized := strings.ToUpper(portName)
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
ports = append(ports, serialPortInfo{
Name: normalized,
Path: normalized,
})
}
sort.Slice(ports, func(i, j int) bool {
return ports[i].Path < ports[j].Path
})
return ports, nil
}
func serialRead(cfg serialConfig, length int, timeout time.Duration) ([]byte, error) {
handle, err := openAndConfigureWindowsSerial(cfg, timeout)
if err != nil {
return nil, err
}
defer windows.CloseHandle(handle)
buf := make([]byte, length)
var read uint32
if err := windows.ReadFile(handle, buf, &read, nil); err != nil {
return nil, err
}
return buf[:read], nil
}
func serialWrite(cfg serialConfig, data []byte, timeout time.Duration) (int, error) {
handle, err := openAndConfigureWindowsSerial(cfg, timeout)
if err != nil {
return 0, err
}
defer windows.CloseHandle(handle)
var written uint32
if err := windows.WriteFile(handle, data, &written, nil); err != nil {
return int(written), err
}
return int(written), nil
}
func openAndConfigureWindowsSerial(cfg serialConfig, timeout time.Duration) (windows.Handle, error) {
path := normalizeWindowsSerialPath(cfg.Port)
handle, err := windows.CreateFile(
windows.StringToUTF16Ptr(path),
windows.GENERIC_READ|windows.GENERIC_WRITE,
0,
nil,
windows.OPEN_EXISTING,
0,
0,
)
if err != nil {
return 0, err
}
if err := configureWindowsSerialPort(handle, cfg, timeout); err != nil {
windows.CloseHandle(handle)
return 0, err
}
return handle, nil
}
func configureWindowsSerialPort(handle windows.Handle, cfg serialConfig, timeout time.Duration) error {
state := dcb{DCBlength: uint32(unsafe.Sizeof(dcb{}))}
r1, _, err := procGetCommState.Call(uintptr(handle), uintptr(unsafe.Pointer(&state)))
if r1 == 0 {
return err
}
state.BaudRate = uint32(cfg.Baud)
state.ByteSize = byte(cfg.DataBits)
state.Flags |= 0x00000001 // fBinary
switch cfg.Parity {
case "even":
state.Parity = 2
state.Flags |= 0x00000002 // fParity
case "odd":
state.Parity = 1
state.Flags |= 0x00000002 // fParity
default:
state.Parity = 0
state.Flags &^= 0x00000002
}
switch cfg.StopBits {
case 2:
state.StopBits = 2
default:
state.StopBits = 0
}
r1, _, err = procSetCommState.Call(uintptr(handle), uintptr(unsafe.Pointer(&state)))
if r1 == 0 {
return err
}
timeoutMS := uint32(timeout / time.Millisecond)
if timeoutMS == 0 {
timeoutMS = 1
}
timeouts := commTimeouts{
ReadIntervalTimeout: timeoutMS,
ReadTotalTimeoutConstant: timeoutMS,
WriteTotalTimeoutConstant: timeoutMS,
ReadTotalTimeoutMultiplier: 0,
WriteTotalTimeoutMultiplier: 0,
}
r1, _, err = procSetCommTimeouts.Call(uintptr(handle), uintptr(unsafe.Pointer(&timeouts)))
if r1 == 0 {
return err
}
procPurgeComm.Call(uintptr(handle), uintptr(purgeRxClear|purgeTxClear))
return nil
}
func normalizeWindowsSerialPath(port string) string {
trimmed := strings.ToUpper(strings.TrimSpace(port))
if strings.HasPrefix(trimmed, `\\.\`) {
return trimmed
}
if filepath.VolumeName(trimmed) != "" {
return trimmed
}
return `\\.\` + trimmed
}
+7 -2
View File
@@ -3,8 +3,9 @@ package tools
import hardwaretools "github.com/sipeed/picoclaw/pkg/tools/hardware"
type (
I2CTool = hardwaretools.I2CTool
SPITool = hardwaretools.SPITool
I2CTool = hardwaretools.I2CTool
SerialTool = hardwaretools.SerialTool
SPITool = hardwaretools.SPITool
)
func NewI2CTool() *I2CTool {
@@ -14,3 +15,7 @@ func NewI2CTool() *I2CTool {
func NewSPITool() *SPITool {
return hardwaretools.NewSPITool()
}
func NewSerialTool() *SerialTool {
return hardwaretools.NewSerialTool()
}