Merge remote-tracking branch 'origin/main' into issue-8-split-kiosk-osd

# Conflicts:
#	README.md
This commit is contained in:
2026-10-01 15:44:05 +02:00
3 changed files with 140 additions and 37 deletions
+46 -28
View File
@@ -1,44 +1,62 @@
# 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 two independent layers on `DISPLAY=:0` for the Viture target before video: the fullscreen fallback/menu canvas below video, and a transparent, input-transparent OSD overlay above video reserved as the future flight OSD rendering surface; both tolerate X11 downtime without restarting the supervisor and stop 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 the OSD overlay stays above video without ever presenting pixels over it or intercepting its input; 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 fullscreen fallback/menu canvas below `fpv-video`, showing `Brak połączenia WFB` only while video is absent;
- maps a transparent, input-transparent OSD overlay above `fpv-video` as the reserved flight OSD rendering surface: it never presents pixels over the video or intercepts its input.
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 templates rather than sharing one global layout. The flight OSD will render on the existing 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 the fullscreen/stacking kiosk window states; without it the kiosk windows map as normal windows and the condition is logged;
- `runuser` from util-linux for the unprivileged kiosk launch.
+35 -9
View File
@@ -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
}
+59
View File
@@ -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)
}
}