Skip to content

Add HDZero-inspired channel scan to the GS menu - #150

Open
moro161 wants to merge 12 commits into
OpenIPC:masterfrom
moro161:channel-scan-menu
Open

moro161 wants to merge 12 commits into
OpenIPC:masterfrom
moro161:channel-scan-menu

Conversation

@moro161

@moro161 moro161 commented Sep 16, 2026

Copy link
Copy Markdown

Summary

  • New System -> WFB-NG -> Channel Scan page in the GS menu, inspired by HDZero goggles' channel scanner: lists every channel with a bar showing how much RF activity was heard on it, noisiest first.
  • Uses the existing dynamic-page pattern (.build/.prefetch, same as the WiFi-networks page) so the scan runs behind a spinner without freezing the UI.
  • Each row is selectable, not just informational: picking a channel calls the same set gs wfbng gs_channel command the existing "Channel" dropdown already uses, so it also pushes the change to the air unit when a drone link is detected.
  • Documents the new chanscan_results protocol entry in the gsmenu.sh integrator template. The real scan implementation (hopping a secondary RX NIC, sampling activity, restoring the live channel) is integrator/platform-specific and lives in the deploying project's own gsmenu.sh, same as every other air/gs parameter here.

Test plan

  • Built and deployed to a real RunCam WiFiLink-RX (RK3566) ground station.
  • Confirmed the scan sweeps the live channel's own band, sorts noisiest-first, and each row is navigable/selectable.
  • Confirmed selecting a channel issues the same command path as the existing Channel dropdown.

🤖 Generated with Claude Code

moro161 and others added 12 commits September 10, 2026 20:16
out_appsink (live RTP pipeline and DVR file playback) set drop=true
with no max-buffers, which defaults to 0 (unlimited) in GStreamer, so
drop=true was a no-op: a decode/pull stall let frames pile up with no
bound, and the picture would catch up in fast-forward once the stall
cleared instead of skipping ahead to live. Add max-buffers=1.

Also trim the restream branch's queue from max-size-time=1s to 150ms
so a slow/stalled restream peer bounds out sooner, and bump the
socket-read packet-rate log from debug to an [FPS-TRACE] info line so
it lines up with the decode/display bottleneck counters.
Drop the once/sec [FPS-TRACE] socket packet-rate log added while
bottleneck-hunting; the decode/handoff/display counters it was meant
to line up with (main.cpp) were never committed and have also been
removed. Back to the original debug-level packet-rate log.
The dev checkout's parent directory has a literal space in its name
("openipc@100hz wifilinkvrx"), which sits above both this repo and
sbc-groundstations. pkg-config emits -I/-L flags as one plain,
unquoted, space-joined string; CMake's FindPkgConfig naively splits
that on whitespace, so any path containing a space loses its -I/-L
prefix. Symptom when configuring a target (aarch64) build against the
sbc-groundstations buildroot sysroot: "fatal error: drm.h: No such
file or directory" from xf86drm.h, plus gcc warnings about sysroot
include paths being passed as bare linker inputs.

tools/cross_configure_target.sh documents the cause and regenerates a
pkg-config wrapper that forces PKG_CONFIG_* to a space-free /tmp
mirror of the buildroot output tree, then configures a build directory
against it. The wrapper's env assignments must be unconditional, not
${VAR:-default}: buildroot's generated toolchainfile.cmake exports
PKG_CONFIG_SYSROOT_DIR (pointing at the real, space-having path) before
pkg-config ever runs, which silently defeats a fallback-style default.
The kernel-generated CVT-RB and standard CVT timings for 1280x720@100
either corrupt the picture (cropping/miscoloring/striping) or produce
no signal at all on this RK3566 board's HDMI-in on a Skyzone O4X Pro
(firmware 4.2.1). Even the "textbook" CEA-family timing captured from
an EDID describing a real Walksnail VRX's expected output still crops
on this unit.

Add the modeline arrived at by hand-tuning porches via live modetest
against this specific VRX+goggles pairing (mainly a larger horizontal
back porch and adjusted vertical front porch), applied only when the
requested mode is exactly 1280x720@100. Also force quant_range=1
(limited range) on the connector in the same atomic commit as the
modeset -- a non-atomic property write was silently ignored and left
colors oversaturated.

