feat: add UART reconnect and configurable channel logging
Build Ground Station / build (push) Successful in 23s
Build Ground Station / build (push) Successful in 23s
This commit is contained in:
+65
-26
@@ -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 = ©
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user