feat(tak): show local server status (#6599)

This commit is contained in:
Benjamin Faershtein authored and GitHub committed 2026-08-12 02:00:38 +00:00
1 parent 80f91b3457
commit a31fa287e2
21 files changed
+411 -32

No files matched your search

+8
View File
@@ -1658,6 +1658,14 @@ tak_server_loading
tak_server_mesh_to_cot
tak_server_mesh_to_cot_desc
tak_server_section
tak_server_status
tak_server_status_connected
tak_server_status_failed
tak_server_status_not_running
tak_server_status_off
tak_server_status_starting
tak_server_status_unavailable
tak_server_status_waiting
tak_server_test_card_title
tak_server_test_idle
tak_server_test_result_bytes
@@ -1697,12 +1697,23 @@
<string name="tak_role_unspecified">Unspecified</string>
<string name="tak_server">TAK Server</string>
<string name="tak_server_enabled">Enable Local TAK Server</string>
<string name="tak_server_enabled_desc">Starts a local TLS server on port 8089 for ATAK/iTAK connections</string>
<string name="tak_server_export_data_package_desc">Generate .zip for ATAK/iTAK to connect to this server</string>
<string name="tak_server_enabled_desc">Starts a local TLS server on port 8089 for ATAK connections</string>
<string name="tak_server_export_data_package_desc">Generate .zip for ATAK to connect to this server</string>
<string name="tak_server_loading"></string>
<string name="tak_server_mesh_to_cot">Mesh to CoT Converter</string>
<string name="tak_server_mesh_to_cot_desc">Show Meshtastic nodes on the ATAK/iTAK map as contacts</string>
<string name="tak_server_mesh_to_cot_desc">Show Meshtastic nodes on the ATAK map as contacts</string>
<string name="tak_server_section">Server</string>
<string name="tak_server_status">Status</string>
<plurals name="tak_server_status_connected">
<item quantity="one">%1$d local client connection</item>
<item quantity="other">%1$d local client connections</item>
</plurals>
<string name="tak_server_status_failed">Unable to start TAK Server. Turn it off and on to retry.</string>
<string name="tak_server_status_not_running">TAK Server is not running</string>
<string name="tak_server_status_off">Off</string>
<string name="tak_server_status_starting">Starting TAK Server</string>
<string name="tak_server_status_unavailable">Local TAK Server is available in Meshtastic for Android</string>
<string name="tak_server_status_waiting">Listening on 127.0.0.1:8089 — waiting for ATAK</string>
<string name="tak_server_test_card_title">TAK Mesh Test (Debug)</string>
<string name="tak_server_test_idle">Send all %1$d test fixtures to mesh</string>
<string name="tak_server_test_result_bytes">%1$dB ✓</string>
@@ -115,7 +115,7 @@ class MeshServiceOrchestrator(
if (isEnabled && !takServerManager.isRunning.value) {
Logger.i { "TAK Server enabled by preference, starting integration" }
takMeshIntegration.start(newScope)
} else if (!isEnabled && takServerManager.isRunning.value) {
} else if (!isEnabled) {
Logger.i { "TAK Server disabled by preference, stopping integration" }
takMeshIntegration.stop()
}
@@ -189,10 +189,7 @@ class MeshServiceOrchestrator(
*/
fun stop() {
Logger.i { "Stopping mesh service orchestrator" }
// Guard stop() so we don't emit a spurious "stopped" log when TAK was never started
if (takServerManager.isRunning.value) {
takMeshIntegration.stop()
}
takMeshIntegration.stop()
// Best-effort polite goodbye on service teardown (onDestroy / process shutdown). We launch
// on a fresh detached scope — not the orchestrator's per-start scope — so the subsequent
// scope.cancel() below doesn't interrupt the short drain delay inside disconnect(). The
@@ -203,6 +203,63 @@ class MeshServiceOrchestratorTest {
orchestrator.stop()
}
@Test
fun testTakServerCanRetryAfterFailedStart() {
val takEnabledFlow = MutableStateFlow(false)
val takRunningFlow = MutableStateFlow(false)
val lifecycleEvents = mutableListOf<String>()
every { takServerManager.start(any()) } calls
{
lifecycleEvents += "start"
Unit
}
every { takServerManager.stop() } calls
{
lifecycleEvents += "stop"
Unit
}
val orchestrator = createOrchestrator(takEnabledFlow = takEnabledFlow, takRunningFlow = takRunningFlow)
orchestrator.start()
// The mock never changes takRunningFlow, modeling a start attempt that failed before listening.
takEnabledFlow.value = true
takEnabledFlow.value = false
takEnabledFlow.value = true
assertEquals(listOf("start", "stop", "start"), lifecycleEvents)
orchestrator.stop()
assertEquals(listOf("start", "stop", "start", "stop"), lifecycleEvents)
}
@Test
fun testStopStopsTakServerWhileStarting() {
val takEnabledFlow = MutableStateFlow(true)
val takRunningFlow = MutableStateFlow(false)
val lifecycleEvents = mutableListOf<String>()
every { takServerManager.start(any()) } calls
{
lifecycleEvents += "start"
Unit
}
every { takServerManager.stop() } calls
{
lifecycleEvents += "stop"
Unit
}
val orchestrator = createOrchestrator(takEnabledFlow = takEnabledFlow, takRunningFlow = takRunningFlow)
orchestrator.start()
orchestrator.stop()
takEnabledFlow.value = false
orchestrator.start()
takEnabledFlow.value = true
orchestrator.stop()
assertEquals(listOf("start", "stop", "start", "stop"), lifecycleEvents)
}
@Test
fun testStartCallsSwitchActiveDatabase() {
// New ordering: start() waits for currentDeviceAddressFlow to surface a valid address,
@@ -32,6 +32,10 @@ import org.meshtastic.core.di.CoroutineDispatchers
*/
interface TAKServer {
/** Whether this platform provides a local TAK listener. */
val isSupported: Boolean
get() = true
/** Observable count of currently-connected TAK clients (ATAK/iTAK). */
val connectionCount: StateFlow<Int>
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.concurrent.Volatile
import kotlin.time.Clock
import kotlin.time.Duration.Companion.minutes
@@ -34,8 +35,11 @@ import kotlin.time.Duration.Companion.minutes
data class InboundCoTMessage(val cotMessage: CoTMessage, val clientInfo: TAKClientInfo? = null)
interface TAKServerManager {
val isSupported: Boolean
val isRunning: StateFlow<Boolean>
val isStarting: StateFlow<Boolean>
val connectionCount: StateFlow<Int>
val hasStartError: StateFlow<Boolean>
val inboundMessages: SharedFlow<InboundCoTMessage>
/**
@@ -61,12 +65,22 @@ internal class TAKServerManagerImpl(private val takServer: TAKServer) : TAKServe
private var scope: CoroutineScope? = null
@Volatile private var startGeneration = 0L
override val isSupported = takServer.isSupported
private val _isRunning = MutableStateFlow(false)
override val isRunning: StateFlow<Boolean> = _isRunning.asStateFlow()
private val _isStarting = MutableStateFlow(false)
override val isStarting: StateFlow<Boolean> = _isStarting.asStateFlow()
// Mirror TAKServer's event-driven connection count — no polling needed
override val connectionCount: StateFlow<Int> = takServer.connectionCount
private val _hasStartError = MutableStateFlow(false)
override val hasStartError: StateFlow<Boolean> = _hasStartError.asStateFlow()
private val _inboundMessages = MutableSharedFlow<InboundCoTMessage>(extraBufferCapacity = 64)
override val inboundMessages: SharedFlow<InboundCoTMessage> = _inboundMessages.asSharedFlow()
@@ -80,6 +94,7 @@ internal class TAKServerManagerImpl(private val takServer: TAKServer) : TAKServe
private val offlineQueue = ArrayDeque<QueuedMessage>()
private val offlineQueueMutex = Mutex()
private val lifecycleMutex = Mutex()
companion object {
private val OFFLINE_QUEUE_TTL = 5.minutes
@@ -87,38 +102,51 @@ internal class TAKServerManagerImpl(private val takServer: TAKServer) : TAKServe
}
override fun start(scope: CoroutineScope) {
if (_isRunning.value) {
if (!isSupported) return
if (_isRunning.value || _isStarting.value) {
Logger.w { "TAKServerManager already running" }
return
}
_hasStartError.value = false
_isStarting.value = true
val generation = ++startGeneration
// Assign scope AFTER the guard so a second concurrent start() can never
// overwrite the active scope without actually restarting the server.
this.scope = scope
scope.launch {
// Wire up inbound message handler BEFORE starting so no messages are lost.
// Use tryEmit (non-suspending) with extraBufferCapacity to avoid launching a
// new coroutine per message, which would create unbounded coroutines under
// high message rates and could reorder messages.
takServer.onMessage = { cotMessage, clientInfo ->
if (!_inboundMessages.tryEmit(InboundCoTMessage(cotMessage, clientInfo))) {
Logger.w { "TAK inbound message buffer full; dropping message from ${clientInfo?.id}" }
lifecycleMutex.withLock {
if (generation != startGeneration) return@withLock
// Wire up inbound message handler BEFORE starting so no messages are lost.
// Use tryEmit (non-suspending) with extraBufferCapacity to avoid launching a
// new coroutine per message, which would create unbounded coroutines under
// high message rates and could reorder messages.
takServer.onMessage = { cotMessage, clientInfo ->
if (!_inboundMessages.tryEmit(InboundCoTMessage(cotMessage, clientInfo))) {
Logger.w { "TAK inbound message buffer full; dropping message from ${clientInfo?.id}" }
}
}
takServer.onClientConnected = {
drainOfflineQueue()
_clientConnected.tryEmit(Unit)
}
}
takServer.onClientConnected = {
drainOfflineQueue()
_clientConnected.tryEmit(Unit)
}
val result = takServer.start(scope)
if (result.isSuccess) {
_isRunning.value = true
Logger.i { "TAK Server started" }
} else {
Logger.e(result.exceptionOrNull()) { "Failed to start TAK Server" }
// Clear both callbacks if start failed so we don't hold a reference unnecessarily
takServer.onMessage = null
takServer.onClientConnected = null
val result = takServer.start(scope)
if (generation != startGeneration) {
if (result.isSuccess) takServer.stop()
return@withLock
}
_isStarting.value = false
if (result.isSuccess) {
_isRunning.value = true
Logger.i { "TAK Server started" }
} else {
_hasStartError.value = true
Logger.e(result.exceptionOrNull()) { "Failed to start TAK Server" }
// Clear both callbacks if start failed so we don't hold a reference unnecessarily
takServer.onMessage = null
takServer.onClientConnected = null
}
}
}
}
@@ -128,7 +156,10 @@ internal class TAKServerManagerImpl(private val takServer: TAKServer) : TAKServe
// any broadcast()/drainOfflineQueue() that races stop() sees _isRunning=false
// and exits early instead of launching coroutines on a scope that is about to
// be discarded.
startGeneration++
_isRunning.value = false
_isStarting.value = false
_hasStartError.value = false
scope = null
takServer.onMessage = null
takServer.onClientConnected = null
@@ -33,9 +33,17 @@ import kotlinx.coroutines.flow.asStateFlow
* only exercise the connected-client / broadcast surface (not the start/stop lifecycle) can ignore it.
*/
internal class FakeTAKServerManager : TAKServerManager {
override val isSupported = true
private val _isRunning = MutableStateFlow(false)
override val isRunning: StateFlow<Boolean> = _isRunning.asStateFlow()
private val _isStarting = MutableStateFlow(false)
override val isStarting: StateFlow<Boolean> = _isStarting.asStateFlow()
private val _hasStartError = MutableStateFlow(false)
override val hasStartError: StateFlow<Boolean> = _hasStartError.asStateFlow()
val connections = MutableStateFlow(0)
override val connectionCount: StateFlow<Int> = connections
@@ -16,10 +16,12 @@
*/
package org.meshtastic.core.takserver
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -186,15 +188,61 @@ class TAKServerManagerTest {
override suspend fun hasConnections(): Boolean = false
}
private class DelayedTAKServer : TAKServer {
override val connectionCount: StateFlow<Int> = MutableStateFlow(0)
override var onMessage: ((CoTMessage, TAKClientInfo?) -> Unit)? = null
override var onClientConnected: (() -> Unit)? = null
val startResults = mutableListOf<CompletableDeferred<Result<Unit>>>()
var stopCount = 0
override suspend fun start(scope: CoroutineScope): Result<Unit> =
CompletableDeferred<Result<Unit>>().also(startResults::add).await()
override fun stop() {
stopCount++
}
override suspend fun broadcast(cotMessage: CoTMessage) {}
override suspend fun broadcastRawXml(xml: String) {}
override suspend fun hasConnections(): Boolean = false
}
@Test
fun `start failure due to port conflict leaves isRunning false`() = runTest {
fun `start failure due to port conflict reports an error state`() = runTest {
val failingServer = FailingTAKServer()
val manager = TAKServerManagerImpl(failingServer)
manager.start(this)
advanceUntilIdle()
// Manager should NOT be running after start failure
assertEquals(false, manager.isRunning.value)
assertTrue(manager.hasStartError.value)
}
@Test
fun `next start waits for stale start cleanup`() = runTest {
val delayedServer = DelayedTAKServer()
val manager = TAKServerManagerImpl(delayedServer)
manager.start(this)
runCurrent()
assertEquals(1, delayedServer.startResults.size)
manager.stop()
manager.start(this)
runCurrent()
assertEquals(1, delayedServer.startResults.size)
delayedServer.startResults[0].complete(Result.success(Unit))
runCurrent()
assertEquals(2, delayedServer.startResults.size)
delayedServer.startResults[1].complete(Result.success(Unit))
advanceUntilIdle()
assertTrue(manager.isRunning.value)
assertEquals(2, delayedServer.stopCount)
}
@Test
@@ -31,6 +31,7 @@ import org.meshtastic.core.di.CoroutineDispatchers
* `TAKServer` interface entirely.
*/
private class NoopTAKServer : TAKServer {
override val isSupported = false
private val _connectionCount = MutableStateFlow(0)
override val connectionCount: StateFlow<Int> = _connectionCount.asStateFlow()
override var onMessage: ((CoTMessage, TAKClientInfo?) -> Unit)? = null
@@ -18,11 +18,15 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -44,6 +48,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import org.meshtastic.core.common.BuildConfigProvider
@@ -65,6 +70,14 @@ import org.meshtastic.core.resources.tak_server_loading
import org.meshtastic.core.resources.tak_server_mesh_to_cot
import org.meshtastic.core.resources.tak_server_mesh_to_cot_desc
import org.meshtastic.core.resources.tak_server_section
import org.meshtastic.core.resources.tak_server_status
import org.meshtastic.core.resources.tak_server_status_connected
import org.meshtastic.core.resources.tak_server_status_failed
import org.meshtastic.core.resources.tak_server_status_not_running
import org.meshtastic.core.resources.tak_server_status_off
import org.meshtastic.core.resources.tak_server_status_starting
import org.meshtastic.core.resources.tak_server_status_unavailable
import org.meshtastic.core.resources.tak_server_status_waiting
import org.meshtastic.core.resources.tak_server_test_card_title
import org.meshtastic.core.resources.tak_server_test_idle
import org.meshtastic.core.resources.tak_server_test_result_bytes
@@ -74,6 +87,7 @@ import org.meshtastic.core.resources.tak_server_test_run
import org.meshtastic.core.resources.tak_server_test_running
import org.meshtastic.core.resources.tak_team
import org.meshtastic.core.takserver.TAKDataPackageGenerator
import org.meshtastic.core.takserver.TAKServerManager
import org.meshtastic.core.takserver.TakMeshTestRunner
import org.meshtastic.core.takserver.TakTestResult
import org.meshtastic.core.ui.component.DropDownPreference
@@ -180,8 +194,22 @@ internal fun handleTakPermissionResult(granted: Boolean, isTakServerEnabled: Boo
@Composable
fun TakServerScreen(onBack: () -> Unit) {
val takPrefs: TakPrefs = koinInject()
val takServerManager: TAKServerManager = koinInject()
val isTakServerEnabled by takPrefs.isTakServerEnabled.collectAsStateWithLifecycle()
val isMeshToCotEnabled by takPrefs.isMeshToCotEnabled.collectAsStateWithLifecycle()
val isTakServerRunning by takServerManager.isRunning.collectAsStateWithLifecycle()
val isTakServerStarting by takServerManager.isStarting.collectAsStateWithLifecycle()
val takClientCount by takServerManager.connectionCount.collectAsStateWithLifecycle()
val hasTakServerStartError by takServerManager.hasStartError.collectAsStateWithLifecycle()
val takServerStatus =
TakServerStatus.resolve(
isSupported = takServerManager.isSupported,
isEnabled = isTakServerEnabled,
isRunning = isTakServerRunning,
isStarting = isTakServerStarting,
clientCount = takClientCount,
hasStartError = hasTakServerStartError,
)
val exportLauncher = rememberDataPackageExporter { TAKDataPackageGenerator.generateDataPackage() }
TakPermissionHandler(
@@ -220,6 +248,8 @@ fun TakServerScreen(onBack: () -> Unit) {
onEnabledChange = { takPrefs.setTakServerEnabled(it) },
isMeshToCotEnabled = isMeshToCotEnabled,
onMeshToCotChange = { takPrefs.setMeshToCotEnabled(it) },
status = takServerStatus,
clientCount = takClientCount,
onExport = { exportLauncher("Meshtastic_TAK_Server.zip") },
)
TakMeshTestCard()
@@ -234,6 +264,8 @@ internal fun TakServerSection(
onEnabledChange: (Boolean) -> Unit,
isMeshToCotEnabled: Boolean,
onMeshToCotChange: (Boolean) -> Unit,
status: TakServerStatus,
clientCount: Int,
onExport: () -> Unit,
) {
TitledCard(title = stringResource(Res.string.tak_server_section)) {
@@ -244,6 +276,8 @@ internal fun TakServerSection(
enabled = true,
onCheckedChange = onEnabledChange,
)
HorizontalDivider()
TakServerStatusRow(status = status, clientCount = clientCount)
if (isTakServerEnabled) {
HorizontalDivider()
SwitchPreference(
@@ -281,6 +315,79 @@ internal fun TakServerSection(
}
}
internal enum class TakServerStatus {
Unavailable,
Off,
Starting,
WaitingForClient,
Connected,
Failed,
NotRunning,
;
companion object {
fun resolve(
isSupported: Boolean,
isEnabled: Boolean,
isRunning: Boolean,
isStarting: Boolean,
clientCount: Int,
hasStartError: Boolean,
): TakServerStatus = when {
!isSupported -> Unavailable
!isEnabled -> Off
hasStartError -> Failed
isStarting -> Starting
isRunning && clientCount > 0 -> Connected
isRunning -> WaitingForClient
else -> NotRunning
}
}
}
@Composable
private fun TakServerStatusRow(status: TakServerStatus, clientCount: Int) {
val (description, color) =
when (status) {
TakServerStatus.Unavailable ->
stringResource(Res.string.tak_server_status_unavailable) to MaterialTheme.colorScheme.onSurfaceVariant
TakServerStatus.Off ->
stringResource(Res.string.tak_server_status_off) to MaterialTheme.colorScheme.onSurfaceVariant
TakServerStatus.Starting ->
stringResource(Res.string.tak_server_status_starting) to MaterialTheme.colorScheme.onSurfaceVariant
TakServerStatus.WaitingForClient ->
stringResource(Res.string.tak_server_status_waiting) to MaterialTheme.colorScheme.primary
TakServerStatus.Connected ->
pluralStringResource(Res.plurals.tak_server_status_connected, clientCount, clientCount) to
MaterialTheme.colorScheme.primary
TakServerStatus.Failed ->
stringResource(Res.string.tak_server_status_failed) to MaterialTheme.colorScheme.error
TakServerStatus.NotRunning ->
stringResource(Res.string.tak_server_status_not_running) to MaterialTheme.colorScheme.onSurfaceVariant
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(modifier = Modifier.size(10.dp).background(color, CircleShape))
Column(modifier = Modifier.padding(start = 12.dp)) {
Text(text = stringResource(Res.string.tak_server_status), style = MaterialTheme.typography.bodyLarge)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
// ── Debug-only TAK Mesh Test Card ────────────────────────────────────────────
@Composable
@@ -48,6 +48,8 @@ fun TakServerSectionDisabledPreview() {
onEnabledChange = {},
isMeshToCotEnabled = false,
onMeshToCotChange = {},
status = TakServerStatus.Off,
clientCount = 0,
onExport = {},
)
}
@@ -62,6 +64,40 @@ fun TakServerSectionEnabledPreview() {
onEnabledChange = {},
isMeshToCotEnabled = true,
onMeshToCotChange = {},
status = TakServerStatus.WaitingForClient,
clientCount = 0,
onExport = {},
)
}
}
@PreviewLightDark
@Composable
fun TakServerSectionConnectedPreview() {
AppTheme {
TakServerSection(
isTakServerEnabled = true,
onEnabledChange = {},
isMeshToCotEnabled = true,
onMeshToCotChange = {},
status = TakServerStatus.Connected,
clientCount = 1,
onExport = {},
)
}
}
@PreviewLightDark
@Composable
fun TakServerSectionFailedPreview() {
AppTheme {
TakServerSection(
isTakServerEnabled = true,
onEnabledChange = {},
isMeshToCotEnabled = true,
onMeshToCotChange = {},
status = TakServerStatus.Failed,
clientCount = 0,
onExport = {},
)
}
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.settings.radio.component
import kotlin.test.Test
import kotlin.test.assertEquals
class TakServerStatusTest {
@Test
fun `disabled server is off`() {
assertEquals(TakServerStatus.Off, TakServerStatus.resolve(true, false, false, false, 0, false))
}
@Test
fun `enabled server reports startup failure`() {
assertEquals(TakServerStatus.Failed, TakServerStatus.resolve(true, true, false, false, 0, true))
}
@Test
fun `enabled server reports starting until it listens`() {
assertEquals(TakServerStatus.Starting, TakServerStatus.resolve(true, true, false, true, 0, false))
}
@Test
fun `listening server distinguishes waiting and connected clients`() {
assertEquals(TakServerStatus.WaitingForClient, TakServerStatus.resolve(true, true, true, false, 0, false))
assertEquals(TakServerStatus.Connected, TakServerStatus.resolve(true, true, true, false, 1, false))
}
@Test
fun `stopped server ignores stale client count`() {
assertEquals(TakServerStatus.NotRunning, TakServerStatus.resolve(true, true, false, false, 1, false))
}
@Test
fun `unsupported platform and inactive service report their distinct states`() {
assertEquals(TakServerStatus.Unavailable, TakServerStatus.resolve(false, true, false, false, 0, false))
assertEquals(TakServerStatus.NotRunning, TakServerStatus.resolve(true, true, false, false, 0, false))
}
}
@@ -42,8 +42,10 @@ import org.meshtastic.feature.settings.radio.component.PacketAuthenticityStrictC
import org.meshtastic.feature.settings.radio.component.PacketAuthenticityStrictPreview
import org.meshtastic.feature.settings.radio.component.PacketAuthenticityUnsupportedPreview
import org.meshtastic.feature.settings.radio.component.TakConfigCardPreview
import org.meshtastic.feature.settings.radio.component.TakServerSectionConnectedPreview
import org.meshtastic.feature.settings.radio.component.TakServerSectionDisabledPreview
import org.meshtastic.feature.settings.radio.component.TakServerSectionEnabledPreview
import org.meshtastic.feature.settings.radio.component.TakServerSectionFailedPreview
import org.meshtastic.feature.settings.radio.component.TakTestCardIdlePreview
import org.meshtastic.feature.settings.radio.component.TakTestCardResultsPreview
import org.meshtastic.feature.settings.radio.component.TakTestCardRunningPreview
@@ -97,6 +99,20 @@ fun ScreenshotTakServerSectionEnabled() {
TakServerSectionEnabledPreview()
}
@PreviewTest
@PreviewLightDark
@Composable
fun ScreenshotTakServerSectionConnected() {
TakServerSectionConnectedPreview()
}
@PreviewTest
@PreviewLightDark
@Composable
fun ScreenshotTakServerSectionFailed() {
TakServerSectionFailedPreview()
}
@PreviewTest
@PreviewLightDark
@Composable
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 75 KiB