This is flagged experimental/hardware-specific, not a general default:
directly verified not to generalize to the textbook timing presumably
correct for other Walksnail VRX + goggles pairings. See issue_draft.md
for the full writeup and the open question about actual Walksnail VRX
HDMI output timing.
Adds a master switch (dvr_on_signal_enabled) that starts DVR recording
when the GS starts receiving data from the air unit and stops it when
that signal drops out. Wired into menu.c's drone_detect_timer -- the
same detected/lost transition that already greys out the drone pages
-- so no separate polling is needed, plus a live GS-menu toggle
(Auto-record on Signal, on the DVR settings page) and a --dvr-on-signal
CLI flag for starting enabled from launch.
New IconSelectorWidget entries in config_osd.json driven by the added
src/icons/*.png set: an armed/disarmed indicator, an 8-point compass
heading icon, and a GPS fix-quality icon (2D/3D/no-fix/none).
Reverts the icon-based widgets and their PNG assets, no longer
referenced anywhere in config_osd.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWYaFJL5iNrmr73dY6Tvuz
The VTX's SoC temp isn't local hardware and isn't known to the flight
controller, so it can't go through the existing os_sensors/MSP paths.
A small script on the VTX opens a plain TCP connection over the
wfb-ng tunnel and writes the temperature as ASCII; VtxTempSensor
listens for it independently of MspDisplayPortWidget telemetry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWYaFJL5iNrmr73dY6Tvuz
SO_REUSEADDR: on some boards an unrelated process (wpa_supplicant/
udhcpc sharing a duplicated fd) intermittently holds this port at
boot, failing bind() and silently aborting widget setup -- the
startup preview then stays stuck with nothing left to clear it.

Buffer: msposd aggregates multiple MSP DisplayPort commands per UDP
datagram (wfb-ng's radio_mtu is 1445 bytes), so a full-screen OSD
update can exceed the previous 1024-byte buffer. recvfrom() silently
truncates oversized datagrams, dropping trailing commands (often the
terminating DRAW_SCREEN) and leaving stale glyphs on screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWYaFJL5iNrmr73dY6Tvuz
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds System -> WFB-NG -> Channel Scan: a colmenu dynamic page (build/
prefetch, same pattern as the existing WiFi-networks page) that reads
per-channel RF activity from gsmenu.sh (channel:count lines), sorts
noisiest-first, and renders each as a selectable row with a bullet-dot
bar. Selecting a row applies that channel via the same "set gs wfbng
gs_channel" command the existing Channel dropdown already uses, so it
also pushes to the air unit when a drone link is detected.

The actual scan (hopping the secondary RX NIC, sampling rx_packets,
restoring the live channel) lives in the integrator's gsmenu.sh, not
here -- see debian/manpage.md's sibling gsmenu.sh template for the new
"chanscan_results" protocol entry this page expects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add channel scanning, signal DVR, and receiver reliability fixes

✨ Enhancement 🐞 Bug fix 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds asynchronous, selectable RF channel scanning through integrator-provided scan results.
• Adds signal-driven DVR recording and tunneled VTX temperature telemetry.
• Fixes media buffering, OSD UDP handling, custom 720p100 output, and cross-build setup.
Diagram

graph TD
  Menu["GS Menu"] --> Backend["gsmenu.sh"] --> RX["Secondary RX"]
  Air["Air Unit"] --> Signal["Link Detector"] --> DVR["DVR Control"]
  Air --> Temp["VTX Temp Sensor"] --> OSD["OSD Facts"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Split changes by subsystem
  • ➕ Separates channel scanning from DRM, DVR, telemetry, media, and build changes.
  • ➕ Enables focused hardware testing and safer rollback.
  • ➕ Reduces reviewer context switching and regression scope.
  • ➖ Requires several PRs and additional coordination.
  • ➖ Delays landing improvements that may already be deployed together.
2. Implement scanning in the application
  • ➕ Could provide typed results, validation, cancellation, and consistent error handling.
  • ➕ Would reduce dependence on shell output parsing.
  • ➖ Couples the application to platform-specific wireless interfaces and driver behavior.
  • ➖ Duplicates the existing gsmenu.sh integration boundary.
  • ➖ Makes support for differing receiver hardware more difficult.

Recommendation: Keep the channel scanner behind the existing gsmenu.sh dynamic-page boundary because channel hopping and activity measurement are platform-specific. However, split the unrelated DRM, DVR, telemetry, GStreamer, OSD, and build-tool changes into focused PRs so each can receive appropriate hardware testing and review.

Files changed (13) +390 / -7

Enhancement (8) +250 / -3
drm.cAdd a tuned 1280x720@100 DRM mode +40/-0

Add a tuned 1280x720@100 DRM mode

• Uses a hand-tuned user-defined modeline for exact 1280x720@100 requests and logs its calculated refresh. Atomic commits also request limited-range output for this mode.

src/drm.c

dvr.cppStore the signal-recording runtime switch +3/-0

Store the signal-recording runtime switch

• Defines the global flag controlling whether DVR recording follows air-unit signal availability.

src/dvr.cpp

dvr.hExpose signal-recording state +6/-0

Expose signal-recording state

• Declares the signal-driven DVR flag and documents its relationship to the menu's drone-detection timer.

src/dvr.h

colmenu_pages.cAdd channel scanning and automatic recording controls +72/-2

Add channel scanning and automatic recording controls

• Adds an asynchronously prefetched Channel Scan page that parses, sorts, normalizes, and bar-graphs activity results. Scan rows apply channels through the existing backend command, while the DVR page gains a live Auto Record switch.

src/gsmenu/colmenu_pages.c

main.cppWire signal DVR and VTX temperature monitoring +19/-1

Wire signal DVR and VTX temperature monitoring

• Adds CLI parsing, help text, and C-callable accessors for signal-driven recording. It also registers the VTX TCP temperature sensor on port 15552 and removes stray whitespace.

src/main.cpp

menu.cDrive DVR recording from air-unit detection +7/-0

Drive DVR recording from air-unit detection

• Starts recording when the air unit becomes detectable and stops it when detection is lost, provided signal-driven recording is enabled. Simulator builds omit these recording actions.

src/menu.c

os_mon.cppReceive VTX temperature over TCP +100/-0

Receive VTX temperature over TCP

• Adds a threaded TCP sensor that accepts ASCII temperature samples, stores the latest value atomically, and publishes it as vtx.temperature. Socket shutdown and thread joining provide lifecycle cleanup.

src/os_mon.cpp

os_mon.hppExpose VTX temperature sensor registration +3/-0

Expose VTX temperature sensor registration

• Adds the OsSensors API for registering a TCP-based air-unit temperature source.

src/os_mon.hpp

Bug fix (2) +36 / -4
gstrtpreceiver.cppBound video and restream buffering +15/-3

Bound video and restream buffering

• Limits live and file-playback appsinks to one buffered frame so drop=true discards stale data. Reduces the leaky restream queue from one second to 150 milliseconds.

src/gstrtpreceiver.cpp

osd.cppHarden MSP DisplayPort UDP reception +21/-1

Harden MSP DisplayPort UDP reception

• Enables address reuse before binding the MSP UDP socket. Enlarges the receive buffer from 1 KiB to 8 KiB to avoid truncating aggregated OSD commands and warns when the buffer is filled.

src/osd.cpp

Documentation (1) +6 / -0
manpage.mdDocument signal-driven DVR recording +6/-0

Document signal-driven DVR recording

• Adds the --dvr-on-signal option to the command synopsis and detailed option reference.

debian/manpage.md

Other (2) +98 / -0
gsmenu.shDefine the channel-scan integration protocol +8/-0

Define the channel-scan integration protocol

• Adds the chanscan_results query template and documents the expected channel:count output. Actual NIC hopping and activity sampling remain the deploying integrator's responsibility.

gsmenu.sh

cross_configure_target.shAdd space-safe cross-build configuration +90/-0

Add space-safe cross-build configuration

• Creates a temporary space-free Buildroot symlink and pkgconf wrapper before configuring CMake. This prevents FindPkgConfig from splitting include and library paths when the checkout path contains spaces.

tools/cross_configure_target.sh

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (8) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. The Auto Record control does nothing 🐞 Bug ≡ Correctness
Description
colmenu_get() and do_set() do not special-case the runtime-only dvr_on_signal parameter, so
the row falls through to gsmenu.sh, whose integrator template has no matching get or set command.
Opening the page consequently reads the switch as off, while changing it reaches an unknown shell
command whose failure occurs before the deferred on_dvr_on_signal() callback can update
dvr_on_signal_enabled.
Code

src/gsmenu/colmenu_pages.c[503]

+    { .kind=COLMENU_SWITCH,   .label="Auto Record",       .param="dvr_on_signal",        .on_change=on_dvr_on_signal },
Evidence
The menu row registers dvr_on_signal, but switch initialization goes through colmenu_get() and
updates normally go through do_set() and gsmenu.sh; neither the menu functions nor the shell
template provide the required runtime-state cases. Although runtime getter and setter functions
exist, the row never calls the getter, and its setter callback is deliberately deferred until after
a successful shell command, making both runtime operations unreachable through this switch.

src/gsmenu/colmenu_pages.c[110-113]
src/gsmenu/colmenu_pages.c[501-512]
src/gsmenu/colmenu.c[100-106]
src/gsmenu/colmenu.c[391-420]
gsmenu.sh[446-481]
gsmenu.sh[566-570]
src/main.cpp[848-852]
src/gsmenu/colmenu.c[100-130]
src/gsmenu/colmenu.c[388-420]
gsmenu.sh[422-481]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `dvr_on_signal` menu parameter represents application runtime state, but it is routed through `gsmenu.sh`, which has no handler for it. As a result, the Auto Record switch cannot display or update `dvr_on_signal_enabled`.

## Fix Focus Areas
- src/gsmenu/colmenu_pages.c[500-512]
- src/gsmenu/colmenu_pages.c[110-113]
- src/gsmenu/colmenu.c[81-130]
- src/gsmenu/colmenu.c[399-421]
- src/main.cpp[848-852]

## Recommended Fix
Handle `dvr_on_signal` directly as application runtime state, following the existing restream controls. Expose `dvr_get_on_signal()` through the column-menu C interface and return its `0`/`1` value from `colmenu_get()` for this parameter; in `do_set()`, bypass `gsmenu.sh`, apply the change with `dvr_set_on_signal()`, and invoke the existing `on_change` callback after the direct update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Live Auto Record changes miss the signal 🐞 Bug ≡ Correctness
Description
dvr_set_on_signal() only changes the boolean, while drone_detect_timer() starts or stops
recording exclusively when its cached detection state transitions. Enabling the option after a link
is already present does not start recording, and disabling it during an automatic recording prevents
the later signal-loss transition from stopping that recording.
Code

src/main.cpp[R848-852]

+    // Live toggle for --dvr-on-signal, so the GS menu can flip it without a
+    // restart (dvr_on_signal_enabled itself is read by menu.c's
+    // drone_detect_timer on every signal-acquired/lost transition).
+    void dvr_set_on_signal(int enabled) { dvr_on_signal_enabled = (bool)enabled; }
+    int  dvr_get_on_signal(void)        { return (int)dvr_on_signal_enabled; }
Evidence
The setter only assigns the master flag, and the timer invokes recording controls only inside `now
!= detected and while the flag is true. The actual DVR state changes only through dvr_start_all()`
and dvr_stop_all(), so changing the flag without a subsequent qualifying transition leaves the old
recording state intact.

src/main.cpp[738-757]
src/main.cpp[848-852]
src/menu.c[62-87]
src/gsmenu/colmenu_pages.c[110-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Changing Auto Record while the signal state is stable does not reconcile the active recording with that state.

## Fix Focus Areas
- src/main.cpp[848-852]
- src/menu.c[62-87]
- src/gsmenu/colmenu_pages.c[110-113]

## Recommended Fix
Expose the current detection state and reconcile it inside the live setter: start when enabling with a currently detected link, and stop an auto-started recording when disabling. Track whether this feature started the recording so disabling it does not inadvertently stop an independently requested recording.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. One idle client freezes temperature updates 🐞 Bug ☼ Reliability
Description
listenLoop() performs an unbounded blocking read() on each accepted socket, while
VtxTempSensor destruction closes only the listening socket before waiting for the worker. Because
the service is enabled unconditionally and binds to every IPv4 interface, any reachable client that
connects without sending data can block subsequent accepts and temperature updates, and leave
worker.join() unable to complete during shutdown.
Code

src/os_mon.cpp[R455-456]

+            char buf[64] = {0};
+            ssize_t n = read(fd, buf, sizeof(buf) - 1);
Evidence
The listener accepts one client and immediately performs a blocking read() before returning to
accept(). Closing server_fd may interrupt a blocked accept(), but it cannot interrupt read()
on the separate accepted descriptor, which is local to the worker and is closed only after that read
returns; because the object is enabled unconditionally, binds INADDR_ANY, and joins the worker
after closing only server_fd, an idle accepted connection can block both further telemetry
processing and destruction.

src/os_mon.cpp[406-418]
src/os_mon.cpp[421-429]
src/os_mon.cpp[447-465]
src/main.cpp[1781-1783]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An accepted VTX temperature client that sends no bytes can indefinitely block the sole listener in `read()`, stopping subsequent temperature telemetry and preventing destruction from completing because only the listening socket is closed before `worker.join()`.

## Fix Focus Areas
- src/os_mon.cpp[406-418]
- src/os_mon.cpp[421-429]
- src/os_mon.cpp[447-465]

## Recommended Fix
Bound all client-read waits by making accepted sockets nonblocking, assigning a short receive timeout, or using `poll`/`select` with a bounded timeout that regularly checks `running`. Track the active accepted descriptor and shut it down during destruction so both `accept()` and client-read paths are guaranteed to unblock before `worker.join()`; return to `accept()` after a timeout or malformed input, and restrict the bind address or otherwise limit access to the intended tunnel where possible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (1)
4. Peers can forge air-unit temperature 🐞 Bug ⛨ Security
Description
VtxTempSensor binds TCP port 15552 to INADDR_ANY and accepts the first payload from every
connecting peer without checking its interface, source address, or identity. Because
addVtxTemp(15552) is unconditional, any host able to reach the ground station can overwrite the
published vtx.temperature fact with arbitrary telemetry.
Code

src/os_mon.cpp[R406-410]

+        sockaddr_in addr{};
+        addr.sin_family = AF_INET;
+        addr.sin_addr.s_addr = INADDR_ANY;
+        addr.sin_port = htons(static_cast<uint16_t>(port));
+        if (bind(server_fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
Evidence
The listener is created regardless of sensor configuration, binds all interfaces, starts a worker,
and stores data from any accepted connection directly into the value later published to OSD facts.

src/main.cpp[1781-1783]
src/os_mon.cpp[396-419]
src/os_mon.cpp[447-465]
src/os_mon.cpp[515-519]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
The new VTX temperature listener accepts unauthenticated TCP connections on every network interface. Reachable peers can submit arbitrary data that becomes the published VTX temperature.

Fix Focus Areas
- src/os_mon.cpp[396-419]
- src/os_mon.cpp[447-465]
- src/main.cpp[1781-1783]

Recommended Fix
Bind the listener only to the configured tunnel endpoint, or enforce an allowlisted tunnel source address before accepting a payload. Validate the complete payload and reject connections from all other interfaces or peers; make listener activation explicitly configurable if the tunnel endpoint is not always present.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Channel results can reflect the old setup 🐞 Bug ≡ Correctness
Description
apply_channel() launches colmenu_exec() and immediately calls colmenu_rescan() without waiting
for the worker running the channel-change command to complete. Because rebuilding the dynamic page
independently starts the prefetched chanscan_results read, it can collect and display measurements
from the previous channel and receiver configuration while the setter is still active, particularly
when the change restarts WFB-NG.
Code

src/gsmenu/colmenu_pages.c[R420-423]

+static void apply_channel(void * ctx)
+{
+    colmenu_exec((const char *)ctx);
+    colmenu_rescan();
Evidence
The call site schedules the command and rescan sequentially, but colmenu_exec() returns
immediately after creating a detached worker, while colmenu_rescan() merely defers an independent
dynamic-page rebuild with no dependency on command completion. That rebuild starts the prefetched
chanscan_results read, proving that scanning can overlap the channel-setting process rather than
waiting for the new configuration to become active.

src/gsmenu/colmenu_pages.c[420-428]
src/gsmenu/colmenu.c[257-281]
src/gsmenu/colmenu.c[318-364]
src/gsmenu/colmenu.c[635-688]
src/gsmenu/colmenu.c[1207-1209]
src/gsmenu/colmenu_pages.c[420-427]
src/gsmenu/colmenu.c[273-281]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Selecting a scanned channel queues a new scan immediately after starting the asynchronous channel-change command, so the scan can overlap the setter and display measurements collected before the new channel and receiver configuration is active.

## Fix Focus Areas
- src/gsmenu/colmenu_pages.c[420-424]
- src/gsmenu/colmenu.c[257-364]
- src/gsmenu/colmenu.c[635-688]

## Recommended Fix
Add or expose an asynchronous command execution API that accepts a completion callback, and invoke `colmenu_rescan()` from that callback only when the channel-set command succeeds. If the command fails, preserve the current scan results, surface the existing command error, and do not schedule a new scan.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Split readings show wrong temperatures 🐞 Bug ≡ Correctness
Description
listenLoop() treats the first successful TCP read() as the complete message, parses it
immediately, and closes the connection. TCP does not preserve write boundaries, so a fragmented
value such as 55.2 arriving first as 5 can be stored and published as 5°C while the remaining
bytes are discarded.
Code

src/os_mon.cpp[R456-459]

+            ssize_t n = read(fd, buf, sizeof(buf) - 1);
+            if (n > 0) {
+                try {
+                    latest.store(std::stod(std::string(buf, static_cast<std::size_t>(n))));
Evidence
The documented sender writes an ASCII value and closes, but the receiver performs exactly one read
and immediately calls std::stod on however many bytes happened to arrive. It then closes the
socket without reading the remainder and publishes the stored value on every sensor cycle.

src/os_mon.cpp[386-394]
src/os_mon.cpp[432-440]
src/os_mon.cpp[447-465]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The TCP listener assumes one read returns the sender's complete temperature value, allowing fragmented input to be accepted as a different valid number.

## Fix Focus Areas
- src/os_mon.cpp[447-465]

## Recommended Fix
Define a message terminator or use connection close as framing, then accumulate bounded input until that terminator or EOF before parsing. Require the parser to consume the entire trimmed message and reject non-finite or out-of-range values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Missing telemetry appears as zero degrees 🐞 Bug ≡ Correctness
Description
VtxTempSensor::run() publishes latest on every polling cycle even though that atomic starts at
0.0 and has no received or freshness state. Before the first packet, and indefinitely after the
sender stops or sends invalid data, consumers receive an invented zero or stale temperature as
current telemetry.
Code

src/os_mon.cpp[439]

+        osd_publish_double_fact("vtx.temperature", tags, 1, latest.load());
Evidence
The publishing path checks only whether socket setup succeeded, not whether telemetry was received.
latest is initialized to zero, invalid input leaves it unchanged, and OsSensors::run()
republishes every registered sensor repeatedly.

src/os_mon.cpp[432-440]
src/os_mon.cpp[457-473]
src/os_mon.cpp[515-519]
src/main.cpp[1081-1089]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The VTX sensor publishes a default or indefinitely stale value when no current temperature has been received.

## Fix Focus Areas
- src/os_mon.cpp[432-440]
- src/os_mon.cpp[457-473]

## Recommended Fix
Track whether a valid reading has been received and its timestamp. Do not publish before the first valid message, and expire or clear the fact once the feed has not refreshed it within the expected reporting interval.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (1)
8. Other 720p100 displays can lose video 🐞 Bug ≡ Correctness
Description
modeset_output_create() substitutes a timing tuned for one Skyzone unit whenever the requested
dimensions and refresh are 1280x720@100, without identifying the display or checking whether the
selected connector advertises or supports that timing. Because modeset_prepare() applies this to
generic connected candidates before advertised-mode selection, another display can reject the atomic
test commit or fail to lock video, reaching the application's fatal startup path rather than
retrying with an advertised preferred mode.
Code

src/drm.c[R587-590]

+	if (mode_width == 1280 && mode_height == 720 && mode_vrefresh == 100) {
+		/* Custom timing tuned by hand for this Skyzone O4X Pro unit's HDMI-in lock:
+		   swept horizontal/vertical front porch and back porch against the
+		   connector's stock 720p60 shape until crop on all four edges cleared. */
Evidence
The custom branch runs after only the connection and nonempty-mode checks, before the loop that
searches the connector's advertised modes, and directly creates a mode blob that bypasses normal
advertised-mode selection. Connector iteration is generic and accepts the first constructed output
before atomic validation, so the later test-only commit can reject the custom modeline; that output
is then aborted without retrying an advertised mode, and the caller treats the resulting modeset
failure as fatal.

