diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt index f98aaa04a..01f5d370c 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt @@ -50,7 +50,6 @@ fun NavGraphBuilder.connectionsGraph(navController: NavHostController) { restoreState = true } }, - onNavigateToSettings = { navController.navigate(SettingsRoutes.Settings()) }, onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, onConfigNavigate = { route -> navController.navigate(route) }, ) diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt index 2c4848f71..d489d5f32 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt @@ -36,6 +36,8 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.meshtastic.core.model.util.anonymize import java.lang.reflect.Method import java.util.UUID @@ -137,6 +139,42 @@ constructor( private lateinit var fromNum: BluetoothGattCharacteristic + // RSSI flow & polling job (null when unavailable / disconnected) + private val _rssiFlow = MutableStateFlow(null) + val rssiFlow: StateFlow = _rssiFlow + + @Volatile private var rssiPollingJob: Job? = null + + // Start polling RSSI every 5 seconds (immediate first read) + @Suppress("MagicNumber", "LoopWithTooManyJumpStatements") + private fun startRssiPolling() { + rssiPollingJob?.cancel() + val s = safe ?: return + // Immediate read for faster UI update + s.asyncReadRemoteRssi { first -> first.getOrNull()?.let { _rssiFlow.value = it } } + rssiPollingJob = + service.serviceScope.handledLaunch { + while (true) { + try { + delay(5000) + if (safe == null) break + safe?.asyncReadRemoteRssi { res -> res.getOrNull()?.let { _rssiFlow.value = it } } + } catch (ex: CancellationException) { + break + } catch (ex: Exception) { + debug("RSSI polling error: ${ex.message}") + } + } + } + } + + // Stop polling and clear current value + private fun stopRssiPolling() { + rssiPollingJob?.cancel() + rssiPollingJob = null + _rssiFlow.value = null + } + /** * With the new rev2 api, our first send is to start the configure readbacks. In that case, rather than waiting for * FromNum notifies - we try to just aggressively read all of the responses. @@ -201,6 +239,7 @@ constructor( /** We had some problem, schedule a reconnection attempt (if one isn't already queued) */ private fun scheduleReconnect(reason: String) { + stopRssiPolling() if (reconnectJob == null) { warn("Scheduling reconnect because $reason") reconnectJob = service.serviceScope.handledLaunch { retryDueToException() } @@ -391,6 +430,7 @@ constructor( service.serviceScope.handledLaunch { info("Connected to radio!") + startRssiPolling() if ( needForceRefresh @@ -431,6 +471,7 @@ constructor( override fun close() { reconnectJob?.cancel() // Cancel any queued reconnect attempts + stopRssiPolling() if (safe != null) { info("Closing BluetoothInterface") diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt index 8b9c10367..34b622cd1 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt @@ -97,6 +97,10 @@ constructor( private var radioIf: IRadioInterface = NopInterface("") + // Expose current bluetooth RSSI (null if not connected or not BLE) + private val _bluetoothRssi = MutableStateFlow(null) + val bluetoothRssi: StateFlow = _bluetoothRssi.asStateFlow() + /** * true if we have started our interface * @@ -254,6 +258,13 @@ constructor( } radioIf = interfaceFactory.createInterface(address) + + // If the new interface is bluetooth, collect its RSSI flow + if (radioIf is BluetoothInterface) { + (radioIf as BluetoothInterface).rssiFlow.onEach { _bluetoothRssi.emit(it) }.launchIn(serviceScope) + } else { + _bluetoothRssi.value = null + } } } } @@ -280,6 +291,7 @@ constructor( if (r !is NopInterface) { onDisconnect(isPermanent = true) // Tell any clients we are now offline } + _bluetoothRssi.value = null } /** diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 05866290d..2f06ed1a2 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -325,6 +325,7 @@ class MeshService : serviceScope.handledLaunch { radioInterfaceService.connect() } radioInterfaceService.connectionState.onEach(::onRadioConnectionState).launchIn(serviceScope) radioInterfaceService.receivedData.onEach(::onReceiveFromRadio).launchIn(serviceScope) + radioInterfaceService.bluetoothRssi.onEach { serviceRepository.setBluetoothRssi(it) }.launchIn(serviceScope) radioConfigRepository.localConfigFlow.onEach { localConfig = it }.launchIn(serviceScope) radioConfigRepository.moduleConfigFlow.onEach { moduleConfig = it }.launchIn(serviceScope) radioConfigRepository.channelSetFlow.onEach { channelSet = it }.launchIn(serviceScope) diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index 662882ecd..969d645d0 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -317,6 +317,11 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { completeWork(status, descriptor) } + + // Added: callback for remote RSSI reads + override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) { + completeWork(status, rssi) + } } // To test loss of BLE faults we can randomly fail a certain % of all work items. We @@ -654,6 +659,14 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD fun asyncWriteDescriptor(c: BluetoothGattDescriptor, cb: (Result) -> Unit) = queueWriteDescriptor(c, CallbackContinuation(cb)) + // Added: Support reading remote RSSI + private fun queueReadRemoteRssi(cont: Continuation, timeout: Long = 0) = + queueWork("readRSSI", cont, timeout) { gatt?.readRemoteRssi() ?: false } + + fun asyncReadRemoteRssi(cb: (Result) -> Unit) = queueReadRemoteRssi(CallbackContinuation(cb)) + + fun readRemoteRssi(timeout: Long = timeoutMsec): Int = makeSync { queueReadRemoteRssi(it, timeout) } + /** * Some old androids have a bug where calling disconnect doesn't guarantee that the onConnectionStateChange callback * gets called but the only safe way to call gatt.close is from that callback. So we set a flag once we start diff --git a/app/src/main/java/com/geeksville/mesh/service/ServiceRepository.kt b/app/src/main/java/com/geeksville/mesh/service/ServiceRepository.kt index 11d3e5f54..85a72e486 100644 --- a/app/src/main/java/com/geeksville/mesh/service/ServiceRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/service/ServiceRepository.kt @@ -50,6 +50,15 @@ class ServiceRepository @Inject constructor() : Logging { _connectionState.value = connectionState } + // Current bluetooth link RSSI (dBm). Null if not connected or not a bluetooth interface. + private val _bluetoothRssi = MutableStateFlow(null) + val bluetoothRssi: StateFlow + get() = _bluetoothRssi + + fun setBluetoothRssi(rssi: Int?) { + _bluetoothRssi.value = rssi + } + private val _clientNotification = MutableStateFlow(null) val clientNotification: StateFlow get() = _clientNotification diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt index fb8111f21..0efeffcc5 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt @@ -103,7 +103,6 @@ fun ConnectionsScreen( scanModel: BTScanModel = hiltViewModel(), radioConfigViewModel: RadioConfigViewModel = hiltViewModel(), onClickNodeChip: (Int) -> Unit, - onNavigateToSettings: () -> Unit, onNavigateToNodeDetails: (Int) -> Unit, onConfigNavigate: (Route) -> Unit, ) { @@ -120,6 +119,7 @@ fun ConnectionsScreen( val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle() val bluetoothState by connectionsViewModel.bluetoothState.collectAsStateWithLifecycle() val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET + val bluetoothRssi by connectionsViewModel.bluetoothRssi.collectAsStateWithLifecycle() val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle() val discoveredTcpDevices by scanModel.discoveredTcpDevicesForUi.collectAsStateWithLifecycle() @@ -222,8 +222,8 @@ fun ConnectionsScreen( node = node, onNavigateToNodeDetails = onNavigateToNodeDetails, onSetShowSharedContact = { showSharedContact = it }, - onNavigateToSettings = onNavigateToSettings, onClickDisconnect = { scanModel.disconnect() }, + bluetoothRssi = bluetoothRssi, ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt index d988804ca..b1866546d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt @@ -60,6 +60,9 @@ constructor( val bluetoothState = bluetoothRepository.state + // Newly added: bluetooth RSSI stream (dBm, null if unavailable) + val bluetoothRssi = serviceRepository.bluetoothRssi + private val _hasShownNotPairedWarning = MutableStateFlow(uiPrefs.hasShownNotPairedWarning) val hasShownNotPairedWarning: StateFlow = _hasShownNotPairedWarning.asStateFlow() diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt index b3501859e..2cf036560 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt @@ -23,13 +23,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -48,19 +45,31 @@ import com.geeksville.mesh.ui.node.components.NodeMenuAction import org.meshtastic.core.database.model.Node import org.meshtastic.core.strings.R import org.meshtastic.core.ui.component.MaterialBatteryInfo +import org.meshtastic.core.ui.component.MaterialBluetoothSignalInfo import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.theme.StatusColors.StatusRed +/** Converts Bluetooth RSSI to a 0-4 bar signal strength level. */ @Composable fun CurrentlyConnectedInfo( node: Node, onNavigateToNodeDetails: (Int) -> Unit, onSetShowSharedContact: (Node) -> Unit, - onNavigateToSettings: () -> Unit, onClickDisconnect: () -> Unit, modifier: Modifier = Modifier, + bluetoothRssi: Int? = null, ) { Column(modifier = modifier) { + Row( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + MaterialBatteryInfo(level = node.batteryLevel) + if (bluetoothRssi != null) { + MaterialBluetoothSignalInfo(rssi = bluetoothRssi) + } + } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Column( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -79,8 +88,6 @@ fun CurrentlyConnectedInfo( } }, ) - - MaterialBatteryInfo(level = node.batteryLevel) } Column(modifier = Modifier.weight(1f, fill = true)) { @@ -95,13 +102,6 @@ fun CurrentlyConnectedInfo( ) } } - - IconButton(enabled = true, onClick = onNavigateToSettings) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = stringResource(id = R.string.radio_configuration), - ) - } } Button( @@ -124,23 +124,26 @@ fun CurrentlyConnectedInfo( @Composable private fun CurrentlyConnectedInfoPreview() { AppTheme { - CurrentlyConnectedInfo( - node = - Node( - num = 13444, - user = MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build(), - isIgnored = false, - paxcounter = PaxcountProtos.Paxcount.newBuilder().setBle(10).setWifi(5).build(), - environmentMetrics = - TelemetryProtos.EnvironmentMetrics.newBuilder() - .setTemperature(25f) - .setRelativeHumidity(60f) - .build(), - ), - onNavigateToNodeDetails = {}, - onSetShowSharedContact = {}, - onNavigateToSettings = {}, - onClickDisconnect = {}, - ) + Surface { + CurrentlyConnectedInfo( + node = + Node( + num = 13444, + user = + MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build(), + isIgnored = false, + paxcounter = PaxcountProtos.Paxcount.newBuilder().setBle(10).setWifi(5).build(), + environmentMetrics = + TelemetryProtos.EnvironmentMetrics.newBuilder() + .setTemperature(25f) + .setRelativeHumidity(60f) + .build(), + ), + bluetoothRssi = -75, // Example RSSI for signal preview + onNavigateToNodeDetails = {}, + onSetShowSharedContact = {}, + onClickDisconnect = {}, + ) + } } } diff --git a/core/strings/src/main/res/values/strings.xml b/core/strings/src/main/res/values/strings.xml index efa195dc3..af9032825 100644 --- a/core/strings/src/main/res/values/strings.xml +++ b/core/strings/src/main/res/values/strings.xml @@ -918,4 +918,5 @@ 24 Hours 48 Hours Filter by Last Heard time: %s + %1$d dBm diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MaterialBluetoothSignalInfo.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MaterialBluetoothSignalInfo.kt new file mode 100644 index 000000000..30c72eae6 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MaterialBluetoothSignalInfo.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2025 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 . + */ + +package org.meshtastic.core.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Bluetooth +import androidx.compose.material.icons.rounded.SignalCellularOff +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import org.meshtastic.core.strings.R +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.SignalCellular0Bar +import org.meshtastic.core.ui.icon.SignalCellular1Bar +import org.meshtastic.core.ui.icon.SignalCellular2Bar +import org.meshtastic.core.ui.icon.SignalCellular3Bar +import org.meshtastic.core.ui.icon.SignalCellular4Bar +import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.core.ui.theme.StatusColors.StatusGreen +import org.meshtastic.core.ui.theme.StatusColors.StatusOrange +import org.meshtastic.core.ui.theme.StatusColors.StatusRed +import org.meshtastic.core.ui.theme.StatusColors.StatusYellow + +private const val SIZE_ICON = 20 + +/** + * A composable that displays a signal strength indicator with an icon and optional text value. The icon and its color + * change based on the number of signal bars. + * + * @param modifier Modifier for this composable. + * @param signalBars The number of signal bars, typically from 0 to 4. Values outside this range (e.g., < 0) will + * display a "signal off" or unknown state icon. + * @param signalStrengthValue Optional text to display next to the icon, such as dBm or SNR value. + */ +@Suppress("MagicNumber") +@Composable +fun MaterialSignalInfo( + signalBars: Int, + modifier: Modifier = Modifier, + signalStrengthValue: String? = null, + typeIcon: ImageVector? = null, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + val (iconVector, iconTint) = + when (signalBars) { + 0 -> MeshtasticIcons.SignalCellular0Bar to MaterialTheme.colorScheme.StatusRed + 1 -> MeshtasticIcons.SignalCellular1Bar to MaterialTheme.colorScheme.StatusRed + 2 -> MeshtasticIcons.SignalCellular2Bar to MaterialTheme.colorScheme.StatusOrange + 3 -> MeshtasticIcons.SignalCellular3Bar to MaterialTheme.colorScheme.StatusYellow + 4 -> MeshtasticIcons.SignalCellular4Bar to MaterialTheme.colorScheme.StatusGreen + else -> Icons.Rounded.SignalCellularOff to MaterialTheme.colorScheme.onSurfaceVariant + } + + val foregroundPainter = typeIcon?.let { rememberVectorPainter(typeIcon) } + Icon( + imageVector = iconVector, + contentDescription = null, + tint = iconTint, + modifier = + Modifier.size(SIZE_ICON.dp).drawWithContent { + drawContent() + @Suppress("MagicNumber") + if (foregroundPainter != null) { + val badgeSize = size.width * .45f + with(foregroundPainter) { + draw(Size(badgeSize, badgeSize), colorFilter = ColorFilter.tint(iconTint)) + } + } + }, + ) + + signalStrengthValue?.let { + Text(text = it, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge) + } + } +} + +@Composable +fun MaterialBluetoothSignalInfo(rssi: Int, modifier: Modifier = Modifier) { + MaterialSignalInfo( + modifier = modifier, + signalBars = getBluetoothSignalBars(rssi = rssi), + signalStrengthValue = stringResource(R.string.dbm_value, rssi), + typeIcon = Icons.Rounded.Bluetooth, + ) +} + +@Suppress("MagicNumber") +private fun getBluetoothSignalBars(rssi: Int): Int = when { + rssi > -60 -> 4 // Excellent + rssi > -70 -> 3 // Good + rssi > -80 -> 2 // Fair + rssi > -90 -> 1 // Weak + else -> 0 // Poor/No Signal +} + +class SignalStrengthProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf(-95, -85, -75, -65, -55) +} + +@PreviewLightDark +@Composable +private fun MaterialBluetoothSignalInfoPreview(@PreviewParameter(SignalStrengthProvider::class) rssi: Int) { + AppTheme { Surface { MaterialBluetoothSignalInfo(rssi = rssi) } } +} diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Signal.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Signal.kt new file mode 100644 index 000000000..326d566ed --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Signal.kt @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2025 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 . + */ + +package org.meshtastic.core.ui.icon + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val MeshtasticIcons.SignalCellular0Bar: ImageVector + get() { + if (signalCellular0Bar != null) { + return signalCellular0Bar!! + } + signalCellular0Bar = + ImageVector.Builder( + name = "SignalCellular0Bar", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ) + .apply { + path(fill = SolidColor(Color(0xFFE3E3E3))) { + moveTo(177f, 880f) + quadToRelative(-27f, 0f, -37.5f, -24.5f) + reflectiveQuadTo(148f, 812f) + lineToRelative(664f, -664f) + quadToRelative(19f, -19f, 43.5f, -8.5f) + reflectiveQuadTo(880f, 177f) + verticalLineToRelative(643f) + quadToRelative(0f, 25f, -17.5f, 42.5f) + reflectiveQuadTo(820f, 880f) + lineTo(177f, 880f) + close() + moveTo(273f, 800f) + horizontalLineToRelative(527f) + verticalLineToRelative(-526f) + lineTo(273f, 800f) + close() + } + } + .build() + + return signalCellular0Bar!! + } + +private var signalCellular0Bar: ImageVector? = null + +val MeshtasticIcons.SignalCellular1Bar: ImageVector + get() { + if (signalCellular1Bar != null) { + return signalCellular1Bar!! + } + signalCellular1Bar = + ImageVector.Builder( + name = "SignalCellular1Bar", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ) + .apply { + path(fill = SolidColor(Color(0xFFE3E3E3))) { + moveTo(177f, 880f) + quadToRelative(-18f, 0f, -29.5f, -12f) + reflectiveQuadTo(136f, 840f) + quadToRelative(0f, -8f, 3f, -15f) + reflectiveQuadToRelative(9f, -13f) + lineToRelative(664f, -664f) + quadToRelative(6f, -6f, 13f, -9f) + reflectiveQuadToRelative(15f, -3f) + quadToRelative(16f, 0f, 28f, 11.5f) + reflectiveQuadToRelative(12f, 29.5f) + verticalLineToRelative(643f) + quadToRelative(0f, 25f, -17.5f, 42.5f) + reflectiveQuadTo(820f, 880f) + lineTo(177f, 880f) + close() + moveTo(400f, 800f) + horizontalLineToRelative(400f) + verticalLineToRelative(-526f) + lineTo(400f, 674f) + verticalLineToRelative(126f) + close() + } + } + .build() + + return signalCellular1Bar!! + } + +private var signalCellular1Bar: ImageVector? = null + +val MeshtasticIcons.SignalCellular2Bar: ImageVector + get() { + if (signalCellular2Bar != null) { + return signalCellular2Bar!! + } + signalCellular2Bar = + ImageVector.Builder( + name = "SignalCellular2Bar", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ) + .apply { + path(fill = SolidColor(Color(0xFFE3E3E3))) { + moveTo(177f, 880f) + quadToRelative(-18f, 0f, -29.5f, -12f) + reflectiveQuadTo(136f, 840f) + quadToRelative(0f, -8f, 3f, -15f) + reflectiveQuadToRelative(9f, -13f) + lineToRelative(664f, -664f) + quadToRelative(6f, -6f, 13f, -9f) + reflectiveQuadToRelative(15f, -3f) + quadToRelative(16f, 0f, 28f, 11.5f) + reflectiveQuadToRelative(12f, 29.5f) + verticalLineToRelative(643f) + quadToRelative(0f, 25f, -17.5f, 42.5f) + reflectiveQuadTo(820f, 880f) + lineTo(177f, 880f) + close() + moveTo(520f, 800f) + horizontalLineToRelative(280f) + verticalLineToRelative(-526f) + lineTo(520f, 554f) + verticalLineToRelative(246f) + close() + } + } + .build() + + return signalCellular2Bar!! + } + +private var signalCellular2Bar: ImageVector? = null + +val MeshtasticIcons.SignalCellular3Bar: ImageVector + get() { + if (signalCellular3Bar != null) { + return signalCellular3Bar!! + } + signalCellular3Bar = + ImageVector.Builder( + name = "SignalCellular3Bar", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ) + .apply { + path(fill = SolidColor(Color(0xFFE3E3E3))) { + moveTo(177f, 880f) + quadToRelative(-18f, 0f, -29.5f, -12f) + reflectiveQuadTo(136f, 840f) + quadToRelative(0f, -8f, 3f, -15f) + reflectiveQuadToRelative(9f, -13f) + lineToRelative(664f, -664f) + quadToRelative(6f, -6f, 13f, -9f) + reflectiveQuadToRelative(15f, -3f) + quadToRelative(16f, 0f, 28f, 11.5f) + reflectiveQuadToRelative(12f, 29.5f) + verticalLineToRelative(643f) + quadToRelative(0f, 25f, -17.5f, 42.5f) + reflectiveQuadTo(820f, 880f) + lineTo(177f, 880f) + close() + moveTo(600f, 800f) + horizontalLineToRelative(200f) + verticalLineToRelative(-526f) + lineTo(600f, 474f) + verticalLineToRelative(326f) + close() + } + } + .build() + + return signalCellular3Bar!! + } + +private var signalCellular3Bar: ImageVector? = null + +val MeshtasticIcons.SignalCellular4Bar: ImageVector + get() { + if (signalCellular4Bar != null) { + return signalCellular4Bar!! + } + signalCellular4Bar = + ImageVector.Builder( + name = "SignalCellular4Bar", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ) + .apply { + path(fill = SolidColor(Color(0xFFE3E3E3))) { + moveTo(177f, 880f) + quadToRelative(-18f, 0f, -29.5f, -12f) + reflectiveQuadTo(136f, 840f) + quadToRelative(0f, -8f, 3f, -15f) + reflectiveQuadToRelative(9f, -13f) + lineToRelative(664f, -664f) + quadToRelative(6f, -6f, 13f, -9f) + reflectiveQuadToRelative(15f, -3f) + quadToRelative(16f, 0f, 28f, 11.5f) + reflectiveQuadToRelative(12f, 29.5f) + verticalLineToRelative(643f) + quadToRelative(0f, 25f, -17.5f, 42.5f) + reflectiveQuadTo(820f, 880f) + lineTo(177f, 880f) + close() + } + } + .build() + + return signalCellular4Bar!! + } + +private var signalCellular4Bar: ImageVector? = null