diff --git a/app/src/main/java/com/freefcc/app/DumlTransport.kt b/app/src/main/java/com/freefcc/app/DumlTransport.kt index 338b0cb..8ec7eaa 100644 --- a/app/src/main/java/com/freefcc/app/DumlTransport.kt +++ b/app/src/main/java/com/freefcc/app/DumlTransport.kt @@ -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, @@ -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() 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) @@ -274,7 +277,7 @@ class DumlTransport { Thread.sleep(interRoundDelayMs) } } - return anySuccess + return allFramesSucceeded(results) } /** @@ -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 = + results.isNotEmpty() && results.all { it } } /** Reused read buffer for ACK reads — avoids a per-frame allocation. */ diff --git a/app/src/main/java/com/freefcc/app/FccViewModel.kt b/app/src/main/java/com/freefcc/app/FccViewModel.kt index af7511e..d05e85b 100644 --- a/app/src/main/java/com/freefcc/app/FccViewModel.kt +++ b/app/src/main/java/com/freefcc/app/FccViewModel.kt @@ -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()) diff --git a/app/src/test/java/com/freefcc/app/DumlTransportSendFramesTest.kt b/app/src/test/java/com/freefcc/app/DumlTransportSendFramesTest.kt new file mode 100644 index 0000000..a261b29 --- /dev/null +++ b/app/src/test/java/com/freefcc/app/DumlTransportSendFramesTest.kt @@ -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())) + } +}