Skip to content
Merged
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
24 changes: 18 additions & 6 deletions app/src/main/java/com/freefcc/app/DumlTransport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ class DumlTransport {
* RC link, vs ~12s with the old generous timeouts).
*
* @param onProgress Called with a 0..1 float as frames are sent
* @return true if at least one frame was written successfully
* @return true only if every frame write succeeded (false for an empty list)
*/
fun sendFrames(
frames: List<ByteArray>,
Expand All @@ -253,19 +253,22 @@ class DumlTransport {
port: Int = PORT,
onProgress: (Float) -> Unit = {}
): Boolean {
if (frames.isEmpty()) return false

// If port is the default (40009), scan for the actual working port.
// If port is explicitly set (e.g. 40007 for LED), use it directly.
val effectivePort = if (port == PORT) findWorkingPort() else port

var anySuccess = false
// Attempt every frame regardless of earlier failures, but only report
// success if every single write succeeded — a partial send is a failed
// apply, not a success. Matches sendFramesUnix() (4G) in this file.
val results = mutableListOf<Boolean>()
val totalSends = frames.size * rounds
var sent = 0

for (round in 0 until rounds) {
for (frame in frames) {
if (sendOneFrame(frame, readWindowMs, effectivePort)) {
anySuccess = true
}
results.add(sendOneFrame(frame, readWindowMs, effectivePort))
sent++
onProgress(sent.toFloat() / totalSends)
if (interFrameDelayMs > 0) Thread.sleep(interFrameDelayMs)
Expand All @@ -274,7 +277,7 @@ class DumlTransport {
Thread.sleep(interRoundDelayMs)
}
}
return anySuccess
return allFramesSucceeded(results)
}

/**
Expand Down Expand Up @@ -560,6 +563,15 @@ class DumlTransport {

/** TCP connect timeout for all socket opens to the DUML proxy. */
private const val CONNECT_TIMEOUT_MS = 2000

/**
* A frame series counts as sent only if it was non-empty and every
* single write succeeded. A partial send is a failed apply, not a
* success — this is what stops the app from reporting "FCC enabled"
* when the link dropped after the first of 42 writes.
*/
internal fun allFramesSucceeded(results: List<Boolean>): Boolean =
results.isNotEmpty() && results.all { it }
}

/** Reused read buffer for ACK reads — avoids a per-frame allocation. */
Expand Down
10 changes: 6 additions & 4 deletions app/src/main/java/com/freefcc/app/FccViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ class FccViewModel(private val app: Application) : AndroidViewModel(app) {
* "frames written but 4G didn't activate" message.
*
* Sources: DJI product list, captured profiles (only wa341 confirmed
* working on real hardware). wa233/wa234 = Matrice 300/350 series,
* wm630 = Inspire 3, wa341 = Mavic 4 Pro. All are DJI enterprise models
* that ship with or accept the Cellular Dongle 2.
* working on real hardware). wm630 = Inspire 3, wa341 = Mavic 4 Pro, and
* the wa233/wa234 Matrice-class codes accept the Cellular Dongle 2.
* wa140 (Mini 4 Pro) is intentionally NOT here — the Mini series has no
* cellular module, so it must not pass the 4G gate (previously it was
* both excluded in this comment and included in the set).
*/
private val MODELS_WITH_4G = setOf("wa341", "wa233", "wa234", "wm630", "wa140")
private val MODELS_WITH_4G = setOf("wa341", "wa233", "wa234", "wm630")
}

private val _state = MutableStateFlow(AppState())
Expand Down
46 changes: 46 additions & 0 deletions app/src/test/java/com/freefcc/app/DumlTransportSendFramesTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.freefcc.app

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* Covers DumlTransport.allFramesSucceeded() — the aggregation rule for
* sendFrames(): a non-empty send series reports success only if EVERY write
* succeeded, and an empty series is always a failure. This is what prevents
* the app from reporting "FCC enabled" when the link dropped partway through
* the 21-frame x 2-round apply (the old "any success = true" behaviour).
*/
class DumlTransportSendFramesTest {

@Test
fun allWritesSucceed_reportsSuccess() {
assertTrue(DumlTransport.allFramesSucceeded(listOf(true, true, true)))
}

@Test
fun singleWriteSucceeds_reportsSuccess() {
assertTrue(DumlTransport.allFramesSucceeded(listOf(true)))
}

@Test
fun oneWriteFails_reportsFailure() {
// The exact scenario from the bug: link drops after the first write.
assertFalse(DumlTransport.allFramesSucceeded(listOf(true, false, true)))
}

@Test
fun onlyFirstWriteSucceeds_reportsFailure() {
assertFalse(DumlTransport.allFramesSucceeded(listOf(true, false, false)))
}

@Test
fun allWritesFail_reportsFailure() {
assertFalse(DumlTransport.allFramesSucceeded(listOf(false, false)))
}

@Test
fun emptySeries_reportsFailure() {
assertFalse(DumlTransport.allFramesSucceeded(emptyList()))
}
}
Loading