mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
165 lines
6.5 KiB
Go
165 lines
6.5 KiB
Go
package roverd
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Both physical servo backends consume these exact motion constants. Keeping
|
|
// them in shared code prevents Pi PWM and ESP32 Firmata movement from drifting
|
|
// apart as either implementation evolves.
|
|
const (
|
|
maxServoDegPerSec = 60.0
|
|
servoStepInterval = 20 * time.Millisecond
|
|
servoAngleEpsilon = 0.01
|
|
)
|
|
|
|
// CameraServoController is the hardware-neutral camera-tilt contract used by
|
|
// WSClient. Native Pi PWM and ESP32 Firmata implementations expose identical
|
|
// logical behavior, so command handling never branches on the rover host type.
|
|
type CameraServoController interface {
|
|
SetAngle(angle float64) error
|
|
Nudge(delta float64) error
|
|
SetPulseWidth(micros int) error
|
|
CurrentAngle() float64
|
|
Configuration() CameraServoConfig
|
|
BackendDescription() string
|
|
Close()
|
|
}
|
|
|
|
// ToggleController keeps headlight and laser command/state behavior independent
|
|
// of whether the electrical write happens on native Pi GPIO or an ESP32 pin.
|
|
type ToggleController interface {
|
|
HandleAction(action string) error
|
|
On() bool
|
|
Configuration() GPIOToggleConfig
|
|
BackendDescription() string
|
|
Close()
|
|
}
|
|
|
|
// RoverHardwareControllers is the result of the single startup-time backend
|
|
// decision. Its effective configurations are derived from whichever backend
|
|
// won, making the normal rover hello accurate on both Pi and laptop hosts.
|
|
type RoverHardwareControllers struct {
|
|
CameraServo CameraServoController
|
|
Headlight ToggleController
|
|
Laser ToggleController
|
|
ignoredESP32Roles []string
|
|
}
|
|
|
|
// StartupBroadcasts returns short operator-facing messages. Detailed pin and
|
|
// protocol information remains in the journal; tty1 only explains which
|
|
// physical backend won and whether an advertised ESP32 role was ignored.
|
|
func (controllers RoverHardwareControllers) StartupBroadcasts() []string {
|
|
var messages []string
|
|
if len(controllers.ignoredESP32Roles) > 0 {
|
|
messages = append(messages, fmt.Sprintf(
|
|
"Ignored ESP32 %s because native GPIO is enabled.",
|
|
strings.Join(controllers.ignoredESP32Roles, ", "),
|
|
))
|
|
}
|
|
messages = append(messages, fmt.Sprintf(
|
|
"Rover hardware ready: camera servo via %s, headlight via %s, laser via %s.",
|
|
controllerBackend(controllers.CameraServo),
|
|
controllerBackend(controllers.Headlight),
|
|
controllerBackend(controllers.Laser),
|
|
))
|
|
return messages
|
|
}
|
|
|
|
func controllerBackend(controller interface{ BackendDescription() string }) string {
|
|
if controller == nil {
|
|
return "disabled"
|
|
}
|
|
return controller.BackendDescription()
|
|
}
|
|
|
|
type nativeHardwareControllerFactories struct {
|
|
newCameraServo func(CameraServoConfig, *log.Logger) (CameraServoController, error)
|
|
newToggle func(string, GPIOToggleConfig, *log.Logger) (ToggleController, error)
|
|
}
|
|
|
|
// ResolveRoverHardwareControllers applies one rule on every real rover build:
|
|
// enabled native GPIO wins, otherwise one discovered ESP32 may fill the role.
|
|
// The rule is intentionally not selected by GOARCH or the debian_laptop tag.
|
|
func ResolveRoverHardwareControllers(cfg *Config, peripherals *PeripheralManager, logger *log.Logger) (RoverHardwareControllers, error) {
|
|
factories := nativeHardwareControllerFactories{
|
|
newCameraServo: func(config CameraServoConfig, logger *log.Logger) (CameraServoController, error) {
|
|
return NewCameraServo(config, logger)
|
|
},
|
|
newToggle: func(name string, config GPIOToggleConfig, logger *log.Logger) (ToggleController, error) {
|
|
return NewGPIOToggle(name, config, logger)
|
|
},
|
|
}
|
|
return resolveRoverHardwareControllers(cfg, peripherals, logger, factories)
|
|
}
|
|
|
|
func resolveRoverHardwareControllers(cfg *Config, peripherals *PeripheralManager, logger *log.Logger, factories nativeHardwareControllerFactories) (RoverHardwareControllers, error) {
|
|
var controllers RoverHardwareControllers
|
|
var err error
|
|
// Record ignored declarations separately from selecting controllers so the
|
|
// same native-first decision can be explained on the local rover console.
|
|
if cfg.CameraServo.Enabled && peripherals.HasRoverRole("cameraServo") {
|
|
controllers.ignoredESP32Roles = append(controllers.ignoredESP32Roles, "camera servo")
|
|
}
|
|
if cfg.Headlight.Enabled && peripherals.HasRoverRole("headlight") {
|
|
controllers.ignoredESP32Roles = append(controllers.ignoredESP32Roles, "headlight")
|
|
}
|
|
if cfg.Laser.Enabled && peripherals.HasRoverRole("laser") {
|
|
controllers.ignoredESP32Roles = append(controllers.ignoredESP32Roles, "laser")
|
|
}
|
|
|
|
controllers.CameraServo, err = resolveCameraServoController(cfg.CameraServo, peripherals, logger, factories.newCameraServo)
|
|
if err != nil {
|
|
return RoverHardwareControllers{}, fmt.Errorf("init camera servo: %w", err)
|
|
}
|
|
controllers.Headlight, err = resolveToggleController("headlight", cfg.Headlight, peripherals, logger, factories.newToggle)
|
|
if err != nil {
|
|
controllers.Close()
|
|
return RoverHardwareControllers{}, fmt.Errorf("init headlight: %w", err)
|
|
}
|
|
controllers.Laser, err = resolveToggleController("laser", cfg.Laser, peripherals, logger, factories.newToggle)
|
|
if err != nil {
|
|
controllers.Close()
|
|
return RoverHardwareControllers{}, fmt.Errorf("init laser: %w", err)
|
|
}
|
|
return controllers, nil
|
|
}
|
|
|
|
func resolveCameraServoController(nativeConfig CameraServoConfig, peripherals *PeripheralManager, logger *log.Logger, newNative func(CameraServoConfig, *log.Logger) (CameraServoController, error)) (CameraServoController, error) {
|
|
if nativeConfig.Enabled {
|
|
if peripherals.HasRoverRole("cameraServo") {
|
|
logger.Printf("ignoring ESP32 cameraServo because native camera servo is enabled")
|
|
}
|
|
return newNative(nativeConfig, logger)
|
|
}
|
|
return peripherals.NewFirmataCameraServo(logger)
|
|
}
|
|
|
|
func resolveToggleController(name string, nativeConfig GPIOToggleConfig, peripherals *PeripheralManager, logger *log.Logger, newNative func(string, GPIOToggleConfig, *log.Logger) (ToggleController, error)) (ToggleController, error) {
|
|
if nativeConfig.Enabled {
|
|
if peripherals.HasRoverRole(name) {
|
|
logger.Printf("ignoring ESP32 %s because native %s is enabled", name, name)
|
|
}
|
|
return newNative(name, nativeConfig, logger)
|
|
}
|
|
return peripherals.NewFirmataToggle(name, logger)
|
|
}
|
|
|
|
// Close releases selected controller resources in reverse dependency order.
|
|
// Firmata controllers do not close the shared serial connection; that remains
|
|
// owned by PeripheralManager and is released by its separate shutdown defer.
|
|
func (controllers *RoverHardwareControllers) Close() {
|
|
if controllers.Laser != nil {
|
|
controllers.Laser.Close()
|
|
}
|
|
if controllers.Headlight != nil {
|
|
controllers.Headlight.Close()
|
|
}
|
|
if controllers.CameraServo != nil {
|
|
controllers.CameraServo.Close()
|
|
}
|
|
}
|