From d3bc369164494438627c9ac82c6ca9421bf08644 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 1 Oct 2026 15:26:24 +0200 Subject: [PATCH 1/2] fix: set 115200 baud via CBAUD bits and verify readback on UART open (#10) --- internal/serialport/serial_linux.go | 44 ++++++++++++++---- internal/serialport/serial_linux_test.go | 59 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 internal/serialport/serial_linux_test.go diff --git a/internal/serialport/serial_linux.go b/internal/serialport/serial_linux.go index ec6472c..3e407a2 100644 --- a/internal/serialport/serial_linux.go +++ b/internal/serialport/serial_linux.go @@ -19,19 +19,45 @@ func Open115200(path string) (*os.File, error) { return nil, err } - termios.Iflag = 0 - termios.Oflag = 0 - termios.Lflag = 0 - termios.Cflag &^= unix.CSIZE | unix.PARENB | unix.CSTOPB | unix.CRTSCTS - termios.Cflag |= unix.CS8 | unix.CREAD | unix.CLOCAL - termios.Ispeed = unix.B115200 - termios.Ospeed = unix.B115200 - termios.Cc[unix.VMIN] = 1 - termios.Cc[unix.VTIME] = 0 + configureRaw115200(termios) if err := unix.IoctlSetTermios(int(file.Fd()), unix.TCSETS, termios); err != nil { file.Close() return nil, fmt.Errorf("configure %s: %w", path, err) } + + effective, err := unix.IoctlGetTermios(int(file.Fd()), unix.TCGETS) + if err != nil { + file.Close() + return nil, fmt.Errorf("read back %s: %w", path, err) + } + if err := verifyBaud(*effective, unix.B115200); err != nil { + file.Close() + return nil, fmt.Errorf("verify %s: %w", path, err) + } return file, nil } + +// configureRaw115200 keeps the raw 115200 8N1 blocking-read configuration. +// On Linux TCSETS takes the line speed from the CBAUD bits in Cflag, so +// CBAUD must be cleared and B115200 selected there; Ispeed/Ospeed alone +// leave an inherited baud (for example 9600 from boot defaults) in effect. +func configureRaw115200(termios *unix.Termios) { + termios.Iflag = 0 + termios.Oflag = 0 + termios.Lflag = 0 + termios.Cflag &^= unix.CSIZE | unix.PARENB | unix.CSTOPB | unix.CRTSCTS | unix.CBAUD + termios.Cflag |= unix.B115200 + termios.Cflag |= unix.CS8 | unix.CREAD | unix.CLOCAL + termios.Ispeed = unix.B115200 + termios.Ospeed = unix.B115200 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 +} + +func verifyBaud(termios unix.Termios, baud uint32) error { + if got := termios.Cflag & unix.CBAUD; got != baud { + return fmt.Errorf("effective baud bits %#o, want %#o", got, baud) + } + return nil +} diff --git a/internal/serialport/serial_linux_test.go b/internal/serialport/serial_linux_test.go new file mode 100644 index 0000000..314bcfb --- /dev/null +++ b/internal/serialport/serial_linux_test.go @@ -0,0 +1,59 @@ +package serialport + +import ( + "testing" + + "golang.org/x/sys/unix" +) + +func TestConfigureRaw115200OverridesInheritedBaud(t *testing.T) { + var termios unix.Termios + termios.Iflag = unix.IGNBRK | unix.ICRNL + termios.Oflag = unix.OPOST + termios.Lflag = unix.ICANON | unix.ECHO + termios.Cflag = unix.B9600 | unix.CS8 | unix.CREAD | unix.CLOCAL | unix.HUPCL + termios.Ispeed = unix.B9600 + termios.Ospeed = unix.B9600 + + configureRaw115200(&termios) + + if got := termios.Cflag & unix.CBAUD; got != unix.B115200 { + t.Fatalf("effective baud bits=%#o want %#o", got, unix.B115200) + } + if got := termios.Cflag & unix.CSIZE; got != unix.CS8 { + t.Fatalf("character size bits=%#o want %#o", got, unix.CS8) + } + for _, flag := range []uint32{unix.CREAD, unix.CLOCAL} { + if termios.Cflag&flag == 0 { + t.Fatalf("flag %#o lost", flag) + } + } + for _, flag := range []uint32{unix.PARENB, unix.CSTOPB, unix.CRTSCTS} { + if termios.Cflag&flag != 0 { + t.Fatalf("flag %#o must be cleared", flag) + } + } + if termios.Iflag != 0 || termios.Oflag != 0 || termios.Lflag != 0 { + t.Fatal("raw mode must clear Iflag, Oflag and Lflag") + } + if termios.Ispeed != unix.B115200 || termios.Ospeed != unix.B115200 { + t.Fatalf("speed fields=%d/%d want 115200/115200", termios.Ispeed, termios.Ospeed) + } + if termios.Cc[unix.VMIN] != 1 || termios.Cc[unix.VTIME] != 0 { + t.Fatalf("VMIN=%d VTIME=%d want 1/0", termios.Cc[unix.VMIN], termios.Cc[unix.VTIME]) + } +} + +func TestVerifyBaudAcceptsOnlyConfiguredLineSpeed(t *testing.T) { + var inherited unix.Termios + inherited.Cflag = unix.B9600 | unix.CS8 + if err := verifyBaud(inherited, unix.B115200); err == nil { + t.Fatal("inherited 9600 line speed must fail verification") + } + + var configured unix.Termios + configured.Cflag = unix.B115200 | unix.CS8 | unix.CREAD + if err := verifyBaud(configured, unix.B115200); err != nil { + t.Fatalf("115200 line speed must verify: %v", err) + } +} From 056cd5440a472c088fc5dddfbae4f1969d07b578 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 1 Oct 2026 15:39:31 +0200 Subject: [PATCH 2/2] docs: explain Ground Station artifact components --- README.md | 73 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 77c5205..405d2ac 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,61 @@ # Ground Station -Orange Pi Zero 3W application for the custom OpenIPC/WFB ground station. +Orange Pi Zero 3W supervisor and kiosk display for the custom OpenIPC/WFB ground station. -## Current baseline +## Components -- receives framed GX12 RC channel data over the dedicated AUX1 UART at 115200 8N1; +The ARM64 artifact intentionally contains two executables: + +- `groundstation-linux-arm64` — the long-running root supervisor. It owns the GX12 UART, logging, RC state, video lifecycle, and starts the display process. +- `groundstation-display-linux-arm64` — the unprivileged X11 kiosk renderer. It runs as `orangepi` on `DISPLAY=:0`; the supervisor owns it during normal use. + +Current capabilities: + +- receives framed GX12 RC channel data at 115200 8N1; - validates protocol version, frame length and CRC-16; -- logs final mixed channel values; -- starts an independent kiosk display process that maps a fullscreen black canvas on `DISPLAY=:0` before video, tolerates X11 downtime without restarting the supervisor, and stops deterministically on shutdown; -- video keeps rendering priority: the kiosk canvas stays below the `fpv-video` window, shows the exact fallback text `Brak połączenia WFB` only while no video output is presented, and future display targets (MIPI/touch) get their own target-specific templates instead of one global layout. +- logs channel changes instead of every frame; +- starts/stops `fpv-video` from the configured CH6 power/lifecycle control; +- starts an independent kiosk display before video, tolerates temporary X11 downtime without restarting the supervisor, and stops deterministically on shutdown; +- keeps the fallback canvas below `fpv-video`, with `Brak połączenia WFB` while video is absent. -The first baseline is intentionally small. It does **not** yet control an AUX, manipulate the flight-control link, or replace the primary video pipeline. +Future MIPI/touch targets use their own template rather than sharing one global layout. Flight OSD will use a separate transparent layer above video, preserving the pilot stream's priority. -## Run +## Installation on Orange Pi + +Extract the artifact and install **both** files: ```bash -# Supervisor (YAML configuration, default /etc/groundstation/config.yml; -# hardware UART paths live in the config file) -groundstation -config /etc/groundstation/config.yml +sudo install -m 0755 groundstation-linux-arm64 /usr/local/bin/groundstation +sudo install -m 0755 groundstation-display-linux-arm64 /usr/local/bin/groundstation-display +sudo systemctl restart groundstation +``` -# Test input mode -groundstation -stdin +Check logs: -# Kiosk canvas alone (the supervisor starts it; standalone for debugging) +```bash +systemctl status groundstation --no-pager +journalctl -u groundstation -b --no-pager -n 80 +``` + +Do not run `groundstation-display` by hand during normal operation. It needs the X11 user/session environment and is managed by the supervisor. For isolated renderer debugging it supports: + +```bash groundstation-display -target x11 ``` +## Configuration + +The service reads `/etc/groundstation/config.yml`. For the verified permanent GX12 connection, use: + +```yaml +serial: + path: /dev/ttyS2 +``` + +`/dev/ttyS0` is the Orange Pi boot/debug UART and must not be used for the permanently connected GX12. + ## Runtime dependencies -- an X11 session on `:0` owned by the `orangepi` desktop user (the kiosk waits for X11 to appear and reconnects after display-manager restarts); -- `wmctrl` for the fullscreen/below kiosk window state — without it the canvas still maps but stays a plain window (logged); -- `runuser` from util-linux (base install) for the unprivileged kiosk launch. - -## Protocol - -The v1 protocol is described in `AGENTS.md` once the repository policy file is approved. It carries 16 final EdgeTX channel outputs as a binary framed message with CRC. - -## Project direction - -1. GX12 Ground Station UART mode sends final RC/switch state to Orange Pi. -2. Orange Pi logs and renders state while supervising the FPV video launcher. -3. Add WFB quality, recorder state, and MIPI touch display independently of pilot video. -4. Add a watchdog-protected override of exactly one reserved `SAFE/NORMAL/HIGH` AUX. +- X11 session on `:0` owned by `orangepi`; the kiosk waits for X11 and reconnects after display-manager restarts; +- `wmctrl` for fullscreen/below kiosk state; without it the canvas maps as a normal window and the condition is logged; +- `runuser` from util-linux for the unprivileged kiosk launch.