Background service and overlay widget: Winmate GCS joystick → RC_CHANNELS_OVERRIDE + MAVLink telemetry for ArduPilot on Linux.
- Reads the Winmate GCS Joystick (or any Linux input device) via
evdev - Sends
RC_CHANNELS_OVERRIDEto ArduPilot at 50 Hz through MAVProxy — independent of QGC - Displays an always-on-top telemetry overlay in the top-right corner of the screen (210×280 px)
- Parses drone state from MAVLink (
HEARTBEAT,ATTITUDE,SYS_STATUS,GPS_RAW_INT,VFR_HUD)
/dev/input/eventX → widget.py / joystick_reader.py → RC_CHANNELS_OVERRIDE (50 Hz) → MAVProxy → ArduPilot
ArduPilot :14550 → MAVProxy only (master)
├─ 14551 → QGC
└─ 14552 → widget (telemetry + RC)
MAV_widget/
├── widget.py # Tkinter telemetry overlay + main entry point
├── joystick_reader.py # evdev joystick reader (auto-detect Winmate)
├── mavlink_link.py # MAVLink RX + RC override TX (50 Hz)
├── drone_state.py # shared telemetry and joystick state
├── config.py # axis/button mapping, MAVLink URI, RC channels
├── probe_input.py # interactive input device probe tool
├── test_joystick.py # quick joystick test
├── requirements.txt
├── docs/
│ ├── MAVPROXY_QGC.md # MAVProxy + QGC port setup and troubleshooting
│ ├── AP_CLIENTS.md # MAVLink + RTSP for Wi‑Fi AP clients
│ └── SECRETS.md # what must not be committed (passwords, keys)
├── INSTALL.md # full GCS installation (network, AP, concurrent Wi‑Fi)
├── scripts/
│ ├── run_widget.sh # launch widget immediately (no MAVProxy wait)
│ ├── start_mavproxy.sh # start MAVProxy daemon
│ ├── wait_gcs_ready.sh # fast hostapd / MAVProxy / heartbeat waits
│ ├── wait_mavproxy_link.sh # wrapper for optional heartbeat wait
│ ├── autostart-gcs.sh # manual / legacy → mavproxy-gcs.service
│ ├── toggle-screen-lock.sh # kiosk mode on/off
│ ├── start-drone-hotspot.sh
│ ├── stop-drone-hotspot.sh
│ ├── toggle-ap.sh # AP on/off (called by tray)
│ ├── gcs-ap-tray.py # top panel tray icon
│ ├── install-ap-tray.sh # install tray + toggle only
│ ├── video-udp-relay.py # eth0:5600 → 127.0.0.1:5601 (RTP copy)
│ ├── start-video-rtsp.sh # MediaMTX + ffmpeg RTP → RTSP on AP
│ ├── check-ap-stream.sh # verify AP MAVLink + video relay
│ └── install-mediamtx.sh
├── systemd/
│ ├── mavproxy-gcs.service # MAVProxy on login (independent of widget)
│ ├── mav-widget.service
│ └── drone-hotspot.service
├── setup_autostart.sh # install widget user service
└── setup_wifi_ap.sh # install Wi-Fi AP boot service
Main application. Connects to MAVProxy on udp:127.0.0.1:14552 for telemetry and RC. Port 14550 is MAVProxy master only — do not point the widget or QGC at it.
Overlay window
| Property | Value |
|---|---|
| Size | 210 × 280 px |
| Position | Top-right of the primary screen (20 px margin) |
| Flags | Always on top, no window decorations |
| Content | Link status, flight mode, battery, GPS, attitude, stick values, button states |
Position is computed at startup from root.winfo_screenwidth(). Override with --geometry +X+Y if needed (Tk format: offset from top-left).
python3 widget.py # top-right (default)
python3 widget.py --geometry +20+20 # fixed positionReads stick axes and buttons from /dev/input/eventX using evdev. Auto-detects the Winmate GCS Joystick by name. Seeds initial axis values on open.
MAVLink connection: parses telemetry in a background thread and sends RC_CHANNELS_OVERRIDE at 50 Hz when the joystick is active. RC is sent through MAVProxy with target_component=1 (autopilot).
Winmate GCS Joystick layout (verified manually):
| Stick | evdev | RC channel |
|---|---|---|
| Left X | ABS_X |
1 — roll |
| Left Y | ABS_Y |
2 — pitch |
| Right X | ABS_Z |
4 — yaw |
| Right Y | ABS_RX |
3 — throttle |
| Button | RC channel |
|---|---|
Left top (BTN_NORTH) |
5 |
Left middle (BTN_WEST) |
6 |
Left bottom (BTN_TL2) |
7 |
Left side (BTN_C) |
8 |
Right top (BTN_TL) |
9 |
Right bottom (BTN_TR) |
10 |
Right side (BTN_Z) |
11 |
Left stick press (BTN_A) |
12 |
Right stick press (BTN_B) |
13 |
Vertical axes (ABS_Y, ABS_RX) are inverted in software — see AXIS_INVERT in config.py.
- Winmate GCS, built‑in radio on
usb0(from scratch, one command): docs/INSTALL_WINMATE.md - Dev‑board layout (external USB‑Ethernet radio →
eth0): INSTALL.md
pip install -r requirements.txtpython3 probe_input.py
# or
python3 -c "from evdev import list_devices, InputDevice
for p in list_devices():
d = InputDevice(p)
if 'Joystick' in d.name: print(p, d.name)"Note:
/dev/input/eventXmay change after reboot. The widget auto-detects the Winmate joystick by name.
python3 widget.py
python3 widget.py -v # verbose logging
python3 widget.py --no-joystick # telemetry only, no RC override
python3 widget.py --device /dev/input/event9
python3 widget.py --geometry +100+50 # manual position (default: top-right)run_widget.sh starts the overlay immediately. MAVProxy and the drone link are independent — mavlink_link.py reconnects in the background (shows NO LINK until telemetry arrives).
MAVProxy is the only process that talks to the drone on UDP 14550. QGC and the widget receive a forwarded copy on separate local ports.
| Port | Address | Client |
|---|---|---|
| 14550 | 192.168.53.1 |
MAVProxy --master (radio / eth0 to drone) |
| 14551 | 127.0.0.1 |
QGroundControl (listen) |
| 14552 | 127.0.0.1 |
This widget (telemetry + RC) |
Start MAVProxy (same as scripts/autostart-gcs.sh):
python3 ~/.local/bin/mavproxy.py \
--master=udpin:192.168.53.1:14550 \
--out=127.0.0.1:14551 \
--out=127.0.0.1:14552 \
--out=udpbcast:192.168.54.255:14550 \
--out=udpin:192.168.54.1:14550 \
--nowait \
--force-connected \
--non-interactiveSee docs/MAVPROXY_QGC.md for official MAVProxy/QGC references and troubleshooting (mav.tlog empty, no telemetry, port conflicts).
| Setting | Value |
|---|---|
| Type | UDP |
| Port | 14551 (Listen) |
| Disable | Settings → General → AutoConnect to UDP (QGC otherwise binds 14550 and blocks MAVProxy) |
| Comm Links | One manual UDP link on 14551 only — no link on 14550 |
In ~/.config/QGroundControl/QGroundControl.ini:
[AutoConnect]
autoConnectUDP=falseInternet sources (uplinks) Recipients (NAT clients)
wlan0 ──┐ uap0 → AP users 192.168.54.x
eth1 ──┼── MASQUERADE ──► eth0 → radio net 192.168.53.x (GW 192.168.53.1)
└── FORWARD ───────────►
| Interface | Role |
|---|---|
wlan0 |
Wi‑Fi internet uplink |
eth0 |
Built-in radio — static 192.168.53.1/24, no default route |
eth1 |
USB debug internet — DHCP (192.168.0.x) |
uap0 |
AP for clients (192.168.54.1/24) — receives internet via NAT |
AP clients — MAVLink broadcast on UDP 14550, video RTSP rtsp://192.168.54.1:8554/stream (RTP H.264 from drone UDP 5600, relayed via ffmpeg/MediaMTX). See docs/AP_CLIENTS.md. Check: check-ap-stream.sh.
Interface names are fixed at boot via systemd .link (MAC → eth0/eth1).
Reboot required after first install. Boot service gcs-network.service re-applies static IP if needed.
sudo ./scripts/setup_network.sh
sudo reboot
check-network.shDev PC (Windows): USB adapter 192.168.0.x = debug internet (direct, not via GCS).
Radio/debug Ethernet 192.168.53.x → gateway 192.168.53.1 (GCS) — internet only through NAT on the GCS.
sudo ./setup_wifi_ap.sh # AP + NAT rules
./scripts/check-nat.sh # from repo (or check-nat.sh after sudo ./setup_autostart.sh)
sudo ./scripts/setup-nat.shAfter setup_wifi_ap.sh:
- Tray icon in the top panel (
gcs-ap-tray) — right-click: on/off, settings - Terminal:
toggle-ap.sh
Uses drone-hotspot.service (not GNOME Settings → Hotspot).
AP clients get gateway 192.168.54.1 via dnsmasq. setup-nat.sh also masquerades phone traffic from 192.168.54.0/24 out eth0, so phones can reach 192.168.53.x devices even if ArduPilot/sonar do not have a route back to the AP subnet.
# MAVProxy + widget (systemd user services, ordered startup)
./setup_autostart.sh
# Wi-Fi AP + hotspot boot service
sudo ./setup_wifi_ap.shBoot after login — MAVProxy and widget start in parallel (no waits between them):
drone-hotspot.service— Wi-Fi AP +hostapd(at boot)mavproxy-gcs.service— MAVProxy whenhostapdis up (--force-connectedkeeps it running before the drone link is up)mav-widget.service— overlay immediately; telemetry connects when MAVProxy/drone are ready
Typical time to overlay: under 1 s after graphical session.
Install or update system scripts:
sudo ./setup_autostart.sh
sudo install -m 755 scripts/start_mavproxy.sh /usr/local/bin/start_mavproxy.sh
sudo install -m 755 scripts/wait_gcs_ready.sh /usr/local/bin/wait_gcs_ready.sh
systemctl --user restart mavproxy-gcs mav-widgetUseful commands:
systemctl --user status mavproxy-gcs mav-widget
mav-gcs-logs.sh -f # follow log files
tail -f ~/.local/state/mav-gcs/mav-widget.logLogs: On some systems
journalctl --userreturnsNo journal files were found— user journal is not persisted. Usesystemctl --user status(shows recent lines),mav-gcs-logs.sh, orjournalctl -b | grep run_widget.
| Library | Purpose |
|---|---|
pymavlink |
MAVLink encode/decode (ArduPilot dialect) |
evdev |
Linux input events — sticks, buttons |
tkinter |
Overlay UI (stdlib) |
pymavlink>=2.4.40
evdev>=1.7.0
[Winmate GCS Joystick]
│ /dev/input/eventX
▼
[widget.py + joystick_reader.py] 50 Hz RC override
│ RC_CHANNELS_OVERRIDE (#70), 13 channels
▼
[widget.py] udp:127.0.0.1:14552 (via MAVProxy)
[MAVProxy] udp:127.0.0.1:14551 (QGC)
│
▼
[ArduPilot / Pixhawk] via radio link (192.168.53.1:14550)
│ MAVLink telemetry
▼
[widget.py + mavlink_link.py]
└── parse → DroneState → overlay UI
MIT © 2026