From bb178eff6de3bce1bffb077498994451b90020e8 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 30 Sep 2026 10:30:01 +0200 Subject: [PATCH] feat: add UART reconnect and configurable channel logging --- cmd/groundstation/main.go | 91 ++++++++++++++++++++++++++++----------- deploy/config.yml | 12 ++++++ go.mod | 5 ++- go.sum | 4 ++ internal/config/config.go | 49 +++++++++++++++++++++ 5 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 deploy/config.yml create mode 100644 internal/config/config.go diff --git a/cmd/groundstation/main.go b/cmd/groundstation/main.go index ceae894..55fbeee 100644 --- a/cmd/groundstation/main.go +++ b/cmd/groundstation/main.go @@ -2,56 +2,99 @@ package main import ( "context" + "errors" "flag" "fmt" "io" "log" "os" "os/signal" + "path/filepath" "syscall" + "time" + "git.piksar.eu/piotr/groundstation/internal/config" "git.piksar.eu/piotr/groundstation/internal/protocol" "git.piksar.eu/piotr/groundstation/internal/serialport" ) func main() { - serialPath := flag.String("serial", "/dev/ttyS0", "GX12 AUX1 UART device") + configPath := flag.String("config", "/etc/groundstation/config.yml", "Ground Station YAML configuration") stdin := flag.Bool("stdin", false, "read framed data from standard input") - videoByAux2 := flag.Bool("video-by-aux2", true, "start fpv-video when AUX2 is above -512") - videoCommand := flag.String("video-command", "fpv-video", "video launcher command") flag.Parse() - if *stdin { - *videoByAux2 = false + cfg, err := config.Load(*configPath) + if err != nil { + log.Fatalf("load config: %v", err) } + logger, closeLog, err := setupLogger(cfg) + if err != nil { + log.Fatal(err) + } + defer closeLog() + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() var video *videoController - if *videoByAux2 { - video = newVideoController(*videoCommand) + if cfg.Video.Enabled { + video = newVideoController(cfg.Video.Command) defer func() { _ = video.Stop() }() } - var input io.Reader = os.Stdin - var port *os.File - var err error - if !*stdin { - port, err = serialport.Open115200(*serialPath) - if err != nil { + if *stdin { + if err := run(ctx, os.Stdin, logger, video, cfg); err != nil && !errors.Is(err, context.Canceled) { log.Fatal(err) } - defer port.Close() - input = port + return } - if err := run(ctx, input, os.Stdout, video); err != nil && err != context.Canceled { - log.Fatal(err) + for ctx.Err() == nil { + port, err := serialport.Open115200(cfg.Serial.Path) + if err != nil { + logger.Printf("UART open %s: %v; retrying", cfg.Serial.Path, err) + wait(ctx, time.Second) + continue + } + + logger.Printf("UART connected: %s", cfg.Serial.Path) + err = run(ctx, port, logger, video, cfg) + _ = port.Close() + if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, io.EOF) { + logger.Printf("UART disconnected: %v", err) + } else if errors.Is(err, io.EOF) { + logger.Printf("UART EOF; reconnecting") + } + wait(ctx, time.Second) } } -func run(ctx context.Context, input io.Reader, output io.Writer, video *videoController) error { +func setupLogger(cfg config.Config) (*log.Logger, func(), error) { + writers := []io.Writer{os.Stdout} + closeLog := func() {} + if cfg.Logging.Enabled && cfg.Logging.Path != "" { + if err := os.MkdirAll(filepath.Dir(cfg.Logging.Path), 0755); err != nil { + return nil, closeLog, err + } + file, err := os.OpenFile(cfg.Logging.Path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return nil, closeLog, err + } + writers = append(writers, file) + closeLog = func() { _ = file.Close() } + } + return log.New(io.MultiWriter(writers...), "", log.LstdFlags), closeLog, nil +} + +func wait(ctx context.Context, duration time.Duration) { + select { + case <-ctx.Done(): + case <-time.After(duration): + } +} + +func run(ctx context.Context, input io.Reader, logger *log.Logger, video *videoController, cfg config.Config) error { buf := make([]byte, protocol.RCSize) var previous *protocol.RCFrame for { @@ -60,22 +103,18 @@ func run(ctx context.Context, input io.Reader, output io.Writer, video *videoCon } frame, err := protocol.DecodeRC(buf) if err != nil { - log.Printf("discard frame: %v", err) + logger.Printf("discard frame: %v", err) continue } - // AUX2 is the dedicated GS video switch: -1024 means management/off, - // while its middle position (0) means preflight/WFB/on. + if video != nil { - if err := video.Set(frame.Channels[5] > -512); err != nil { + if err := video.Set(frame.Channels[cfg.Video.AuxIndex] > cfg.Video.Threshold); err != nil { return err } } if previous == nil || previous.Channels != frame.Channels { - fmt.Fprintf(output, "seq=%d t=%d ch1=%d ch2=%d ch3=%d ch4=%d aux1=%d aux2=%d\n", - frame.Sequence, frame.Timestamp, - frame.Channels[0], frame.Channels[1], frame.Channels[2], frame.Channels[3], - frame.Channels[4], frame.Channels[5]) + logger.Printf("RC seq=%d t=%d channels=%v", frame.Sequence, frame.Timestamp, frame.Channels) copy := frame previous = © } diff --git a/deploy/config.yml b/deploy/config.yml new file mode 100644 index 0000000..5e13bc5 --- /dev/null +++ b/deploy/config.yml @@ -0,0 +1,12 @@ +serial: + path: /dev/ttyS0 + +video: + enabled: true + aux_index: 5 # AUX2, zero-based + threshold: -512 # -1024 = management/off; 0 = WFB/on + command: fpv-video + +logging: + enabled: true + path: /var/log/groundstation/groundstation.log diff --git a/go.mod b/go.mod index 3df57b8..7963d84 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module git.piksar.eu/piotr/groundstation go 1.24.0 -require golang.org/x/sys v0.40.0 +require ( + golang.org/x/sys v0.40.0 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/go.sum b/go.sum index d117642..2ed8652 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0fd2c32 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,49 @@ +package config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + 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"` + Logging struct { + Enabled bool `yaml:"enabled"` + Path string `yaml:"path"` + } `yaml:"logging"` +} + +func Default() Config { + var c Config + 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.Logging.Enabled = true + return c +} + +func Load(path string) (Config, error) { + config := Default() + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return config, nil + } + return config, err + } + if err := yaml.Unmarshal(data, &config); err != nil { + return config, err + } + return config, nil +}