Skip to content

Commit b98bc57

Browse files
mavlink
1 parent 139aa1b commit b98bc57

14 files changed

Lines changed: 829 additions & 45 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ CMakeUserPresets.json
66
*.deb
77
examples/media/
88
examples/examples/
9+
/CMakeFiles

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ The video path has priority over everything else. `GlideView` should be able to
2828
- A development preview backend exists using SDL2 plus OpenGL ES. It is for WSL/Windows-style development only and does not replace the production DRM/KMS path.
2929
- `glide-ui` now uses LVGL v9.2.2 with LVGL's SDL backend for WSL development. Do not revive the hand-drawn GLES UI path.
3030
- `openhd-glide --preview-stack` runs the WSL development stack by launching `glide-flow` first and an LVGL/SDL `glide-ui` sidebar preview over the left side.
31+
- `examples/run-wsl-ui-preview.sh` now launches the full WSL preview stack by default so Flow OSD controls are visible with the UI; set `GLIDE_UI_ONLY=1` for the old standalone LVGL UI preview.
3132
- `openhd-glide --kms-stack` is the target-device stack entry point. It validates DRM/KMS probing, process startup, CPU assignment, Unix-socket IPC, and starts `glide-flow` with a direct GBM/EGL KMS scanout surface. `--kmd-stack` is accepted as a typo-compatible alias.
3233
- The WSL preview stack starts a controller-owned Unix socket at `/tmp/openhd-glide.sock` by default. Workers register with `hello <worker>`. `glide-ui` toggles the Flow FPS overlay by sending `set fps 0/1`; the controller broadcasts `state fps 0/1` to `glide-flow`.
34+
- `openhd-glide --preview-stack` and `--kms-stack` include a controller-owned MAVLink UDP bridge on port 14550 by default. It decodes common MAVLink telemetry and OpenHD/QOpenHD parameter/status traffic into the existing `mav ...` IPC state lines for OSD/UI use, and translates UI `mav set ...` / `mav command ...` actions back to MAVLink packets once a UDP peer is known.
3335
- `glide-ui` has a first LVGL QOpenHD-style sidebar shell: large icon rail, `Find Air Unit` scan panel, and an FPS overlay toggle in the `MISC` panel. In WSL the UI window is sized as a sidebar surface, not a transparent full-screen overlay.
3436
- On the device stack, `glide-ui --headless` is used until the LVGL shared-buffer/plane backend exists. Do not make the UI a DRM/KMS master.
3537
- `glide-view --udp-video --udp-port 5600` uses GStreamer to receive RTP/H.264 over UDP and decode into `appsink`. It must not use `kmssink` because only `openhd-glide` should own KMS. It prefers `v4l2h264dec`/`v4l2slh264dec` and warns if it falls back to `avdec_h264`.

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ add_library(glide_common
2525
src/common/ipc.cpp
2626
src/common/logging.cpp
2727
src/common/mavlink_state.cpp
28+
src/common/mavlink_udp_bridge.cpp
2829
src/common/preview_control.cpp
2930
src/platform/core_assignment.cpp
3031
src/platform/cpu_topology.cpp

README.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,10 @@ examples/run-wsl-ui-preview.sh
254254
examples/run-wsl-minimap-preview.sh
255255
```
256256

257-
For WSL development, `openhd-glide --preview-stack` starts `glide-flow` first and then places a
257+
For WSL development, `openhd-glide --preview-stack` starts `glide-view`, `glide-flow`, and then places a
258258
`glide-ui` LVGL/SDL preview over the left side. In WSL, the UI preview is kept as a sidebar surface to avoid
259-
covering the Flow preview. The sidebar contains a `MISC` panel with an FPS overlay toggle wired through the
260-
controller Unix socket.
259+
covering the Flow preview. The sidebar `OSD` panel has toggles for FPS, coordinates, and the Flow speed/altitude
260+
ladder versus compact text mode. The custom wind indicator is rendered by Flow inside the performance horizon.
261261
The UI preview also owns the LVGL minimap layer. Press `M` to cycle menu -> minimap -> hidden, or press `N` to
262262
cycle minimap -> menu -> hidden. When the minimap is visible, `+` zooms in and `-` zooms out. The two views share one
263263
layer and are never displayed at the same time.
@@ -301,8 +301,10 @@ The helper script does the same build and tile generation steps:
301301
examples/run-wsl-minimap-preview.sh
302302
```
303303

304-
`examples/run-wsl-ui-preview.sh` also generates the fake minimap tiles and exports `GLIDE_MINIMAP_TILE_ROOT`, so the
305-
integrated UI preview can show the round minimap with `M` or `N` immediately. Use `+` and `-` to zoom the map.
304+
`examples/run-wsl-ui-preview.sh` starts that full preview stack by default, generates the fake minimap tiles, and
305+
exports `GLIDE_MINIMAP_TILE_ROOT`, so the integrated UI preview can show the round minimap with `M` or `N`
306+
immediately. Use `GLIDE_UI_ONLY=1 examples/run-wsl-ui-preview.sh` for the old standalone UI window. Use `+` and `-`
307+
to zoom the map.
306308

307309
To install the WSL build into `~/.local`:
308310

@@ -333,9 +335,17 @@ Terminal and MAVLink-state IPC helpers:
333335
./build-kms/glide-send mav message "FC heartbeat received"
334336
```
335337

336-
`mav ...` lines are intentionally a temporary bridge contract: the real MAVLink reader should publish the same state
337-
updates to the controller IPC socket, and UI actions emit `mav set ...` / `mav command ...` lines that the MAVLink
338-
writer can translate into OpenHD parameter writes and commands.
338+
`mav ...` lines are the internal bridge contract: the controller publishes decoded MAVLink state to workers, and UI
339+
actions emit `mav set ...` / `mav command ...` lines that the controller can translate into OpenHD parameter writes and
340+
commands.
341+
342+
`openhd-glide --preview-stack` and `--kms-stack` now start a controller-owned MAVLink UDP bridge on `0.0.0.0:14550`
343+
by default. Override it with `--mavlink-udp-port <port>` or disable it with `--no-mavlink`. The bridge decodes common
344+
MAVLink flight data (`HEARTBEAT`, `ATTITUDE`, `GPS_RAW_INT`, `GLOBAL_POSITION_INT`, `VFR_HUD`, `SYS_STATUS`,
345+
`RC_CHANNELS`, `STATUSTEXT`) and OpenHD/QOpenHD-style parameter/status traffic (`PARAM_VALUE`, `PARAM_EXT_VALUE`,
346+
plus custom OpenHD message IDs as status lines), then broadcasts normalized `mav ...` state to Flow and UI. Menu
347+
actions are translated back into MAVLink `PARAM_EXT_SET`, numeric `PARAM_SET`, or command-long packets when a peer has
348+
been seen on the UDP socket.
339349

340350
UI navigation is directional: `up/down` moves through the sidebar or focused setting rows, `right` enters the settings
341351
panel, `left` returns to the sidebar or collapses it, and `enter` activates the focused row.

docs/architecture.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,12 @@ uses before drawing the FPS overlay. The socket path can be changed with `--ipc-
9191

9292
The LVGL UI is intentionally backend-isolated: the current SDL display driver is for WSL/Windows-style iteration. The target backend should render the same LVGL tree into shared buffers or a plane-owned buffer path supplied by `openhd-glide`, without letting `glide-ui` become DRM/KMS master.
9393

94-
The UI and Flow consume a small controller-broadcast `mav ...` IPC vocabulary while the real MAVLink bridge is being
95-
built. This keeps the rendering processes decoupled from MAVLink transport details: a bridge can publish snapshots such
96-
as `mav alive air 1`, `mav link 5745 20 2 1200`, `mav scan 50`, and `mav message ...`; UI actions emit `mav set ...` or
97-
`mav command ...` for the bridge to translate into OpenHD `PARAM_EXT_*` writes and command-long calls.
94+
The UI and Flow consume a small controller-broadcast `mav ...` IPC vocabulary. The controller owns the MAVLink UDP
95+
bridge by default on `0.0.0.0:14550` (`--mavlink-udp-port`, `--no-mavlink`) and keeps rendering processes decoupled
96+
from transport details. It decodes common flight messages into snapshots such as `mav attitude ...`, `mav position ...`,
97+
`mav speed ...`, `mav battery ...`, `mav rc ...`, plus OpenHD/QOpenHD-style `PARAM_VALUE` and `PARAM_EXT_VALUE` settings
98+
into `mav param ...`. UI actions emit `mav set ...` or `mav command ...`; the controller translates those back into
99+
MAVLink parameter writes or command-long packets once it has seen a UDP peer.
98100

99101
## Device KMS Mode
100102

examples/run-wsl-ui-preview.sh

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,40 @@ WIDTH="${GLIDE_UI_WIDTH:-760}"
88
HEIGHT="${GLIDE_UI_HEIGHT:-720}"
99
X="${GLIDE_PREVIEW_X:-80}"
1010
Y="${GLIDE_PREVIEW_Y:-40}"
11-
IPC_SOCKET="${GLIDE_IPC_SOCKET:-/tmp/openhd-glide-ui-preview.sock}"
11+
FLOW_WIDTH="${GLIDE_FLOW_WIDTH:-${GLIDE_WIDTH:-1280}}"
12+
FLOW_HEIGHT="${GLIDE_FLOW_HEIGHT:-${GLIDE_HEIGHT:-720}}"
13+
PORT="${GLIDE_VIEW_PORT:-5600}"
14+
CODEC="${GLIDE_VIEW_CODEC:-${GLIDE_CODEC:-h264}}"
15+
IPC_SOCKET="${GLIDE_IPC_SOCKET:-/tmp/openhd-glide.sock}"
1216
TILE_ROOT="${GLIDE_MINIMAP_TILE_ROOT:-${ROOT_DIR}/assets/maps}"
1317
MINIMAP_ZOOM="${GLIDE_MINIMAP_ZOOM:-15}"
18+
UI_ONLY="${GLIDE_UI_ONLY:-0}"
19+
20+
CODEC="$(printf "%s" "$CODEC" | tr '[:upper:]' '[:lower:]')"
21+
case "$CODEC" in
22+
h264|avc)
23+
CODEC="h264"
24+
;;
25+
h265|hevc)
26+
CODEC="h265"
27+
;;
28+
*)
29+
echo "unsupported codec '${CODEC}'; use h264 or h265" >&2
30+
exit 2
31+
;;
32+
esac
1433

