Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions debian/manpage.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pixelpilot-rk - OpenIPC video display client for wfb-ng

--mavlink-dvr-on-arm - Start recording when armed

--dvr-on-signal - Start recording when the air unit's signal is
acquired, stop when it's lost

--codec <codec> - Video codec, should be the same as on VTX (Default: h265 <h264|h265>)

--log-level <level> - Log verbosity level, debug|info|warn|error (Default: info)
Expand Down Expand Up @@ -123,6 +126,9 @@ a complete description, see the **info**(1) files.
**--mavlink-dvr-on-arm**
: Start recording when armed

**--dvr-on-signal**
: Start recording when the air unit's signal is acquired, stop when it's lost

**--codec \<codec\>**
: Video codec, should be the same as on VTX (Default: h265 <h264|h265>)

Expand Down
8 changes: 8 additions & 0 deletions gsmenu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,14 @@ case "$@" in
"get gs wfbng adaptivelink")
echo 0
;;
"get gs wfbng chanscan_results")
# integrator: hop your secondary RX NIC across every supported channel,
# sampling per-channel RF activity (e.g. rx_packets delta over a short
# dwell, since `iw survey dump` isn't implemented by every driver), and
# print one "channel:count" line per channel. See PixelPilot's GS menu
# "WFB-NG -> Channel Scan" page, which normalizes and bar-graphs this.
printf ""
;;

"set gs wfbng gs_channel"*) : ;;
"set gs wfbng bandwidth"*) : ;;
Expand Down
40 changes: 40 additions & 0 deletions src/drm.c
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,39 @@ struct modeset_output *modeset_output_create(int fd, drmModeRes *res, drmModeCon
goto out_error;
}

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. */
Comment on lines +587 to +590

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

drmModeModeInfo custom;
memset(&custom, 0, sizeof(custom));
custom.clock = 150705;
custom.hdisplay = 1280;
custom.hsync_start = 1380;
custom.hsync_end = 1420;
custom.htotal = 1970;
custom.hskew = 0;
custom.vdisplay = 720;
custom.vsync_start = 740;
custom.vsync_end = 745;
custom.vtotal = 765;
custom.vscan = 0;
custom.vrefresh = 100;
custom.flags = 0;
custom.type = 0x40; /* DRM_MODE_TYPE_USERDEF */
snprintf(custom.name, sizeof(custom.name), "custom1280x720-100");
memcpy(&out->mode, &custom, sizeof(out->mode));
{
uint32_t requested_mhz = mode_refresh_mhz(&custom);
fprintf(stdout, "Using custom 1280x720 modeline: clock=%ukHz htotal=%u vtotal=%u "
"-> requested refresh %u.%03u Hz (this is what we asked the kernel "
"for; it does not confirm the display actually locks at this rate)\n",
custom.clock, custom.htotal, custom.vtotal,
requested_mhz / 1000, requested_mhz % 1000);
}
goto have_mode;
}

