This commit is contained in:
legop3
2025-12-02 14:43:56 -05:00
parent 9bc5d2d06d
commit ace39d5a70
21 changed files with 238 additions and 18 deletions
+10 -1
View File
@@ -69,10 +69,19 @@ func main() {
defer cameraServo.Close()
}
var nightVision *roverd.NightVisionLight
if cfg.NightVision.Enabled {
nightVision, err = roverd.NewNightVisionLight(cfg.NightVision, logger)
if err != nil {
logger.Fatalf("init night vision: %v", err)
}
defer nightVision.Close()
}
autoCharge := roverd.NewAutoChargeController(adapter, eventStream, logger)
go autoCharge.Run(ctx, sensorSamples)
client := roverd.NewWSClient(cfg, adapter, sensorFrames, eventStream, mediaSupervisor, cameraServo, logger)
client := roverd.NewWSClient(cfg, adapter, sensorFrames, eventStream, mediaSupervisor, cameraServo, nightVision, logger)
retryDelay := time.Second
for ctx.Err() == nil {
+6
View File
@@ -8,6 +8,7 @@ type helloMessage struct {
Media MediaConfig `json:"media"`
CameraServo CameraServoConfig `json:"cameraServo"`
Audio AudioConfig `json:"audio"`
NightVision NightVisionConfig `json:"nightVision"`
}
type sensorMessage struct {
@@ -26,6 +27,7 @@ type inboundMessage struct {
Media *mediaCommand `json:"media,omitempty"`
Servo *servoPayload `json:"servo,omitempty"`
TTS *ttsPayload `json:"tts,omitempty"`
NightVision *nightVisionPayload `json:"nightVision,omitempty"`
}
type driveDirectPayload struct {
@@ -61,6 +63,10 @@ type ttsPayload struct {
Speak bool `json:"speak,omitempty"`
}
type nightVisionPayload struct {
Action string `json:"action"`
}
type ackMessage struct {
Type string `json:"type"`
ID string `json:"id"`
+30
View File
@@ -95,6 +95,13 @@ type CameraServoConfig struct {
AllowRawPulse bool `yaml:"allowRawPulse" json:"allowRawPulse"`
}
type NightVisionConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
GPIOPin int `yaml:"gpioPin" json:"gpioPin"`
GPIOChip string `yaml:"gpioChip" json:"gpioChip"`
InitialOn bool `yaml:"initialOn" json:"initialOn"`
}
type Config struct {
Name string `yaml:"name"`
ServerURL string `yaml:"serverUrl"`
@@ -105,6 +112,7 @@ type Config struct {
Media MediaConfig `yaml:"media"`
CameraServo CameraServoConfig `yaml:"cameraServo"`
Audio AudioConfig `yaml:"audio"`
NightVision NightVisionConfig `yaml:"nightVision" json:"nightVision"`
}
func LoadConfig(path string) (*Config, error) {
@@ -154,6 +162,12 @@ func LoadConfig(path string) (*Config, error) {
DefaultVoice: "rms",
DefaultPitch: 50,
},
NightVision: NightVisionConfig{
Enabled: true,
GPIOPin: 22,
GPIOChip: "gpiochip0",
InitialOn: true,
},
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
@@ -214,6 +228,9 @@ func LoadConfig(path string) (*Config, error) {
if err := validateServoConfig(&cfg.CameraServo); err != nil {
return nil, fmt.Errorf("cameraServo: %w", err)
}
if err := validateNightVisionConfig(&cfg.NightVision); err != nil {
return nil, fmt.Errorf("nightVision: %w", err)
}
validateAudioConfig(&cfg.Audio)
return &cfg, nil
}
@@ -278,6 +295,19 @@ func validateAudioConfig(cfg *AudioConfig) {
}
}
func validateNightVisionConfig(cfg *NightVisionConfig) error {
if !cfg.Enabled {
return nil
}
if cfg.GPIOPin <= 0 {
return errors.New("gpioPin must be > 0")
}
if cfg.GPIOChip == "" {
cfg.GPIOChip = "gpiochip0"
}
return nil
}
func derivePublishURL(serverURL, streamName string, port int) (string, error) {
if streamName == "" {
return "", errors.New("missing stream name for publishUrl")
+97
View File
@@ -0,0 +1,97 @@
//go:build !dummy
package roverd
import (
"fmt"
"log"
"strings"
"sync"
gpiocdev "github.com/warthog618/go-gpiocdev"
)
type NightVisionLight struct {
cfg NightVisionConfig
logger *log.Logger
line *gpiocdev.Line
mu sync.Mutex
on bool
closed bool
}
func NewNightVisionLight(cfg NightVisionConfig, logger *log.Logger) (*NightVisionLight, error) {
if !cfg.Enabled {
return nil, fmt.Errorf("night vision disabled")
}
chip := cfg.GPIOChip
if chip == "" {
chip = "gpiochip0"
}
initial := 0
if cfg.InitialOn {
initial = 1
}
line, err := gpiocdev.RequestLine(
chip,
cfg.GPIOPin,
gpiocdev.AsOutput(initial),
gpiocdev.WithConsumer("roverd-nightvision"),
)
if err != nil {
return nil, fmt.Errorf("gpio request: %w", err)
}
nv := &NightVisionLight{
cfg: cfg,
logger: logger,
line: line,
on: cfg.InitialOn,
}
logger.Printf("night vision LED on GPIO %d (initial=%v)", cfg.GPIOPin, cfg.InitialOn)
return nv, nil
}
func (n *NightVisionLight) Close() {
n.mu.Lock()
defer n.mu.Unlock()
if n.closed {
return
}
_ = n.line.SetValue(boolToGPIO(n.on))
n.line.Close()
n.closed = true
}
func (n *NightVisionLight) HandleAction(action string) error {
n.mu.Lock()
defer n.mu.Unlock()
if n.closed {
return fmt.Errorf("night vision controller closed")
}
act := strings.ToLower(strings.TrimSpace(action))
switch act {
case "", "toggle":
return n.setLocked(!n.on)
case "on":
return n.setLocked(true)
case "off":
return n.setLocked(false)
default:
return fmt.Errorf("unknown action %q", action)
}
}
func (n *NightVisionLight) setLocked(on bool) error {
if err := n.line.SetValue(boolToGPIO(on)); err != nil {
return err
}
n.on = on
return nil
}
func boolToGPIO(value bool) int {
if value {
return 1
}
return 0
}
+20
View File
@@ -0,0 +1,20 @@
//go:build dummy
package roverd
import (
"fmt"
"log"
)
type NightVisionLight struct{}
func NewNightVisionLight(cfg NightVisionConfig, logger *log.Logger) (*NightVisionLight, error) {
return nil, fmt.Errorf("night vision not supported in dummy build")
}
func (n *NightVisionLight) Close() {}
func (n *NightVisionLight) HandleAction(action string) error {
return fmt.Errorf("night vision not supported in dummy build")
}
+5
View File
@@ -47,3 +47,8 @@ audio:
defaultEngine: flite
defaultVoice: rms
defaultPitch: 50
nightVision:
enabled: true
gpioPin: 22
gpioChip: gpiochip0
initialOn: true
+9 -1
View File
@@ -18,10 +18,11 @@ type WSClient struct {
events <-chan RoverEvent
media *MediaSupervisor
servo *CameraServo
nightVision *NightVisionLight
log *log.Logger
}
func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, events <-chan RoverEvent, media *MediaSupervisor, servo *CameraServo, logger *log.Logger) *WSClient {
func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, events <-chan RoverEvent, media *MediaSupervisor, servo *CameraServo, nightVision *NightVisionLight, logger *log.Logger) *WSClient {
return &WSClient{
cfg: cfg,
adapter: adapter,
@@ -29,6 +30,7 @@ func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, even
events: events,
media: media,
servo: servo,
nightVision: nightVision,
log: logger,
}
}
@@ -72,6 +74,7 @@ func (c *WSClient) sendHello(ctx context.Context, conn *websocket.Conn) error {
Media: c.cfg.Media,
CameraServo: c.cfg.CameraServo,
Audio: c.cfg.Audio,
NightVision: c.cfg.NightVision,
}
c.log.Printf("sending hello (camera servo enabled=%v pin=%d)", msg.CameraServo.Enabled, msg.CameraServo.Pin)
return writeJSON(ctx, conn, msg)
@@ -153,6 +156,11 @@ func (c *WSClient) dispatch(ctx context.Context, msg *inboundMessage) error {
return c.handleServoCommand(msg.Servo)
case msg.TTS != nil:
return c.handleTTSPayload(msg.TTS)
case msg.NightVision != nil:
if c.nightVision == nil {
return fmt.Errorf("night vision disabled")
}
return c.nightVision.HandleAction(msg.NightVision.Action)
default:
return fmt.Errorf("unsupported command type: %s", msg.Type)
}