1534
if [ "${GLIDE_SKIP_BUILD:-0}" != "1" ]; then
1635
if ! command -v cmake >/dev/null 2>&1; then
17-
echo "cmake is required to build glide-ui; set GLIDE_SKIP_BUILD=1 to reuse an existing binary" >&2
36+
echo "cmake is required to build the WSL preview; set GLIDE_SKIP_BUILD=1 to reuse existing binaries" >&2
1837
exit 1
1938
fi
2039
cmake -S "${ROOT_DIR}" -B "${BUILD_DIR}"
21-
cmake --build "${BUILD_DIR}" --target glide-ui -j"$(nproc)"
40+
if [ "${UI_ONLY}" = "1" ]; then
41+
cmake --build "${BUILD_DIR}" --target glide-ui -j"$(nproc)"
42+
else
43+
cmake --build "${BUILD_DIR}" --target openhd-glide glide-view glide-flow glide-ui -j"$(nproc)"
44+
fi
2245
fi
2346

2447
if [ "${GLIDE_SKIP_TILE_GEN:-0}" != "1" ]; then
@@ -30,27 +53,56 @@ if [ "${GLIDE_SKIP_TILE_GEN:-0}" != "1" ]; then
3053
fi
3154
fi
3255

33-
if [ ! -x "${BUILD_DIR}/glide-ui" ]; then
34-
echo "missing executable: ${BUILD_DIR}/glide-ui" >&2
35-
exit 1
56+
if [ "${UI_ONLY}" = "1" ]; then
57+
REQUIRED_BINARIES="glide-ui"
58+
else
59+
REQUIRED_BINARIES="openhd-glide glide-view glide-flow glide-ui"
3660
fi
3761