int fc = 0;
int preferred_fc = -1;
if (mode_width>0 && mode_height>0 && mode_vrefresh>0) {
Expand Down Expand Up @@ -625,6 +658,7 @@ struct modeset_output *modeset_output_create(int fd, drmModeRes *res, drmModeCon
printf( "Using screen mode %dx%d@%d (%u.%03u Hz)\n",conn->modes[fc].hdisplay, conn->modes[fc].vdisplay , conn->modes[fc].vrefresh, used_mhz / 1000, used_mhz % 1000 );
}
memcpy(&out->mode, &conn->modes[fc], sizeof(out->mode));
have_mode:
if (drmModeCreatePropertyBlob(fd, &out->mode, sizeof(out->mode), &out->mode_blob_id) != 0) {
fprintf(stderr, "couldn't create a blob property\n");
goto out_error;
Expand Down Expand Up @@ -898,6 +932,12 @@ int modeset_atomic_prepare_commit(int fd, struct modeset_output *out, drmModeAto
{
if (set_drm_object_property(req, &out->connector, "CRTC_ID", out->crtc.id) < 0)
return -1;
if (out->mode.hdisplay == 1280 && out->mode.vdisplay == 720 && out->mode.vrefresh == 100) {
/* Force limited-range output for our custom 720p100 timing: colors
looked oversaturated under the driver's default (full range)
output on this connector's non-standard userdef mode. */
set_drm_object_property(req, &out->connector, "quant_range", 1);
}
if (set_drm_object_property(req, &out->crtc, "MODE_ID", out->mode_blob_id) < 0)
return -1;
if (set_drm_object_property(req, &out->crtc, "ACTIVE", 1) < 0)
Expand Down
3 changes: 3 additions & 0 deletions src/dvr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace fs = std::filesystem;
// Master record flag; defined here, declared extern in dvr.h.
int dvr_enabled = 0;

// --dvr-on-signal master switch; defined here, declared extern in dvr.h.
bool dvr_on_signal_enabled = false;

const int SEQUENCE_PADDING = 4; // zero-padding width for --dvr-sequenced-files

std::string dvr_next_base_path(const char* filename_template, bool with_sequence) {
Expand Down
6 changes: 6 additions & 0 deletions src/dvr.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ enum DvrMode { DVR_MODE_RAW = 0, DVR_MODE_REENCODE = 1, DVR_MODE_BOTH = 2 };
// mirrored to the receiver's recorders.
extern int dvr_enabled;

// --dvr-on-signal: auto-start recording when the GS starts receiving data
// from the air unit (tunnel/RX traffic) and auto-stop when it drops out.
// Watched by menu.c's drone_detect_timer, the same signal that greys out the
// drone pages when the VTX isn't seen.
extern bool dvr_on_signal_enabled;

// Resolve the next DVR recording's base path from a filename template WITHOUT
// the extension: <dir>/[NNNN_]<strftime(template)> minus a trailing ".mp4".
// Honours --dvr-sequenced-files. Returns "" if the directory does not exist.
Expand Down
74 changes: 72 additions & 2 deletions src/gsmenu/colmenu_pages.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ void dvr_set_mode(int mode);
void dvr_set_max_size(int mb);
void dvr_start_all(void);
void dvr_stop_all(void);
void dvr_set_on_signal(int enabled);
#endif

static void on_live_colortrans(const char * value)
Expand Down Expand Up @@ -106,6 +107,11 @@ static void on_rec_enabled(const char * value) /* start/stop recording now */
if(on) MENU_LIVE(dvr_start_all(), "dvr_start_all()%s", "");
else MENU_LIVE(dvr_stop_all(), "dvr_stop_all()%s", "");
}
static void on_dvr_on_signal(const char * value) /* auto rec while the air unit's signal is present */
{
int on = (value && strcmp(value, "on") == 0) ? 1 : 0;
MENU_LIVE(dvr_set_on_signal(on), "dvr_set_on_signal(%d)", on);
}
static void on_dvr_osd(const char * value) /* burn OSD into the re-encode */
{
int on = (value && strcmp(value, "on") == 0) ? 1 : 0;
Expand Down Expand Up @@ -399,15 +405,78 @@ static const colmenu_page_t gs_actions_page = { .title="Actions", .type="gs",

/* ── GS ────────────────────────────────────────────────────────────────────── */

/* Channel scan (HDZero-style bar graph). gsmenu.sh's chanscan_run() hops the GS's
* secondary wfb-ng RX NIC across every channel the adapter supports and reports
* back raw per-channel frame counts as "channel:count" lines (see its comment for
* why frame-count rather than `iw survey dump`, and why the primary NIC/video
* feed isn't touched). Normalizing against the scan's own max here, rather than
* in the shell, keeps the bar scale correct regardless of how many channels or
* how much traffic a given scan sees. */
static void chanscan_rescan(void * ctx) { (void)ctx; colmenu_rescan(); }
/* Selecting a scanned channel applies it exactly like the "Channel" dropdown
* (same "set gs wfbng gs_channel" command) — which already both reconfigures
* the GS locally and, when a drone is linked, SSHes the air unit to switch its
* channel too and restarts wifibroadcast on both ends. */
static void apply_channel(void * ctx)
{
colmenu_exec((const char *)ctx);
colmenu_rescan();
Comment on lines +420 to +423

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

}
static void build_chanscan(colmenu_emit_t * e)
{
char * out = colmenu_get("gs", "wfbng", "chanscan_results", NULL);
int chans[64], counts[64], n = 0, max_count = 1;
if(out) {
char * save = NULL;
for(char * line = strtok_r(out, "\n", &save); line && n < 64; line = strtok_r(NULL, "\n", &save)) {
int ch, count;
if(sscanf(line, "%d:%d", &ch, &count) != 2) continue;
chans[n] = ch; counts[n] = count; n++;
if(count > max_count) max_count = count;
}
}
free(out);
/* Noisiest first — the point of the scan is to surface congestion, and the
* OSD column only shows a handful of rows at a time without scrolling. */
for(int i = 1; i < n; i++) {
int kc = counts[i], kh = chans[i], j = i - 1;
while(j >= 0 && counts[j] < kc) { counts[j+1] = counts[j]; chans[j+1] = chans[j]; j--; }
counts[j+1] = kc; chans[j+1] = kh;
}
for(int i = 0; i < n; i++) {
int pct = counts[i] * 100 / max_count;
/* Montserrat's compiled cmap only carries a handful of non-ASCII glyphs
* (the LV_SYMBOL_* icon set plus the bullet), no Unicode block elements —
* "\xE2\x80\xA2" (U+2022 bullet) is the closest thing to a solid/filled
* cell that's actually in the font, verified against
* lv_font_montserrat_20.c's unicode_list rather than assumed. */
char bar[64];
int filled = pct / 10, pos = 0;
for(int j = 0; j < filled; j++) { memcpy(bar + pos, "\xE2\x80\xA2", 3); pos += 3; }
for(int j = filled; j < 10; j++) bar[pos++] = '.';
bar[pos] = '\0';
char label[80];
snprintf(label, sizeof(label), "Ch %-3d %s %d", chans[i], bar, counts[i]);
char cmd[64];
snprintf(cmd, sizeof(cmd), "gsmenu.sh set gs wfbng gs_channel %d", chans[i]);
colmenu_emit_action(e, LV_SYMBOL_WIFI, label, apply_channel, strdup(cmd));
}
if(n == 0) colmenu_emit_label(e, "No scan results yet");
colmenu_emit_action(e, LV_SYMBOL_REFRESH, "Rescan", chanscan_rescan, NULL);
}
static const char * const chanscan_prefetch[] = { "chanscan_results", NULL };
static const colmenu_page_t gs_chanscan_page = { .title="Channel Scan", .type="gs", .page="wfbng", .build=build_chanscan, .prefetch=chanscan_prefetch };

static const colmenu_item_t gs_wfbng_items[] = {
{ .kind=COLMENU_LABEL, .label="WFB-NG" },
{ .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Channel", .param="gs_channel" },
{ .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Bandwidth", .param="bandwidth" },
{ .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_SETTINGS, .label="TXPower (%)", .param="txpower" },
{ .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_WIFI, .label="Channel Scan", .sub=&gs_chanscan_page },
{ .kind=COLMENU_LABEL, .label="Adaptive Link" },
{ .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="Enabled", .param="adaptivelink" },
};
static const colmenu_page_t gs_wfbng_page = { "WFB-NG", "gs", "wfbng", gs_wfbng_items, 6 };
static const colmenu_page_t gs_wfbng_page = { "WFB-NG", "gs", "wfbng", gs_wfbng_items, 7 };

/* System → Receiver / Display / DVR */
static const colmenu_item_t sys_receiver_items[] = {
Expand All @@ -431,6 +500,7 @@ static const colmenu_item_t sys_display_items[] = {
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

{ .kind=COLMENU_DROPDOWN, .label="Mode", .param="dvr_mode", .on_change=on_dvr_mode },
{ .kind=COLMENU_SLIDER, .label="Max file size (MB)", .param="dvr_max_size", .on_change=on_dvr_max_size, .display_scale=100 },
{ .kind=COLMENU_DROPDOWN, .label="Codec", .param="dvr_reenc_codec", .on_change=on_dvr_reenc_codec },
Expand All @@ -439,7 +509,7 @@ static const colmenu_item_t sys_dvr_items[] = {
{ .kind=COLMENU_DROPDOWN, .label="Bitrate (kbps)", .param="dvr_reenc_bitrate", .on_change=on_dvr_reenc_bitrate },
{ .kind=COLMENU_SWITCH, .label="Record OSD in DVR", .param="dvr_osd", .on_change=on_dvr_osd },
};
static const colmenu_page_t sys_dvr_page = { "DVR", "gs", "system", sys_dvr_items, 8 };
static const colmenu_page_t sys_dvr_page = { "DVR", "gs", "system", sys_dvr_items, 9 };

/* Restream to phone/laptop over the local WiFi. Both rows are served by the app
* itself, not gsmenu.sh — colmenu.c intercepts the "restream_" params on read
Expand Down
18 changes: 15 additions & 3 deletions src/gstrtpreceiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,11 @@ namespace {
static std::string create_restream_branch() {
std::stringstream ss;
ss << " rtp_tee. ! valve name=restream_valve drop=true"
" ! queue leaky=downstream max-size-buffers=0 max-size-bytes=0 max-size-time=1000000000 silent=true"
// Keep the restream branch shallow: leaky=downstream drops the
// oldest buffered data first, but max-size-time was 1s, so a
// stalled/slow restream peer could sit a full second of stale
// video behind live before drops even start. 150ms bounds that.
" ! queue leaky=downstream max-size-buffers=0 max-size-bytes=0 max-size-time=150000000 silent=true"
" ! udpsink name=restream_sink host=0.0.0.0 port=5600 sync=false async=false qos=false";
return ss.str();
}
Expand Down Expand Up @@ -1320,7 +1324,12 @@ std::string GstRtpReceiver::construct_gstreamer_pipeline()
}
ss<<pipeline::create_parse_for_codec(m_video_codec);
ss<<pipeline::create_out_caps(m_video_codec);
ss<<"appsink drop=true name=out_appsink";
// max-buffers bounds the appsink's internal queue so drop=true actually
// does something (GStreamer default max-buffers=0 is unlimited, which
// makes drop=true a no-op) -- without this a decode/pull stall lets
// frames pile up unbounded and the picture catches up in fast-forward
// once the stall clears, instead of just skipping ahead to live.
ss<<"appsink drop=true max-buffers=1 name=out_appsink";
if (play_audio) {
ss<<pipeline::create_audio_branch(m_audio_pt, m_audio_device);
}
Expand Down Expand Up @@ -1555,7 +1564,10 @@ std::string GstRtpReceiver::construct_file_playback_pipeline(const char * file_p
ss<<"filesrc location="<<file_path<<" ! qtdemux ! ";
ss<<pipeline::create_parse_for_codec(file_codec);
ss << pipeline::create_out_caps(file_codec);
ss << " appsink drop=true name=out_appsink";
// Same reasoning as the live pipeline's out_appsink: bound the queue so
// drop=true isn't a no-op and DVR playback tracks live instead of
// free-running ahead of the display.
ss << " appsink drop=true max-buffers=1 name=out_appsink";
return ss.str();
}

Expand Down
20 changes: 19 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ void *__DISPLAY_THREAD__(void *param)
uint64_t decode_and_handover_display_ms=get_time_ms()-decoding_pts;
osd_publish_uint_fact("video.decode_and_handover_ms", NULL, 0, decode_and_handover_display_ms);
}
end:
end:
spdlog::info("Display thread done.");
return nullptr;
}
Expand Down Expand Up @@ -844,6 +844,12 @@ extern "C" {
void dvr_reenc_set_mode(int enabled) {
dvr_set_mode(enabled ? DVR_MODE_REENCODE : DVR_MODE_RAW);
}

// 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; }
Comment on lines +848 to +852

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

}

int decoder_stalled_count=0;
Expand Down Expand Up @@ -1236,6 +1242,9 @@ void printHelp() {
"\n"
" --mavlink-dvr-on-arm - Start recording when armed\n"
"\n"
" --dvr-on-signal - Start recording when the air unit's signal is\n"
" acquired, stop when it's lost\n"
"\n"
" --codec <codec> - Video codec, should be the same as on VTX (Default: h265 <h264|h265|auto>)\n"
"\n"
" --audio - Play Opus audio muxed into the RTP stream (same port, by payload type)\n"
Expand Down Expand Up @@ -1516,6 +1525,11 @@ int main(int argc, char **argv)
continue;
}

__OnArgument("--dvr-on-signal") {
dvr_on_signal_enabled = true;
continue;
}

__OnArgument("--osd") {
enable_osd = 1;
mavlink_thread = 1;
Expand Down Expand Up @@ -1764,6 +1778,10 @@ int main(int argc, char **argv)
std::cerr << "Configuration error: " << e.what() << std::endl;
}

// Independent of the os_sensors config above -- the VTX's SoC temp isn't
// local hardware, it arrives over its own small TCP feed on the tunnel.
os_sensors.addVtxTemp(15552);

spdlog::info("disable_vsync: {}", disable_vsync);

if (enable_osd == 0 ) {
Expand Down
7 changes: 7 additions & 0 deletions src/menu.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum RXMode RXMODE = WFB;

/* DVR backend (defined in main.cpp / dvr.cpp). */
extern int dvr_enabled;
extern bool dvr_on_signal_enabled; /* --dvr-on-signal master switch */
void dvr_start_all(void);
void dvr_stop_all(void);

Expand Down Expand Up @@ -77,6 +78,12 @@ static void drone_detect_timer(lv_timer_t * t)
detected = now;
setenv("GSMENU_VTX_DETECTED", detected ? "1" : "0", 1);
colmenu_set_drone_detected(detected);
#ifndef USE_SIMULATOR
if(dvr_on_signal_enabled) {
if(detected) dvr_start_all();
else dvr_stop_all();
}
#endif
}
}

Expand Down
Loading