mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
152 lines
4.8 KiB
Go
152 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
roverd "multiroombarover/pi/roverd"
|
|
|
|
"github.com/tarm/serial"
|
|
)
|
|
|
|
func main() {
|
|
var portName string
|
|
var baud int
|
|
var timeout time.Duration
|
|
var startupWait time.Duration
|
|
var controlID string
|
|
var rawValue string
|
|
|
|
flag.StringVar(&portName, "port", "", "serial device, for example /dev/ttyUSB0 or /dev/ttyACM0")
|
|
flag.IntVar(&baud, "baud", 115200, "Firmata serial baud rate")
|
|
flag.DurationVar(&timeout, "timeout", 5*time.Second, "timeout for each Firmata response")
|
|
flag.DurationVar(&startupWait, "startup-wait", 2*time.Second, "time allowed for boards that reset when the port opens")
|
|
flag.StringVar(&controlID, "control", "", "optional declared control ID to exercise")
|
|
flag.StringVar(&rawValue, "value", "", "JSON value for -control, such as 90, true, or \"hello\"")
|
|
flag.Parse()
|
|
|
|
if portName == "" {
|
|
log.Fatal("-port is required")
|
|
}
|
|
if (controlID == "") != (rawValue == "") {
|
|
log.Fatal("-control and -value must be provided together")
|
|
}
|
|
|
|
port, err := serial.OpenPort(&serial.Config{
|
|
Name: portName,
|
|
Baud: baud,
|
|
ReadTimeout: 100 * time.Millisecond,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("open %s: %v", portName, err)
|
|
}
|
|
defer port.Close()
|
|
|
|
// CH340 and native-USB development boards may reset when the host opens the
|
|
// port. Waiting here makes the same probe work with both connection styles
|
|
// without baking that diagnostic delay into the production Firmata client.
|
|
time.Sleep(startupWait)
|
|
|
|
rootContext, cancelRoot := context.WithCancel(context.Background())
|
|
defer cancelRoot()
|
|
client := roverd.NewFirmataClient(port)
|
|
client.Start(rootContext)
|
|
|
|
firmware, err := withTimeout(timeout, client.QueryFirmware)
|
|
if err != nil {
|
|
log.Fatalf("query firmware: %v", err)
|
|
}
|
|
fmt.Printf("Firmata firmware: %s %d.%d\n", firmware.Name, firmware.Major, firmware.Minor)
|
|
|
|
capabilities, err := withTimeout(timeout, client.QueryCapabilities)
|
|
if err != nil {
|
|
log.Fatalf("query capabilities: %v", err)
|
|
}
|
|
fmt.Printf("Firmata pins described: %d\n", len(capabilities))
|
|
|
|
description, err := withTimeout(timeout, client.Describe)
|
|
if err != nil {
|
|
log.Fatalf("describe rover peripheral: %v", err)
|
|
}
|
|
formatted, err := json.MarshalIndent(description, "", " ")
|
|
if err != nil {
|
|
log.Fatalf("format description: %v", err)
|
|
}
|
|
fmt.Printf("Peripheral description:\n%s\n", formatted)
|
|
|
|
if controlID != "" {
|
|
if err := exerciseControl(client, description, controlID, json.RawMessage(rawValue)); err != nil {
|
|
log.Fatalf("exercise control %q: %v", controlID, err)
|
|
}
|
|
fmt.Fprintf(os.Stdout, "Control %q accepted.\n", controlID)
|
|
}
|
|
}
|
|
|
|
// withTimeout gives every boot-time exchange its own deadline. A missing board
|
|
// therefore reports the exact handshake stage that failed instead of consuming
|
|
// one shared timeout and obscuring which response was absent.
|
|
func withTimeout[T any](timeout time.Duration, operation func(context.Context) (T, error)) (T, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
return operation(ctx)
|
|
}
|
|
|
|
func exerciseControl(client *roverd.FirmataClient, description roverd.PeripheralDescription, controlID string, rawValue json.RawMessage) error {
|
|
var selected *roverd.PeripheralControl
|
|
for index := range description.Controls {
|
|
if description.Controls[index].ID == controlID {
|
|
selected = &description.Controls[index]
|
|
break
|
|
}
|
|
}
|
|
if selected == nil {
|
|
return errors.New("control is not present in the device description")
|
|
}
|
|
|
|
var value any
|
|
if err := json.Unmarshal(rawValue, &value); err != nil {
|
|
return fmt.Errorf("parse -value as JSON: %w", err)
|
|
}
|
|
|
|
// Standard outputs deliberately use standard Firmata commands. Only custom
|
|
// callbacks use the rover-peripheral CONTROL operation, which is the central
|
|
// distinction the probe is intended to validate on real hardware.
|
|
switch selected.Output.Type {
|
|
case "custom":
|
|
return client.SendPeripheralControl(selected.ID, value)
|
|
case "digital":
|
|
enabled, ok := value.(bool)
|
|
if !ok {
|
|
return errors.New("digital control value must be true or false")
|
|
}
|
|
if selected.Output.ActiveLow {
|
|
enabled = !enabled
|
|
}
|
|
if err := client.SetPinMode(byte(*selected.Output.Pin), roverd.FirmataPinModeOutput); err != nil {
|
|
return err
|
|
}
|
|
return client.SetDigitalPin(byte(*selected.Output.Pin), enabled)
|
|
case "pwm", "servo":
|
|
number, ok := value.(float64)
|
|
if !ok || number != float64(int(number)) {
|
|
return errors.New("PWM and servo control values must be whole numbers")
|
|
}
|
|
mode := roverd.FirmataPinModePWM
|
|
if selected.Output.Type == "servo" {
|
|
mode = roverd.FirmataPinModeServo
|
|
}
|
|
if err := client.SetPinMode(byte(*selected.Output.Pin), mode); err != nil {
|
|
return err
|
|
}
|
|
return client.ExtendedAnalog(byte(*selected.Output.Pin), int(number))
|
|
default:
|
|
return fmt.Errorf("unsupported output %q", selected.Output.Type)
|
|
}
|
|
}
|