mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
config reshape
This commit is contained in:
+195
-66
@@ -56,13 +56,13 @@ type BatteryConfig struct {
|
||||
}
|
||||
|
||||
type AudioConfig struct {
|
||||
CaptureEnabled bool `yaml:"captureEnabled" json:"captureEnabled"`
|
||||
CaptureDevice string `yaml:"captureDevice" json:"captureDevice,omitempty"`
|
||||
PlaybackDevice string `yaml:"playbackDevice" json:"playbackDevice,omitempty"`
|
||||
TTSEnabled bool `yaml:"ttsEnabled" json:"ttsEnabled"`
|
||||
DefaultEngine string `yaml:"defaultEngine" json:"defaultEngine,omitempty"`
|
||||
DefaultVoice string `yaml:"defaultVoice" json:"defaultVoice,omitempty"`
|
||||
DefaultPitch int `yaml:"defaultPitch" json:"defaultPitch,omitempty"`
|
||||
// AudioConfig is only for sounds generated by roverd itself. Live capture
|
||||
// and playback are media-publisher concerns, so those settings live under
|
||||
// MediaConfig where they can be written directly into media.env.
|
||||
TTSEnabled bool `yaml:"ttsEnabled" json:"ttsEnabled"`
|
||||
DefaultEngine string `yaml:"defaultEngine" json:"defaultEngine,omitempty"`
|
||||
DefaultVoice string `yaml:"defaultVoice" json:"defaultVoice,omitempty"`
|
||||
DefaultPitch int `yaml:"defaultPitch" json:"defaultPitch,omitempty"`
|
||||
}
|
||||
|
||||
type HornConfig struct {
|
||||
@@ -77,21 +77,59 @@ type HornConfig struct {
|
||||
}
|
||||
|
||||
type MediaConfig struct {
|
||||
PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"`
|
||||
AudioPublishURL string `yaml:"audioPublishUrl" json:"audioPublishUrl,omitempty"`
|
||||
AudioForwardURL string `yaml:"audioForwardUrl" json:"audioForwardUrl,omitempty"`
|
||||
PublishPort int `yaml:"publishPort" json:"-"`
|
||||
CameraInverted bool `yaml:"cameraInverted" json:"-"`
|
||||
Manage bool `yaml:"manage"`
|
||||
ManageAudio bool `yaml:"manageAudio"`
|
||||
Service string `yaml:"service"`
|
||||
AudioService string `yaml:"audioService"`
|
||||
HealthURL string `yaml:"healthUrl"`
|
||||
HealthInterval Duration `yaml:"healthInterval"`
|
||||
VideoWidth int `yaml:"videoWidth" json:"-"`
|
||||
VideoHeight int `yaml:"videoHeight" json:"-"`
|
||||
VideoFPS int `yaml:"videoFps" json:"-"`
|
||||
VideoBitrate int `yaml:"videoBitrate" json:"-"`
|
||||
// PublishPort is shared by the derived video, microphone, and forwarded-audio
|
||||
// SRT URLs. Keeping it at this level prevents each nested block from needing
|
||||
// to repeat the same server port when the common MediaMTX listener is used.
|
||||
PublishPort int `yaml:"publishPort" json:"-"`
|
||||
Manage bool `yaml:"manage" json:"manage"`
|
||||
HealthURL string `yaml:"healthUrl" json:"healthUrl,omitempty"`
|
||||
HealthInterval Duration `yaml:"healthInterval" json:"-"`
|
||||
Video VideoMediaConfig `yaml:"video" json:"video"`
|
||||
AudioCapture AudioCaptureConfig `yaml:"audioCapture" json:"audioCapture"`
|
||||
AudioPlayback AudioPlaybackConfig `yaml:"audioPlayback" json:"audioPlayback"`
|
||||
}
|
||||
|
||||
type VideoMediaConfig struct {
|
||||
// Publisher selects the installed publisher script/pipeline family. The
|
||||
// first pass uses pi-libcamera for current rovers; laptop-v4l2 can be added
|
||||
// without changing the server-facing media shape again.
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Service string `yaml:"service" json:"service,omitempty"`
|
||||
Publisher string `yaml:"publisher" json:"publisher,omitempty"`
|
||||
PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"`
|
||||
Device string `yaml:"device" json:"device,omitempty"`
|
||||
Width int `yaml:"width" json:"-"`
|
||||
Height int `yaml:"height" json:"-"`
|
||||
FPS int `yaml:"fps" json:"-"`
|
||||
Bitrate int `yaml:"bitrate" json:"-"`
|
||||
Inverted bool `yaml:"inverted" json:"-"`
|
||||
SensorMode string `yaml:"sensorMode" json:"-"`
|
||||
}
|
||||
|
||||
type AudioCaptureConfig struct {
|
||||
// AudioCapture describes the rover microphone stream that browsers can
|
||||
// subscribe to as "<rover>-audio". A disabled capture block still has
|
||||
// normalized defaults so enabling it only requires flipping enabled: true.
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Service string `yaml:"service" json:"service,omitempty"`
|
||||
PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"`
|
||||
Device string `yaml:"device" json:"device,omitempty"`
|
||||
SampleRate int `yaml:"sampleRate" json:"-"`
|
||||
Channels int `yaml:"channels" json:"-"`
|
||||
Bitrate int `yaml:"bitrate" json:"-"`
|
||||
}
|
||||
|
||||
type AudioPlaybackConfig struct {
|
||||
// AudioPlayback describes the reverse stream that VIP users publish into
|
||||
// MediaMTX for playback on the rover speaker. The URL is a request/read URL
|
||||
// for the rover listener, while the server converts it to publish mode when
|
||||
// it needs to inject audio.
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Service string `yaml:"service" json:"service,omitempty"`
|
||||
ForwardURL string `yaml:"forwardUrl" json:"forwardUrl,omitempty"`
|
||||
Device string `yaml:"device" json:"device,omitempty"`
|
||||
Normalize bool `yaml:"normalize" json:"-"`
|
||||
NormalizeFilter string `yaml:"normalizeFilter" json:"-"`
|
||||
}
|
||||
|
||||
type CameraServoConfig struct {
|
||||
@@ -189,9 +227,33 @@ func LoadConfig(path string) (*Config, error) {
|
||||
},
|
||||
Media: MediaConfig{
|
||||
PublishPort: 9000,
|
||||
CameraInverted: true,
|
||||
HealthInterval: Duration{Duration: 30 * time.Second},
|
||||
VideoBitrate: 2000000,
|
||||
Video: VideoMediaConfig{
|
||||
Enabled: true,
|
||||
Service: "video-publisher.service",
|
||||
Publisher: "pi-libcamera",
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FPS: 30,
|
||||
Bitrate: 2000000,
|
||||
Inverted: true,
|
||||
SensorMode: "1296:972",
|
||||
},
|
||||
AudioCapture: AudioCaptureConfig{
|
||||
Enabled: false,
|
||||
Service: "audio-only-publisher.service",
|
||||
Device: "hw:0,0",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
Bitrate: 510000,
|
||||
},
|
||||
AudioPlayback: AudioPlaybackConfig{
|
||||
Enabled: true,
|
||||
Service: "audio-forward-listener.service",
|
||||
Device: "forward",
|
||||
Normalize: true,
|
||||
NormalizeFilter: "dynaudnorm=f=75:g=15:m=10:p=0.9,alimiter=limit=0.85:level=disabled",
|
||||
},
|
||||
},
|
||||
CameraServo: CameraServoConfig{
|
||||
Pin: 12,
|
||||
@@ -205,13 +267,10 @@ func LoadConfig(path string) (*Config, error) {
|
||||
NudgeDegrees: 2,
|
||||
},
|
||||
Audio: AudioConfig{
|
||||
CaptureEnabled: false,
|
||||
CaptureDevice: "rovermic",
|
||||
PlaybackDevice: "forward",
|
||||
TTSEnabled: false,
|
||||
DefaultEngine: "flite",
|
||||
DefaultVoice: "rms",
|
||||
DefaultPitch: 50,
|
||||
TTSEnabled: false,
|
||||
DefaultEngine: "flite",
|
||||
DefaultVoice: "rms",
|
||||
DefaultPitch: 50,
|
||||
},
|
||||
Horn: HornConfig{
|
||||
Enabled: false,
|
||||
@@ -283,38 +342,11 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if cfg.BRC.GPIOChip == "" {
|
||||
cfg.BRC.GPIOChip = "gpiochip0"
|
||||
}
|
||||
if cfg.Media.Manage && cfg.Media.Service == "" {
|
||||
return nil, errors.New("media.manage requires media.service")
|
||||
}
|
||||
if cfg.Media.Manage && cfg.Media.HealthInterval.Duration <= 0 {
|
||||
cfg.Media.HealthInterval = Duration{Duration: 30 * time.Second}
|
||||
}
|
||||
if cfg.Media.VideoBitrate <= 0 {
|
||||
cfg.Media.VideoBitrate = 3000000
|
||||
}
|
||||
if cfg.Media.PublishPort <= 0 {
|
||||
cfg.Media.PublishPort = 9000
|
||||
}
|
||||
if cfg.Media.PublishURL == "" {
|
||||
derived, err := derivePublishURL(cfg.ServerURL, cfg.Name, cfg.Media.PublishPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive publishUrl: %w", err)
|
||||
}
|
||||
cfg.Media.PublishURL = derived
|
||||
}
|
||||
if cfg.Media.AudioPublishURL == "" {
|
||||
derived, err := derivePublishURL(cfg.ServerURL, cfg.Name+"-audio", cfg.Media.PublishPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive audioPublishUrl: %w", err)
|
||||
}
|
||||
cfg.Media.AudioPublishURL = derived
|
||||
}
|
||||
if cfg.Media.AudioForwardURL == "" {
|
||||
derived, err := deriveReadURL(cfg.ServerURL, cfg.Name+"-fwd", cfg.Media.PublishPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive audioForwardUrl: %w", err)
|
||||
}
|
||||
cfg.Media.AudioForwardURL = derived
|
||||
if err := validateMediaConfig(&cfg.Media, cfg.ServerURL, cfg.Name); err != nil {
|
||||
return nil, fmt.Errorf("media: %w", err)
|
||||
}
|
||||
if err := validateServoConfig(&cfg.CameraServo); err != nil {
|
||||
return nil, fmt.Errorf("cameraServo: %w", err)
|
||||
@@ -375,12 +407,6 @@ func clampFloat(value, min, max float64) float64 {
|
||||
}
|
||||
|
||||
func validateAudioConfig(cfg *AudioConfig) {
|
||||
if cfg.CaptureEnabled && cfg.CaptureDevice == "" {
|
||||
cfg.CaptureDevice = "hw:0,0"
|
||||
}
|
||||
if cfg.PlaybackDevice == "" || cfg.PlaybackDevice == "default" {
|
||||
cfg.PlaybackDevice = "forward"
|
||||
}
|
||||
if cfg.DefaultEngine == "" {
|
||||
cfg.DefaultEngine = "flite"
|
||||
}
|
||||
@@ -392,6 +418,109 @@ func validateAudioConfig(cfg *AudioConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
func validateMediaConfig(cfg *MediaConfig, serverURL string, roverName string) error {
|
||||
/*
|
||||
The nested media config is the source of truth for the publisher scripts,
|
||||
so defaults that used to live in shell are normalized here before any env
|
||||
file is written. This keeps the Pi behavior stable while making laptop
|
||||
and future publisher variants explicit configuration choices.
|
||||
*/
|
||||
if cfg.PublishPort <= 0 {
|
||||
cfg.PublishPort = 9000
|
||||
}
|
||||
if cfg.HealthInterval.Duration <= 0 {
|
||||
cfg.HealthInterval = Duration{Duration: 30 * time.Second}
|
||||
}
|
||||
if err := validateVideoMediaConfig(&cfg.Video, serverURL, roverName, cfg.PublishPort); err != nil {
|
||||
return fmt.Errorf("video: %w", err)
|
||||
}
|
||||
if err := validateAudioCaptureConfig(&cfg.AudioCapture, serverURL, roverName, cfg.PublishPort); err != nil {
|
||||
return fmt.Errorf("audioCapture: %w", err)
|
||||
}
|
||||
if err := validateAudioPlaybackConfig(&cfg.AudioPlayback, serverURL, roverName, cfg.PublishPort); err != nil {
|
||||
return fmt.Errorf("audioPlayback: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateVideoMediaConfig(cfg *VideoMediaConfig, serverURL string, roverName string, publishPort int) error {
|
||||
if cfg.Service == "" {
|
||||
cfg.Service = "video-publisher.service"
|
||||
}
|
||||
if cfg.Publisher == "" {
|
||||
cfg.Publisher = "pi-libcamera"
|
||||
}
|
||||
if cfg.Width <= 0 {
|
||||
cfg.Width = 640
|
||||
}
|
||||
if cfg.Height <= 0 {
|
||||
cfg.Height = 480
|
||||
}
|
||||
if cfg.FPS <= 0 {
|
||||
cfg.FPS = 30
|
||||
}
|
||||
if cfg.Bitrate <= 0 {
|
||||
cfg.Bitrate = 2000000
|
||||
}
|
||||
if cfg.SensorMode == "" && cfg.Publisher == "pi-libcamera" {
|
||||
cfg.SensorMode = "1296:972"
|
||||
}
|
||||
if cfg.PublishURL == "" {
|
||||
derived, err := derivePublishURL(serverURL, roverName, publishPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("derive publishUrl: %w", err)
|
||||
}
|
||||
cfg.PublishURL = derived
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAudioCaptureConfig(cfg *AudioCaptureConfig, serverURL string, roverName string, publishPort int) error {
|
||||
if cfg.Service == "" {
|
||||
cfg.Service = "audio-only-publisher.service"
|
||||
}
|
||||
if cfg.Device == "" {
|
||||
cfg.Device = "hw:0,0"
|
||||
}
|
||||
if cfg.SampleRate <= 0 {
|
||||
cfg.SampleRate = 48000
|
||||
}
|
||||
if cfg.Channels <= 0 {
|
||||
cfg.Channels = 2
|
||||
}
|
||||
if cfg.Bitrate <= 0 {
|
||||
cfg.Bitrate = 510000
|
||||
}
|
||||
if cfg.PublishURL == "" {
|
||||
derived, err := derivePublishURL(serverURL, roverName+"-audio", publishPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("derive publishUrl: %w", err)
|
||||
}
|
||||
cfg.PublishURL = derived
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAudioPlaybackConfig(cfg *AudioPlaybackConfig, serverURL string, roverName string, publishPort int) error {
|
||||
if cfg.Service == "" {
|
||||
cfg.Service = "audio-forward-listener.service"
|
||||
}
|
||||
if cfg.Device == "" {
|
||||
cfg.Device = "forward"
|
||||
}
|
||||
if cfg.NormalizeFilter == "" {
|
||||
cfg.NormalizeFilter = "dynaudnorm=f=75:g=15:m=10:p=0.9,alimiter=limit=0.85:level=disabled"
|
||||
}
|
||||
if cfg.ForwardURL == "" {
|
||||
derived, err := deriveReadURL(serverURL, roverName+"-fwd", publishPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("derive forwardUrl: %w", err)
|
||||
}
|
||||
cfg.ForwardURL = derived
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHornConfig(cfg *HornConfig) {
|
||||
if cfg.Volume <= 0 {
|
||||
cfg.Volume = 0.25
|
||||
|
||||
Reference in New Issue
Block a user