38-
if command -v ldd >/dev/null 2>&1; then
39-
LDD_OUTPUT="$(ldd "${BUILD_DIR}/glide-ui" 2>&1 || true)"
40-
MISSING_LIBS="$(printf "%s\n" "${LDD_OUTPUT}" | sed -n '/=>.*not found/p;/Error loading shared library/p')"
41-
if [ -n "${MISSING_LIBS}" ]; then
42-
echo "glide-ui is missing runtime libraries:" >&2
43-
echo "${MISSING_LIBS}" >&2
62+
for binary in ${REQUIRED_BINARIES}; do
63+
if [ ! -x "${BUILD_DIR}/${binary}" ]; then
64+
echo "missing executable: ${BUILD_DIR}/${binary}" >&2
4465
exit 1
4566
fi
67+
done
68+
69+
if command -v ldd >/dev/null 2>&1; then
70+
for binary in ${REQUIRED_BINARIES}; do
71+
LDD_OUTPUT="$(ldd "${BUILD_DIR}/${binary}" 2>&1 || true)"
72+
MISSING_LIBS="$(printf "%s\n" "${LDD_OUTPUT}" | sed -n '/=>.*not found/p;/Error loading shared library/p')"
73+
if [ -n "${MISSING_LIBS}" ]; then
74+
echo "${binary} is missing runtime libraries:" >&2
75+
echo "${MISSING_LIBS}" >&2
76+
exit 1
77+
fi
78+
done
79+
fi
80+
81+
if [ "${UI_ONLY}" = "1" ]; then
82+
GLIDE_MINIMAP_TILE_ROOT="${TILE_ROOT}" \
83+
GLIDE_MINIMAP_ZOOM="${MINIMAP_ZOOM}" \
84+
exec "${BUILD_DIR}/glide-ui" \
85+
--preview \
86+
--width "${WIDTH}" \
87+
--height "${HEIGHT}" \
88+
--x "${X}" \
89+
--y "${Y}" \
90+
--ipc-socket "${IPC_SOCKET}"
4691
fi
4792

