package config import ( "fmt" "os" "git.piksar.eu/piotr/groundstation/internal/osd" "git.piksar.eu/piotr/groundstation/internal/protocol" "gopkg.in/yaml.v3" ) // DisplayConfig configures the independent kiosk canvas process. type DisplayConfig struct { Enabled bool `yaml:"enabled"` Command string `yaml:"command"` User string `yaml:"user"` Display string `yaml:"display"` Xauthority string `yaml:"xauthority"` } // OSDConfig configures the supervisor's flight OSD snapshot publication. type OSDConfig struct { // StatePath is the snapshot state file the display process reads. StatePath string `yaml:"state_path"` // ArmIndex and ArmedThreshold derive the arm state from the final RC // channels: armed while that channel stays above the threshold. ArmIndex int `yaml:"arm_index"` ArmedThreshold int16 `yaml:"armed_threshold"` // VtxTableGen and VtxTableHash are the operator-selected VTX power // table identifiers. Zero means no table is selected yet; a static // selection is operator intent and never applied VTX state. VtxTableGen uint32 `yaml:"vtx_table_gen"` VtxTableHash uint64 `yaml:"vtx_table_hash"` } type Config struct { Display DisplayConfig `yaml:"display"` Serial struct { Path string `yaml:"path"` } `yaml:"serial"` Video struct { Enabled bool `yaml:"enabled"` AuxIndex int `yaml:"aux_index"` Threshold int16 `yaml:"threshold"` Command string `yaml:"command"` } `yaml:"video"` OSD OSDConfig `yaml:"osd"` Logging struct { Enabled bool `yaml:"enabled"` Path string `yaml:"path"` } `yaml:"logging"` } func Default() Config { var c Config c.Display = DefaultDisplay() c.Serial.Path = "/dev/ttyS0" c.Video.Enabled = true c.Video.AuxIndex = 5 // AUX2, zero-based c.Video.Threshold = -512 c.Video.Command = "fpv-video" c.OSD = DefaultOSD() c.Logging.Enabled = true return c } // DefaultDisplay starts the kiosk canvas before video on the desktop X // server owned by the unprivileged display user. func DefaultDisplay() DisplayConfig { return DisplayConfig{ Enabled: true, Command: "/usr/local/bin/groundstation-display", User: "orangepi", Display: ":0", Xauthority: "/home/orangepi/.Xauthority", } } // DefaultOSD publishes snapshots to the default tmpfs path and derives the // arm state from CH5/AUX1, armed above the midpoint. func DefaultOSD() OSDConfig { return OSDConfig{ StatePath: osd.DefaultStatePath, ArmIndex: 4, ArmedThreshold: 0, } } func Load(path string) (Config, error) { config := Default() data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return config, config.validate() } return config, err } if err := yaml.Unmarshal(data, &config); err != nil { return config, err } return config, config.validate() } // validate rejects channel indexes outside the final RC channel range: an // out-of-range index would panic on the first decoded frame. func (c Config) validate() error { for name, index := range map[string]int{ "video.aux_index": c.Video.AuxIndex, "osd.arm_index": c.OSD.ArmIndex, } { if index < 0 || index >= protocol.Channels { return fmt.Errorf("%s %d outside the RC channel range 0..%d", name, index, protocol.Channels-1) } } return nil }