From d3398389ec8bf476177f8ce53508e34163f83ebe Mon Sep 17 00:00:00 2001 From: kkbin505 <19462942@qq.com> Date: Tue, 28 Jul 2026 10:49:36 -0700 Subject: [PATCH] Make the ground-station uplink configurable and cut its airtime The adapter is half duplex, so every uplink frame is a receive blackout. The uplink was hardcoded to k=1/n=5 at MCS0 and Transmitter::sendPacket emits a block's fragments back-to-back, measuring ~102 frames/s in bursts of 10-12 inside 1-2 ms. A burst blinds the receiver long enough to drop CONSECUTIVE video fragments, the one loss pattern the downlink FEC cannot repair -- it sits pinned at its maximum setting and still fails, the decoder gets partial frames, and the unwritten part of the output buffer shows as colour blocks. This was latent until devourer 5f099ee ("drop DEVOURER_TX_HT_MCS gate") made the uplink actually reach the air: before it, fixed_rate stayed at MGN_1M, and CCK is illegal on the 5 GHz channels this link runs on, so the frames were never transmitted. The uplink was silently dead and the radio was effectively receive-only. Expose the uplink's airtime as settings rather than constants, because the right trade-off depends on the airframe: a short-range quad can spend uplink margin freely, a long-range wing cannot, and a ground station with a separate RC/telemetry radio wants no uplink here at all. Adaptive -> Uplink... offers enable/MCS/redundancy, stored in SharedPreferences and applied on the next link start, matching the existing LDPC/STBC lifecycle. Out-of-range values are rejected rather than clamped: transmitting at a rate the operator did not choose is worse than keeping the previous one. Defaults move one MCS step and leave redundancy alone. Measured at 5 GHz ch161/20 MHz on an RTL8812AU, in fragments/s the downlink FEC could not recover: mcs 0, n 5 9.78 - 15.82 (previous hardcoded values) mcs 1, n 5 2.18 <- new default mcs 3, n 2 0.12 uplink off 0.00 ~3 dB of uplink margin buys a 7x reduction. Redundancy is deliberately not the knob turned by default -- losing mavlink is worse than losing video. The gain is superlinear in airtime because shorter blackouts stop clipping runs of fragments, and the downlink FEC absorbs isolated losses. Spacing the five fragments 8 ms apart was tried and is worse: total lost fragments fall (15.8 -> 10.4) but artefacts become more frequent, as the same damage spreads over more downlink FEC blocks and one damaged block is one visible glitch. The lever is total airtime, not smoother airtime. Each measurement was validated as actually transmitting (TxFrame countPIncoming > 0 plus the expected fixed_rate in the devourer log) before its numbers were used; the uplink can die silently, which invalidates a run that otherwise looks clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015LuniKiNRWAcTvaRV8ZhH8 --- .../com/openipc/pixelpilot/VideoActivity.java | 81 +++++++++++++++++++ app/wfbngrtl8812/src/main/cpp/WfbngLink.cpp | 51 +++++++++++- app/wfbngrtl8812/src/main/cpp/WfbngLink.hpp | 19 +++++ .../com/openipc/wfbngrtl8812/WfbNgLink.java | 18 +++++ 4 files changed, 166 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java index e3c8da26..1bce10d4 100644 --- a/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java +++ b/app/src/main/java/com/openipc/pixelpilot/VideoActivity.java @@ -781,6 +781,86 @@ private void setupAdaptiveLinkSubMenu(PopupMenu popup) { showFecThresholdsDialog(); return true; }); + + // --- Uplink airtime --- + adaptiveMenu.add("Uplink...").setOnMenuItemClickListener(item -> { + showUplinkDialog(); + return true; + }); + } + + // Uplink airtime settings. The adapter is half duplex: every uplink frame is + // a receive blackout, and the video fragments that land in it are lost + // consecutively, which is what the downlink FEC cannot repair. Measured on an + // RTL8812AU at 5 GHz, in unrecovered fragments/s: 15.82 at MCS0 + n=5, 2.18 + // at MCS1 + n=5 (the default), 0.12 at MCS3 + n=2, 0.00 with the uplink off. + private void showUplinkDialog() { + SharedPreferences prefs = getSharedPreferences("general", MODE_PRIVATE); + boolean enabled = prefs.getBoolean("uplink_enabled", true); + int mcs = prefs.getInt("uplink_mcs", 1); + int fecN = prefs.getInt("uplink_fec_n", 5); + + android.widget.LinearLayout layout = new android.widget.LinearLayout(this); + layout.setOrientation(android.widget.LinearLayout.VERTICAL); + + android.widget.CheckBox enableBox = new android.widget.CheckBox(this); + enableBox.setText("Uplink enabled"); + enableBox.setChecked(enabled); + layout.addView(enableBox); + + android.widget.TextView note = new android.widget.TextView(this); + note.setText("Off = receive only: no adaptive bitrate and no mavlink uplink.\n" + + "Use it when RC and telemetry ride a separate radio."); + layout.addView(note); + + android.widget.TextView mcsLabel = new android.widget.TextView(this); + mcsLabel.setText("\nUplink MCS (0-7). Higher = shorter transmit, less range."); + layout.addView(mcsLabel); + android.widget.EditText mcsEdit = new android.widget.EditText(this); + mcsEdit.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); + mcsEdit.setText(String.valueOf(mcs)); + layout.addView(mcsEdit); + + android.widget.TextView fecLabel = new android.widget.TextView(this); + fecLabel.setText("\nUplink redundancy (1-8 copies per message)."); + layout.addView(fecLabel); + android.widget.EditText fecEdit = new android.widget.EditText(this); + fecEdit.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); + fecEdit.setText(String.valueOf(fecN)); + layout.addView(fecEdit); + + new android.app.AlertDialog.Builder(this) + .setTitle("Uplink") + .setView(layout) + .setPositiveButton("OK", (dialog, which) -> { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean("uplink_enabled", enableBox.isChecked()); + // Out-of-range entries are dropped, not clamped: silently + // transmitting at a rate the user did not ask for is worse + // than keeping the previous one. + try { + int v = Integer.parseInt(mcsEdit.getText().toString()); + if (v >= 0 && v <= 7) editor.putInt("uplink_mcs", v); + } catch (Exception ignored) { + } + try { + int v = Integer.parseInt(fecEdit.getText().toString()); + if (v >= 1 && v <= 8) editor.putInt("uplink_fec_n", v); + } catch (Exception ignored) { + } + editor.apply(); + setUplinkFromPrefs(); + Toast.makeText(this, "Uplink settings apply on reconnect", Toast.LENGTH_LONG).show(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void setUplinkFromPrefs() { + SharedPreferences prefs = getSharedPreferences("general", MODE_PRIVATE); + wfbLink.nativeSetUplinkEnabled(prefs.getBoolean("uplink_enabled", true)); + wfbLink.nativeSetUplinkMcs(prefs.getInt("uplink_mcs", 1)); + wfbLink.nativeSetUplinkFecN(prefs.getInt("uplink_fec_n", 5)); } // Show dialog to configure FEC thresholds for all levels @@ -858,6 +938,7 @@ void initDefaultOptions() { wfbLink.nativeSetUseStbc(stbcEnabled ? 1 : 0); setFecThresholdsFromPrefs(); + setUplinkFromPrefs(); } // Read FEC thresholds from prefs and call native method to apply diff --git a/app/wfbngrtl8812/src/main/cpp/WfbngLink.cpp b/app/wfbngrtl8812/src/main/cpp/WfbngLink.cpp index 217a9214..cd159a1f 100644 --- a/app/wfbngrtl8812/src/main/cpp/WfbngLink.cpp +++ b/app/wfbngrtl8812/src/main/cpp/WfbngLink.cpp @@ -207,19 +207,26 @@ int WfbngLink::run(JNIEnv *env, jobject context, jint wifiChannel, jint bw, jint .ChannelWidth = bandWidth, }); - if (!usb_tx_thread) { + // Uplink off: start neither the TX worker nor the adaptive-link thread, + // so the radio only ever receives. Costs adaptive bitrate and the + // mavlink uplink — the setting exists for ground stations that carry RC + // and telemetry on a separate radio, where uplink airtime here buys + // nothing and only steals receive time. + if (!uplink_enabled) { + __android_log_print(ANDROID_LOG_WARN, TAG, "uplink disabled: RX only, no adaptive link"); + } else if (!usb_tx_thread) { std::shared_ptr args = std::make_shared(); args->udp_port = 8001; args->link_id = link_id; args->keypair = keyPath; args->stbc = stbc_enabled; args->ldpc = ldpc_enabled; - args->mcs_index = 0; + args->mcs_index = uplink_mcs; args->vht_mode = false; args->short_gi = false; args->bandwidth = 20; args->k = 1; - args->n = 5; + args->n = uplink_fec_n; args->radio_port = wfb_tx_port; __android_log_print( @@ -582,6 +589,44 @@ extern "C" JNIEXPORT void JNICALL Java_com_openipc_wfbngrtl8812_WfbNgLink_native link->stbc_enabled = (use != 0); } +/* Uplink airtime controls. Applied when the link (re)starts, since TxArgs is + * built once per run() — same lifecycle as ldpc/stbc. */ +extern "C" JNIEXPORT void JNICALL Java_com_openipc_wfbngrtl8812_WfbNgLink_nativeSetUplinkEnabled(JNIEnv *env, + jclass clazz, + jlong wfbngLinkN, + jint enabled) { + WfbngLink *link = native(wfbngLinkN); + link->uplink_enabled = (enabled != 0); +} + +extern "C" JNIEXPORT void JNICALL Java_com_openipc_wfbngrtl8812_WfbNgLink_nativeSetUplinkMcs(JNIEnv *env, + jclass clazz, + jlong wfbngLinkN, + jint mcs) { + WfbngLink *link = native(wfbngLinkN); + // Single spatial stream: 0..7. Out-of-range values are ignored rather than + // clamped, so a bad setting cannot silently become a different rate. + if (mcs >= 0 && mcs <= 7) { + link->uplink_mcs = mcs; + } else { + __android_log_print(ANDROID_LOG_ERROR, TAG, "ignoring out-of-range uplink MCS %d", mcs); + } +} + +extern "C" JNIEXPORT void JNICALL Java_com_openipc_wfbngrtl8812_WfbNgLink_nativeSetUplinkFecN(JNIEnv *env, + jclass clazz, + jlong wfbngLinkN, + jint n) { + WfbngLink *link = native(wfbngLinkN); + // k is 1, so n is copies-per-message and must be >= 1. Upper bound keeps the + // airtime this was written to contain from creeping back. + if (n >= 1 && n <= 8) { + link->uplink_fec_n = n; + } else { + __android_log_print(ANDROID_LOG_ERROR, TAG, "ignoring out-of-range uplink FEC n %d", n); + } +} + extern "C" JNIEXPORT void JNICALL Java_com_openipc_wfbngrtl8812_WfbNgLink_nativeSetFecThresholds( JNIEnv *env, jclass clazz, jlong nativeInstance, jint lostTo5, jint recTo4, jint recTo3, jint recTo2, jint recTo1) { WfbngLink *link = reinterpret_cast(nativeInstance); diff --git a/app/wfbngrtl8812/src/main/cpp/WfbngLink.hpp b/app/wfbngrtl8812/src/main/cpp/WfbngLink.hpp index 24f74c5b..bc2feb26 100644 --- a/app/wfbngrtl8812/src/main/cpp/WfbngLink.hpp +++ b/app/wfbngrtl8812/src/main/cpp/WfbngLink.hpp @@ -58,6 +58,25 @@ class WfbngLink { bool ldpc_enabled{true}; bool stbc_enabled{true}; + // Uplink airtime controls. The adapter is half duplex, so every uplink frame + // is a receive blackout and the downlink loses whatever video fragments land + // in it. Measured on an RTL8812AU at 5 GHz, in lost video fragments/s that + // the downlink FEC could not recover: + // + // mcs 0, n 5 15.82 (the old hardcoded values) + // mcs 1, n 5 2.18 <- default: one MCS step, redundancy untouched + // mcs 3, n 2 0.12 + // uplink off 0.00 + // + // The default spends ~3 dB of uplink margin and keeps all five copies, which + // matters because losing mavlink is worse than losing video. Anyone who wants + // the last of the artefacts gone can trade further in the Uplink dialog. + // Read when TxArgs is built, so a change applies on the next link start — + // same lifecycle as ldpc/stbc. + bool uplink_enabled{true}; // false = transmit nothing at all + int uplink_mcs{1}; // HT MCS index for uplink frames + int uplink_fec_n{5}; // uplink FEC n (with k=1: copies per message) + std::map> rtl_devices; std::unique_ptr link_quality_thread{nullptr}; bool should_clear_stats{false}; diff --git a/app/wfbngrtl8812/src/main/java/com/openipc/wfbngrtl8812/WfbNgLink.java b/app/wfbngrtl8812/src/main/java/com/openipc/wfbngrtl8812/WfbNgLink.java index ea0347de..c7b2305d 100644 --- a/app/wfbngrtl8812/src/main/java/com/openipc/wfbngrtl8812/WfbNgLink.java +++ b/app/wfbngrtl8812/src/main/java/com/openipc/wfbngrtl8812/WfbNgLink.java @@ -49,6 +49,9 @@ public void setFecThresholds(int lostTo5, int recTo4, int recTo3, int recTo2, in public static native void nativeSetUseFec(long nativeInstance, int use); public static native void nativeSetUseLdpc(long nativeInstance, int use); public static native void nativeSetUseStbc(long nativeInstance, int use); + public static native void nativeSetUplinkEnabled(long nativeInstance, int enabled); + public static native void nativeSetUplinkMcs(long nativeInstance, int mcs); + public static native void nativeSetUplinkFecN(long nativeInstance, int n); public WfbNgLink(final AppCompatActivity parent) { this.context = parent; @@ -92,6 +95,21 @@ public void nativeSetUseStbc(int use) { nativeSetUseStbc(nativeWfbngLink, use); } + // Uplink airtime controls. Every uplink frame is a receive blackout on this + // half-duplex radio, so these trade uplink margin for video integrity. Like + // LDPC/STBC they are read when the link starts, so a change needs a reconnect. + public void nativeSetUplinkEnabled(boolean enabled) { + nativeSetUplinkEnabled(nativeWfbngLink, enabled ? 1 : 0); + } + + public void nativeSetUplinkMcs(int mcs) { + nativeSetUplinkMcs(nativeWfbngLink, mcs); + } + + public void nativeSetUplinkFecN(int n) { + nativeSetUplinkFecN(nativeWfbngLink, n); + } + public synchronized void start(int wifiChannel, int bandWidth, UsbDevice usbDevice) { Log.d(TAG, "wfb-ng monitoring on " + usbDevice.getDeviceName() + " using wifi channel " + wifiChannel); UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);