src/drm.c[575-617]
src/drm.c[620-660]
src/drm.c[761-792]
src/drm.c[880-904]
src/main.cpp[278-279]
src/drm.c[587-617]
src/drm.c[620-662]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A timing tuned for one Skyzone display replaces the normal mode selection for every connector requested at 1280x720@100. Displays other than the intended target may reject the mode or fail to lock video, causing startup modesetting to fail instead of using a connector-advertised mode.

## Fix Focus Areas
- src/drm.c[565-618]
- src/drm.c[620-660]
- src/drm.c[761-792]

## Recommended Fix
Use the connector-advertised 1280x720@100 mode by default. Gate the custom timing behind an explicit target-display or platform configuration option, or reliable connector identification; for all other connectors, preserve the existing advertised-mode selection path. If the custom timing's atomic test fails, retry with an advertised mode, preferring the matching 1280x720@100 mode when available and otherwise falling back to the connector's advertised preferred mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This is a dense, cross-cutting PR spanning menu behavior, recording state, networking, sensors, DRM timing, display pipelines, and build tooling, with many independent logic paths and substantial potential for subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

static const colmenu_page_t sys_display_page = { "Display", "gs", "system", sys_display_items, 5 };
static const colmenu_item_t sys_dvr_items[] = {
{ .kind=COLMENU_SWITCH, .label="Enabled", .param="rec_enabled", .on_change=on_rec_enabled },
{ .kind=COLMENU_SWITCH, .label="Auto Record", .param="dvr_on_signal", .on_change=on_dvr_on_signal },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. The auto record control does nothing 🐞 Bug ≡ Correctness

colmenu_get() and do_set() do not special-case the runtime-only dvr_on_signal parameter, so
the row falls through to gsmenu.sh, whose integrator template has no matching get or set command.
Opening the page consequently reads the switch as off, while changing it reaches an unknown shell
command whose failure occurs before the deferred on_dvr_on_signal() callback can update
dvr_on_signal_enabled.
Agent Prompt
## Issue description
The `dvr_on_signal` menu parameter represents application runtime state, but it is routed through `gsmenu.sh`, which has no handler for it. As a result, the Auto Record switch cannot display or update `dvr_on_signal_enabled`.

## Fix Focus Areas
- src/gsmenu/colmenu_pages.c[500-512]
- src/gsmenu/colmenu_pages.c[110-113]
- src/gsmenu/colmenu.c[81-130]
- src/gsmenu/colmenu.c[399-421]
- src/main.cpp[848-852]

## Recommended Fix
Handle `dvr_on_signal` directly as application runtime state, following the existing restream controls. Expose `dvr_get_on_signal()` through the column-menu C interface and return its `0`/`1` value from `colmenu_get()` for this parameter; in `do_set()`, bypass `gsmenu.sh`, apply the change with `dvr_set_on_signal()`, and invoke the existing `on_change` callback after the direct update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/main.cpp
Comment on lines +848 to +852
// Live toggle for --dvr-on-signal, so the GS menu can flip it without a
// restart (dvr_on_signal_enabled itself is read by menu.c's
// drone_detect_timer on every signal-acquired/lost transition).
void dvr_set_on_signal(int enabled) { dvr_on_signal_enabled = (bool)enabled; }
int dvr_get_on_signal(void) { return (int)dvr_on_signal_enabled; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Live auto record changes miss the signal 🐞 Bug ≡ Correctness

dvr_set_on_signal() only changes the boolean, while drone_detect_timer() starts or stops
recording exclusively when its cached detection state transitions. Enabling the option after a link
is already present does not start recording, and disabling it during an automatic recording prevents
the later signal-loss transition from stopping that recording.
Agent Prompt
## Issue description
Changing Auto Record while the signal state is stable does not reconcile the active recording with that state.

## Fix Focus Areas
- src/main.cpp[848-852]
- src/menu.c[62-87]
- src/gsmenu/colmenu_pages.c[110-113]

## Recommended Fix
Expose the current detection state and reconcile it inside the live setter: start when enabling with a currently detected link, and stop an auto-started recording when disabling. Track whether this feature started the recording so disabling it does not inadvertently stop an independently requested recording.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +420 to +423
static void apply_channel(void * ctx)
{
colmenu_exec((const char *)ctx);
colmenu_rescan();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Channel results can reflect the old setup 🐞 Bug ≡ Correctness

apply_channel() launches colmenu_exec() and immediately calls colmenu_rescan() without waiting
for the worker running the channel-change command to complete. Because rebuilding the dynamic page
independently starts the prefetched chanscan_results read, it can collect and display measurements
from the previous channel and receiver configuration while the setter is still active, particularly
when the change restarts WFB-NG.
Agent Prompt
## Issue description
Selecting a scanned channel queues a new scan immediately after starting the asynchronous channel-change command, so the scan can overlap the setter and display measurements collected before the new channel and receiver configuration is active.

## Fix Focus Areas
- src/gsmenu/colmenu_pages.c[420-424]
- src/gsmenu/colmenu.c[257-364]
- src/gsmenu/colmenu.c[635-688]

## Recommended Fix
Add or expose an asynchronous command execution API that accepts a completion callback, and invoke `colmenu_rescan()` from that callback only when the channel-set command succeeds. If the command fails, preserve the current scan results, surface the existing command error, and do not schedule a new scan.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/os_mon.cpp
Comment on lines +455 to +456
char buf[64] = {0};
ssize_t n = read(fd, buf, sizeof(buf) - 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. One idle client freezes temperature updates 🐞 Bug ☼ Reliability

listenLoop() performs an unbounded blocking read() on each accepted socket, while
VtxTempSensor destruction closes only the listening socket before waiting for the worker. Because
the service is enabled unconditionally and binds to every IPv4 interface, any reachable client that
connects without sending data can block subsequent accepts and temperature updates, and leave
worker.join() unable to complete during shutdown.
Agent Prompt
## Issue description
An accepted VTX temperature client that sends no bytes can indefinitely block the sole listener in `read()`, stopping subsequent temperature telemetry and preventing destruction from completing because only the listening socket is closed before `worker.join()`.

## Fix Focus Areas
- src/os_mon.cpp[406-418]
- src/os_mon.cpp[421-429]
- src/os_mon.cpp[447-465]

## Recommended Fix
Bound all client-read waits by making accepted sockets nonblocking, assigning a short receive timeout, or using `poll`/`select` with a bounded timeout that regularly checks `running`. Track the active accepted descriptor and shut it down during destruction so both `accept()` and client-read paths are guaranteed to unblock before `worker.join()`; return to `accept()` after a timeout or malformed input, and restrict the bind address or otherwise limit access to the intended tunnel where possible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/os_mon.cpp
Comment on lines +456 to +459
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
try {
latest.store(std::stod(std::string(buf, static_cast<std::size_t>(n))));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Split readings show wrong temperatures 🐞 Bug ≡ Correctness

listenLoop() treats the first successful TCP read() as the complete message, parses it
immediately, and closes the connection. TCP does not preserve write boundaries, so a fragmented
value such as 55.2 arriving first as 5 can be stored and published as 5°C while the remaining
bytes are discarded.
Agent Prompt
## Issue description
The TCP listener assumes one read returns the sender's complete temperature value, allowing fragmented input to be accepted as a different valid number.

## Fix Focus Areas
- src/os_mon.cpp[447-465]

## Recommended Fix
Define a message terminator or use connection close as framing, then accumulate bounded input until that terminator or EOF before parsing. Require the parser to consume the entire trimmed message and reject non-finite or out-of-range values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/os_mon.cpp
osd_tag tags[1];
strcpy(tags[0].key, "name");
strcpy(tags[0].val, "vtx");
osd_publish_double_fact("vtx.temperature", tags, 1, latest.load());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

7. Missing telemetry appears as zero degrees 🐞 Bug ≡ Correctness

VtxTempSensor::run() publishes latest on every polling cycle even though that atomic starts at
0.0 and has no received or freshness state. Before the first packet, and indefinitely after the
sender stops or sends invalid data, consumers receive an invented zero or stale temperature as
current telemetry.
Agent Prompt
## Issue description
The VTX sensor publishes a default or indefinitely stale value when no current temperature has been received.

## Fix Focus Areas
- src/os_mon.cpp[432-440]
- src/os_mon.cpp[457-473]

## Recommended Fix
Track whether a valid reading has been received and its timestamp. Do not publish before the first valid message, and expire or clear the fact once the feed has not refreshed it within the expected reporting interval.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/drm.c
Comment on lines +587 to +590
if (mode_width == 1280 && mode_height == 720 && mode_vrefresh == 100) {
/* Custom timing tuned by hand for this Skyzone O4X Pro unit's HDMI-in lock:
swept horizontal/vertical front porch and back porch against the
connector's stock 720p60 shape until crop on all four edges cleared. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

8. Other 720p100 displays can lose video 🐞 Bug ≡ Correctness

modeset_output_create() substitutes a timing tuned for one Skyzone unit whenever the requested
dimensions and refresh are 1280x720@100, without identifying the display or checking whether the
selected connector advertises or supports that timing. Because modeset_prepare() applies this to
generic connected candidates before advertised-mode selection, another display can reject the atomic
test commit or fail to lock video, reaching the application's fatal startup path rather than
retrying with an advertised preferred mode.
Agent Prompt
## Issue description
A timing tuned for one Skyzone display replaces the normal mode selection for every connector requested at 1280x720@100. Displays other than the intended target may reject the mode or fail to lock video, causing startup modesetting to fail instead of using a connector-advertised mode.

## Fix Focus Areas
- src/drm.c[565-618]
- src/drm.c[620-660]
- src/drm.c[761-792]

## Recommended Fix
Use the connector-advertised 1280x720@100 mode by default. Gate the custom timing behind an explicit target-display or platform configuration option, or reliable connector identification; for all other connectors, preserve the existing advertised-mode selection path. If the custom timing's atomic test fails, retry with an advertised mode, preferring the matching 1280x720@100 mode when available and otherwise falling back to the connector's advertised preferred mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/os_mon.cpp
Comment on lines +406 to +410
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(static_cast<uint16_t>(port));
if (bind(server_fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Peers can forge air-unit temperature 🐞 Bug ⛨ Security

VtxTempSensor binds TCP port 15552 to INADDR_ANY and accepts the first payload from every
connecting peer without checking its interface, source address, or identity. Because
addVtxTemp(15552) is unconditional, any host able to reach the ground station can overwrite the
published vtx.temperature fact with arbitrary telemetry.
Agent Prompt
Issue description
The new VTX temperature listener accepts unauthenticated TCP connections on every network interface. Reachable peers can submit arbitrary data that becomes the published VTX temperature.

Fix Focus Areas
- src/os_mon.cpp[396-419]
- src/os_mon.cpp[447-465]
- src/main.cpp[1781-1783]

Recommended Fix
Bind the listener only to the configured tunnel endpoint, or enforce an allowlisted tunnel source address before accepting a payload. Validate the complete payload and reject connections from all other interfaces or peers; make listener activation explicitly configurable if the tunnel endpoint is not always present.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant