Skip to content

Commit 1fff721

Browse files
feat: split out transports module, add tinygo uart transport (#4)
* feat: split out transports module, add tinygo uart transport * transports/mcu: corrections needed to run correctly on bare-metal microcontrollers (#7) --------- Co-authored-by: Ron Evans <ron@hybridgroup.com>
1 parent a7b9837 commit 1fff721

8 files changed

Lines changed: 306 additions & 161 deletions

File tree

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,71 @@ func main() {
570570
}
571571
```
572572

573+
### TinyGo Support through MCU UART Transport
574+
575+
```go
576+
//go:build baremetal
577+
578+
package main
579+
580+
import (
581+
"machine"
582+
583+
"context"
584+
"time"
585+
586+
"github.com/hipsterbrown/feetech-servo/feetech"
587+
)
588+
589+
func main() {
590+
if err := machine.UART0.Configure(machine.UARTConfig{
591+
BaudRate: 1000000,
592+
TX: machine.UART_TX_PIN,
593+
RX: machine.UART_RX_PIN,
594+
}); err != nil {
595+
failure("Failed to configure UART:" + err.Error())
596+
}
597+
598+
println("Starting servo example...")
599+
ctx := context.Background()
600+
601+
// Create a new servo bus
602+
bus, err := feetech.NewBus(feetech.BusConfig{
603+
Port: "0",
604+
BaudRate: 1000000,
605+
Protocol: feetech.ProtocolSTS,
606+
})
607+
if err != nil {
608+
failure("Failed to create bus:" + err.Error())
609+
}
610+
defer bus.Close()
611+
612+
// Create a servo instance (defaults to STS3215)
613+
servo := feetech.NewServo(bus, 1, nil)
614+
615+
// Detect model
616+
if err := servo.DetectModel(ctx); err != nil {
617+
failure("Failed to detect model:" + err.Error())
618+
}
619+
println("Connected to:", servo.Model().Name)
620+
621+
// Enable torque and move to center position
622+
servo.Enable(ctx)
623+
servo.SetPosition(ctx, 2048) // Center position for 12-bit servo
624+
625+
// Read current position
626+
pos, _ := servo.Position(ctx)
627+
println("Current position:", pos)
628+
}
629+
630+
func failure(msg string) {
631+
for {
632+
println(msg)
633+
time.Sleep(5 * time.Second)
634+
}
635+
}
636+
```
637+
573638
### Testing with MockTransport
574639

575640
```go

_examples/mock_transport/example.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import (
66
"log"
77

88
"github.com/hipsterbrown/feetech-servo/feetech"
9+
"github.com/hipsterbrown/feetech-servo/transports"
910
)
1011

1112
func main() {
1213
// For testing without hardware
13-
mock := &feetech.MockTransport{
14+
mock := &transports.MockTransport{
1415
// Pre-load a ping response
1516
ReadData: []byte{0xFF, 0xFF, 0x01, 0x02, 0x00, 0xFC},
1617
}

feetech/bus.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"sync"
88
"time"
9+
10+
"github.com/hipsterbrown/feetech-servo/transports"
911
)
1012

1113
// Bus manages communication with servos on a Feetech bus.
@@ -63,7 +65,7 @@ func NewBus(cfg BusConfig) (*Bus, error) {
6365
return nil, errors.New("either Transport or Port must be specified")
6466
}
6567
var err error
66-
transport, err = OpenSerial(SerialConfig{
68+
transport, err = transports.OpenSerial(transports.SerialConfig{
6769
Port: cfg.Port,
6870
BaudRate: cfg.BaudRate,
6971
Timeout: cfg.Timeout,

feetech/bus_test.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"context"
66
"testing"
77
"time"
8+
9+
"github.com/hipsterbrown/feetech-servo/transports"
810
)
911

1012
func TestBus_Ping(t *testing.T) {
1113
// Set up mock with ping response + model number read response
12-
mock := &MockTransport{}
14+
mock := &transports.MockTransport{}
1315
readIdx := 0
1416
responses := [][]byte{
1517
{0xFF, 0xFF, 0x01, 0x02, 0x00, 0xFC}, // Ping response
@@ -55,7 +57,7 @@ func TestBus_Ping(t *testing.T) {
5557

5658
func TestBus_ReadRegister(t *testing.T) {
5759
// Mock response for reading 2 bytes
58-
mock := &MockTransport{
60+
mock := &transports.MockTransport{
5961
ReadData: []byte{0xFF, 0xFF, 0x01, 0x04, 0x00, 0x00, 0x08, 0xF2}, // Position 2048
6062
}
6163

@@ -82,7 +84,7 @@ func TestBus_ReadRegister(t *testing.T) {
8284
}
8385

8486
func TestBus_WriteRegister(t *testing.T) {
85-
mock := &MockTransport{
87+
mock := &transports.MockTransport{
8688
ReadData: []byte{0xFF, 0xFF, 0x01, 0x02, 0x00, 0xFC}, // Ack response
8789
}
8890

@@ -109,7 +111,7 @@ func TestBus_WriteRegister(t *testing.T) {
109111
}
110112

111113
func TestBus_SyncWrite(t *testing.T) {
112-
mock := &MockTransport{}
114+
mock := &transports.MockTransport{}
113115

114116
bus, _ := NewBus(BusConfig{
115117
Transport: mock,
@@ -139,7 +141,7 @@ func TestBus_SyncWrite(t *testing.T) {
139141

140142
func TestBus_SyncRead(t *testing.T) {
141143
// Mock two servo responses
142-
mock := &MockTransport{
144+
mock := &transports.MockTransport{
143145
ReadData: []byte{
144146
0xFF, 0xFF, 0x01, 0x04, 0x00, 0x00, 0x08, 0xF2, // ID 1, position 2048
145147
0xFF, 0xFF, 0x02, 0x04, 0x00, 0x00, 0x04, 0xF5, // ID 2, position 1024
@@ -176,7 +178,7 @@ func TestBus_SyncRead(t *testing.T) {
176178
}
177179

178180
func TestBus_SyncRead_SCSUnsupported(t *testing.T) {
179-
mock := &MockTransport{}
181+
mock := &transports.MockTransport{}
180182

181183
bus, _ := NewBus(BusConfig{
182184
Transport: mock,
@@ -192,7 +194,7 @@ func TestBus_SyncRead_SCSUnsupported(t *testing.T) {
192194
}
193195

194196
func TestBus_InvalidID(t *testing.T) {
195-
mock := &MockTransport{}
197+
mock := &transports.MockTransport{}
196198
bus, _ := NewBus(BusConfig{Transport: mock})
197199
defer bus.Close()
198200

@@ -211,7 +213,7 @@ func TestBus_InvalidID(t *testing.T) {
211213
}
212214

213215
func TestBus_Close(t *testing.T) {
214-
mock := &MockTransport{}
216+
mock := &transports.MockTransport{}
215217
bus, _ := NewBus(BusConfig{Transport: mock})
216218

217219
err := bus.Close()
@@ -230,7 +232,7 @@ func TestBus_Close(t *testing.T) {
230232
}
231233

232234
func TestBus_ClosedOperations(t *testing.T) {
233-
mock := &MockTransport{}
235+
mock := &transports.MockTransport{}
234236
bus, _ := NewBus(BusConfig{Transport: mock})
235237
bus.Close()
236238

@@ -243,7 +245,7 @@ func TestBus_ClosedOperations(t *testing.T) {
243245
}
244246

245247
func TestServo_Position(t *testing.T) {
246-
mock := &MockTransport{
248+
mock := &transports.MockTransport{
247249
ReadData: []byte{0xFF, 0xFF, 0x01, 0x04, 0x00, 0x00, 0x08, 0xF2},
248250
}
249251

@@ -267,7 +269,7 @@ func TestServo_Position(t *testing.T) {
267269
}
268270

269271
func TestServo_SetPosition(t *testing.T) {
270-
mock := &MockTransport{
272+
mock := &transports.MockTransport{
271273
ReadData: []byte{0xFF, 0xFF, 0x01, 0x02, 0x00, 0xFC},
272274
}
273275

@@ -294,7 +296,7 @@ func TestServo_SetPosition(t *testing.T) {
294296
}
295297

296298
func TestServo_TorqueEnable(t *testing.T) {
297-
mock := &MockTransport{
299+
mock := &transports.MockTransport{
298300
ReadData: []byte{0xFF, 0xFF, 0x01, 0x02, 0x00, 0xFC},
299301
}
300302

@@ -323,7 +325,7 @@ func TestServo_TorqueEnable(t *testing.T) {
323325

324326
func TestBus_ContextCancellation(t *testing.T) {
325327
// Simulate slow transport
326-
mock := &MockTransport{
328+
mock := &transports.MockTransport{
327329
ReadFunc: func(p []byte) (int, error) {
328330
time.Sleep(500 * time.Millisecond)
329331
return 0, nil

feetech/transport.go

Lines changed: 0 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package feetech
22

33
import (
4-
"errors"
5-
"fmt"
64
"io"
75
"time"
8-
9-
"go.bug.st/serial"
106
)
117

128
// Transport is the interface for low-level communication with the servo bus.
@@ -20,145 +16,3 @@ type Transport interface {
2016
// Flush discards any buffered input data.
2117
Flush() error
2218
}
23-
24-
// SerialTransport implements Transport using a hardware serial port.
25-
type SerialTransport struct {
26-
port serial.Port
27-
portName string
28-
timeout time.Duration
29-
}
30-
31-
// SerialConfig holds configuration for opening a serial port.
32-
type SerialConfig struct {
33-
Port string
34-
BaudRate int
35-
Timeout time.Duration
36-
}
37-
38-
// OpenSerial opens a serial port with the given configuration.
39-
func OpenSerial(cfg SerialConfig) (*SerialTransport, error) {
40-
if cfg.Port == "" {
41-
return nil, errors.New("serial port path is required")
42-
}
43-
44-
if cfg.BaudRate == 0 {
45-
cfg.BaudRate = 1000000
46-
}
47-
48-
if cfg.Timeout == 0 {
49-
cfg.Timeout = time.Second
50-
}
51-
52-
mode := &serial.Mode{
53-
BaudRate: cfg.BaudRate,
54-
DataBits: 8,
55-
Parity: serial.NoParity,
56-
StopBits: serial.OneStopBit,
57-
}
58-
59-
port, err := serial.Open(cfg.Port, mode)
60-
if err != nil {
61-
return nil, fmt.Errorf("failed to open serial port: %w", err)
62-
}
63-
64-
if err := port.SetReadTimeout(cfg.Timeout); err != nil {
65-
port.Close()
66-
return nil, fmt.Errorf("failed to set read timeout: %w", err)
67-
}
68-
69-
return &SerialTransport{
70-
port: port,
71-
portName: cfg.Port,
72-
timeout: cfg.Timeout,
73-
}, nil
74-
}
75-
76-
func (t *SerialTransport) Read(p []byte) (int, error) {
77-
return t.port.Read(p)
78-
}
79-
80-
func (t *SerialTransport) Write(p []byte) (int, error) {
81-
return t.port.Write(p)
82-
}
83-
84-
func (t *SerialTransport) Close() error {
85-
return t.port.Close()
86-
}
87-
88-
func (t *SerialTransport) SetReadTimeout(timeout time.Duration) error {
89-
t.timeout = timeout
90-
return t.port.SetReadTimeout(timeout)
91-
}
92-
93-
func (t *SerialTransport) Flush() error {
94-
// Read and discard any buffered data
95-
buf := make([]byte, 4096)
96-
t.port.SetReadTimeout(10 * time.Millisecond)
97-
for {
98-
n, err := t.port.Read(buf)
99-
if n == 0 || err != nil {
100-
break
101-
}
102-
}
103-
// Restore original timeout
104-
t.port.SetReadTimeout(t.timeout)
105-
return nil
106-
}
107-
108-
// PortName returns the serial port name.
109-
func (t *SerialTransport) PortName() string {
110-
return t.portName
111-
}
112-
113-
// MockTransport implements Transport for testing.
114-
type MockTransport struct {
115-
ReadData []byte
116-
ReadErr error
117-
WriteData []byte
118-
WriteErr error
119-
Closed bool
120-
ReadTimeout time.Duration
121-
Flushed bool
122-
123-
// ReadFunc allows custom read behavior for complex tests
124-
ReadFunc func(p []byte) (int, error)
125-
}
126-
127-
func (m *MockTransport) Read(p []byte) (int, error) {
128-
if m.ReadFunc != nil {
129-
return m.ReadFunc(p)
130-
}
131-
if m.ReadErr != nil {
132-
return 0, m.ReadErr
133-
}
134-
n := copy(p, m.ReadData)
135-
m.ReadData = m.ReadData[n:]
136-
if n == 0 {
137-
return 0, io.EOF
138-
}
139-
return n, nil
140-
}
141-
142-
func (m *MockTransport) Write(p []byte) (int, error) {
143-
if m.WriteErr != nil {
144-
return 0, m.WriteErr
145-
}
146-
m.WriteData = append(m.WriteData, p...)
147-
return len(p), nil
148-
}
149-
150-
func (m *MockTransport) Close() error {
151-
m.Closed = true
152-
return nil
153-
}
154-
155-
func (m *MockTransport) SetReadTimeout(timeout time.Duration) error {
156-
m.ReadTimeout = timeout
157-
return nil
158-
}
159-
160-
func (m *MockTransport) Flush() error {
161-
m.Flushed = true
162-
// Don't clear ReadData - tests need to preserve mock response data
163-
return nil
164-
}

0 commit comments

Comments
 (0)