kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat(bluetooth): expose and display bluetooth signal strength (RSSI) (#3235)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>pull/3237/head
rodzic
00e9be0919
commit
92202e3ebf
|
|
@ -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) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Int?>(null)
|
||||
val rssiFlow: StateFlow<Int?> = _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")
|
||||
|
|
|
|||
|
|
@ -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<Int?>(null)
|
||||
val bluetoothRssi: StateFlow<Int?> = _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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<BluetoothGattDescriptor>) -> Unit) =
|
||||
queueWriteDescriptor(c, CallbackContinuation(cb))
|
||||
|
||||
// Added: Support reading remote RSSI
|
||||
private fun queueReadRemoteRssi(cont: Continuation<Int>, timeout: Long = 0) =
|
||||
queueWork("readRSSI", cont, timeout) { gatt?.readRemoteRssi() ?: false }
|
||||
|
||||
fun asyncReadRemoteRssi(cb: (Result<Int>) -> 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
|
||||
|
|
|
|||
|
|
@ -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<Int?>(null)
|
||||
val bluetoothRssi: StateFlow<Int?>
|
||||
get() = _bluetoothRssi
|
||||
|
||||
fun setBluetoothRssi(rssi: Int?) {
|
||||
_bluetoothRssi.value = rssi
|
||||
}
|
||||
|
||||
private val _clientNotification = MutableStateFlow<MeshProtos.ClientNotification?>(null)
|
||||
val clientNotification: StateFlow<MeshProtos.ClientNotification?>
|
||||
get() = _clientNotification
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Boolean> = _hasShownNotPairedWarning.asStateFlow()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -918,4 +918,5 @@
|
|||
<string name="one_day">24 Hours</string>
|
||||
<string name="two_days">48 Hours</string>
|
||||
<string name="last_heard_filter_label">Filter by Last Heard time: %s</string>
|
||||
<string name="dbm_value">%1$d dBm</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Int> {
|
||||
override val values: Sequence<Int> = sequenceOf(-95, -85, -75, -65, -55)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun MaterialBluetoothSignalInfoPreview(@PreviewParameter(SignalStrengthProvider::class) rssi: Int) {
|
||||
AppTheme { Surface { MaterialBluetoothSignalInfo(rssi = rssi) } }
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
Ładowanie…
Reference in New Issue