93+
echo "Starting WSL preview stack with Flow OSD, LVGL UI, and View decode worker." >&2
94+
echo "Feed video with: examples/stream-videotestsrc-to-glide-view.sh 127.0.0.1 ${PORT}" >&2
95+
4896
GLIDE_MINIMAP_TILE_ROOT="${TILE_ROOT}" \
4997
GLIDE_MINIMAP_ZOOM="${MINIMAP_ZOOM}" \
50-
exec "${BUILD_DIR}/glide-ui" \
51-
--preview \
52-
--width "${WIDTH}" \
53-
--height "${HEIGHT}" \
54-
--x "${X}" \
55-
--y "${Y}" \
98+
exec "${BUILD_DIR}/openhd-glide" \
99+
--preview-stack \
100+
--preview-width "${FLOW_WIDTH}" \
101+
--flow-height "${FLOW_HEIGHT}" \
102+
--ui-width "${WIDTH}" \
103+
--ui-height "${HEIGHT}" \
104+
--preview-x "${X}" \
105+
--preview-y "${Y}" \
106+
--view-udp-port "${PORT}" \
107+
--view-udp-codec "${CODEC}" \
56108
--ipc-socket "${IPC_SOCKET}"

manual-wsl.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ PowerShell form:
9090
wsl -d Ubuntu-22.04 -- openhd-glide --preview-stack --preview-width 1280 --flow-height 720 --ui-width 760 --preview-x 60 --preview-y 40 --ui-opacity 1.0
9191
```
9292

93-
The UI `OSD` panel has switches for the video FPS overlay and the compact Flow coordinate field. The coordinate switch sends `set coords 0/1` through the preview IPC socket.
93+
The UI `OSD` panel has switches for the video FPS overlay, coordinates, and the speed/altitude ladder versus compact text mode. The wind indicator is drawn by Flow in the performance horizon, so run the controller-owned preview stack or `examples/run-wsl-ui-preview.sh` without `GLIDE_UI_ONLY=1` when checking it.
9494

9595
## Rebuild And Reinstall
9696

src/common/mavlink_state.cpp

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "common/mavlink_state.hpp"
22

33
#include <algorithm>
4+
#include <cctype>
45
#include <sstream>
56

67
namespace glide::mavlink {
@@ -36,6 +37,23 @@ void push_message(Snapshot& snapshot, std::string message)
3637
snapshot.messages.back() = std::move(message);
3738
}
3839

40+
std::string upper_copy(std::string value)
41+
{
42+
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
43+
return static_cast<char>(std::toupper(c));
44+
});
45+
return value;
46+
}
47+
48+
int parse_int_or(int fallback, const std::string& value)
49+
{
50+
try {
51+
return std::stoi(value);
52+
} catch (...) {
53+
return fallback;
54+
}
55+
}
56+
3957
} // namespace
4058

4159
bool apply_ipc_line(Snapshot& snapshot, const std::string& line)
@@ -67,6 +85,35 @@ bool apply_ipc_line(Snapshot& snapshot, const std::string& line)
6785
snapshot.armed = armed != 0;
6886
return true;
6987
}
88+
if (key == "attitude") {
89+
stream >> snapshot.roll_degrees >> snapshot.pitch_degrees >> snapshot.yaw_degrees;
90+
snapshot.attitude_valid = true;
91+
return true;
92+
}
93+
if (key == "position") {
94+
stream >> snapshot.latitude_deg >> snapshot.longitude_deg >> snapshot.altitude_m;
95+
snapshot.position_valid = true;
96+
snapshot.altitude_valid = true;
97+
return true;
98+
}
99+
if (key == "speed") {
100+
stream >> snapshot.ground_speed_mps >> snapshot.airspeed_mps;
101+
snapshot.speed_valid = true;
102+
return true;
103+
}
104+
if (key == "battery") {
105+
stream >> snapshot.voltage_v >> snapshot.battery_percent;
106+
snapshot.battery_valid = true;
107+
return true;
108+
}
109+
if (key == "gps") {
110+
stream >> snapshot.satellites;
111+
return true;
112+
}
113+
if (key == "mode") {
114+
snapshot.flight_mode = rest_after(stream);
115+
return true;
116+
}
70117
if (key == "link") {
71118
stream >> snapshot.frequency_mhz >> snapshot.channel_width_mhz >> snapshot.mcs_index >> snapshot.tx_power_mw;
72119
return true;
@@ -87,20 +134,37 @@ bool apply_ipc_line(Snapshot& snapshot, const std::string& line)
87134
std::string param;
88135
stream >> target >> param;
89136
const auto value = rest_after(stream);
90-
if (param == "RESOLUTION_FPS") {
137+
const auto upper_param = upper_copy(param);
138+
if (upper_param == "RESOLUTION_FPS" || upper_param == "VIDEO_FORMAT" || upper_param == "CAMERA_FORMAT") {
91139
snapshot.resolution_fps = value;
92-
} else if (param == "ROTATION_FLIP" || param == "ROTATION_DEG") {
140+
} else if (upper_param == "ROTATION_FLIP" || upper_param == "ROTATION_DEG" || upper_param == "VIDEO_ROTATION") {
93141
snapshot.rotation = value;
94-
} else if (param == "AIR_RECORDING_E") {
142+
} else if (upper_param == "AIR_RECORDING_E" || upper_param == "RECORDING" || upper_param == "REC_ENABLED") {
95143
snapshot.recording = value;
96-
} else if (param == "WIFI_MODE" && target == "air") {
144+
} else if (upper_param == "WIFI_MODE" && target == "air") {
97145
snapshot.air_wifi_mode = value;
98-
} else if (param == "WIFI_MODE" && target == "ground") {
146+
} else if (upper_param == "WIFI_MODE" && target == "ground") {
99147
snapshot.ground_wifi_mode = value;
100-
} else if (param == "WIFI_HOTSPOT_E" && target == "air") {
148+
} else if (upper_param == "WIFI_HOTSPOT_E" && target == "air") {
101149
snapshot.air_hotspot = value;
102-
} else if (param == "WIFI_HOTSPOT_E" && target == "ground") {
150+
} else if (upper_param == "WIFI_HOTSPOT_E" && target == "ground") {
103151
snapshot.ground_hotspot = value;
152+
} else if (upper_param == "FREQ" || upper_param == "FREQUENCY" || upper_param == "FREQUENCY_MHZ" || upper_param == "WB_FREQUENCY") {
153+
snapshot.frequency_mhz = parse_int_or(snapshot.frequency_mhz, value);
154+
} else if (upper_param == "CHANNEL_WIDTH" || upper_param == "CHANNEL_WIDTH_MHZ" || upper_param == "WB_CHANNEL_WIDTH" || upper_param == "BANDWIDTH") {
155+
snapshot.channel_width_mhz = parse_int_or(snapshot.channel_width_mhz, value);
156+
} else if (upper_param == "MCS" || upper_param == "MCS_INDEX" || upper_param == "WB_MCS_INDEX") {
157+
snapshot.mcs_index = parse_int_or(snapshot.mcs_index, value);
158+
} else if (upper_param == "TX_POWER" || upper_param == "TX_POWER_MW" || upper_param == "WB_TX_POWER_MW") {
159+
snapshot.tx_power_mw = parse_int_or(snapshot.tx_power_mw, value);
160+
} else if (upper_param == "OPENHD_VERSION" || upper_param == "VERSION") {
161+
snapshot.openhd_version = value;
162+
} else if (upper_param == "AIR_CHIPSET") {
163+
snapshot.air_chipset = value;
164+
} else if (upper_param == "GROUND_CHIPSET") {
165+
snapshot.ground_chipset = value;
166+
} else if (upper_param == "CAMERA" || upper_param == "CAMERA_TYPE") {
167+
snapshot.camera = value;
104168
}
105169
return true;
106170
}

src/common/mavlink_state.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,29 @@ struct Snapshot {
1010
bool ground_alive {};
1111
bool fc_alive {};
1212
bool armed {};
13+
bool attitude_valid {};
14+
bool altitude_valid {};
15+
bool speed_valid {};
16+
bool position_valid {};
17+
bool battery_valid {};
1318
int frequency_mhz { 5745 };
1419
int channel_width_mhz { 20 };
1520
int mcs_index { 2 };
1621
int tx_power_mw { 1200 };
1722
int scan_progress { 0 };
1823
int rc_channels[4] { 1500, 1500, 1000, 1500 };
24+
int battery_percent { -1 };
25+
int satellites {};
26+
float roll_degrees {};
27+
float pitch_degrees {};
28+
float yaw_degrees {};
29+
float altitude_m {};
30+
float ground_speed_mps {};
31+
float airspeed_mps {};
32+
float voltage_v {};
33+
double latitude_deg {};
34+
double longitude_deg {};
35+
std::string flight_mode { "N/A" };
1936
std::string resolution_fps { "1920x1080@120" };
2037
std::string rotation { "0 deg" };
2138
std::string recording { "Disabled" };

0 commit comments

Comments
 (0)