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
81 changes: 81 additions & 0 deletions app/src/main/java/com/openipc/pixelpilot/VideoActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -858,6 +938,7 @@ void initDefaultOptions() {
wfbLink.nativeSetUseStbc(stbcEnabled ? 1 : 0);

setFecThresholdsFromPrefs();
setUplinkFromPrefs();
}

// Read FEC thresholds from prefs and call native method to apply
Expand Down
51 changes: 48 additions & 3 deletions app/wfbngrtl8812/src/main/cpp/WfbngLink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<TxArgs> args = std::make_shared<TxArgs>();
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(
Expand Down Expand Up @@ -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<WfbngLink *>(nativeInstance);
Expand Down
19 changes: 19 additions & 0 deletions app/wfbngrtl8812/src/main/cpp/WfbngLink.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, std::shared_ptr<IRtlDevice>> rtl_devices;
std::unique_ptr<std::thread> link_quality_thread{nullptr};
bool should_clear_stats{false};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down