mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Implement iOS support and unify Compose Multiplatform infrastructure (#4876)
This commit is contained in:
parent
f04924ded5
commit
d136b162a4
170 changed files with 2208 additions and 2432 deletions
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.core.ble
|
||||
|
||||
import com.juul.kable.Peripheral
|
||||
import com.juul.kable.PeripheralBuilder
|
||||
|
||||
/** No-op stubs for iOS target in core:ble. */
|
||||
internal actual fun PeripheralBuilder.platformConfig(device: BleDevice, autoConnect: () -> Boolean) {
|
||||
// No-op for stubs
|
||||
}
|
||||
|
||||
internal actual fun createPeripheral(address: String, builderAction: PeripheralBuilder.() -> Unit): Peripheral =
|
||||
throw UnsupportedOperationException("iOS Peripheral not yet implemented")
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.core.common.util
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
/** Access to the IO dispatcher in a multiplatform-safe way. */
|
||||
expect val ioDispatcher: CoroutineDispatcher
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.core.common.util
|
||||
|
||||
/** Multiplatform string formatting helper. */
|
||||
expect fun formatString(pattern: String, vararg args: Any?): String
|
||||
|
|
@ -81,7 +81,7 @@ object HomoglyphCharacterStringTransformer {
|
|||
*/
|
||||
fun optimizeUtf8StringWithHomoglyphs(value: String): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
for (c in value.toCharArray()) stringBuilder.append(homoglyphCharactersSubstitutionMapping.getOrDefault(c, c))
|
||||
for (c in value.toCharArray()) stringBuilder.append(homoglyphCharactersSubstitutionMapping[c] ?: c)
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.core.common.util
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
actual val ioDispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.core.common.util
|
||||
|
||||
/** Apple (iOS) implementation of string formatting. Stub implementation for compile-only validation. */
|
||||
actual fun formatString(pattern: String, vararg args: Any?): String = throw UnsupportedOperationException(
|
||||
"formatString is not supported on iOS at runtime; this target is intended for compile-only validation.",
|
||||
)
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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.core.common.util
|
||||
|
||||
/** No-op stubs for iOS target in core:common. */
|
||||
actual object BuildUtils {
|
||||
actual val isEmulator: Boolean = false
|
||||
actual val sdkInt: Int = 0
|
||||
}
|
||||
|
||||
actual class CommonUri(actual val host: String?, actual val fragment: String?, actual val pathSegments: List<String>) {
|
||||
actual fun getQueryParameter(key: String): String? = null
|
||||
|
||||
actual fun getBooleanQueryParameter(key: String, defaultValue: Boolean): Boolean = defaultValue
|
||||
|
||||
actual override fun toString(): String = ""
|
||||
|
||||
actual companion object {
|
||||
actual fun parse(uriString: String): CommonUri = CommonUri(null, null, emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
actual fun CommonUri.toPlatformUri(): Any = Any()
|
||||
|
||||
actual object DateFormatter {
|
||||
actual fun formatRelativeTime(timestampMillis: Long): String = ""
|
||||
|
||||
actual fun formatDateTime(timestampMillis: Long): String = ""
|
||||
|
||||
actual fun formatShortDate(timestampMillis: Long): String = ""
|
||||
|
||||
actual fun formatTime(timestampMillis: Long): String = ""
|
||||
|
||||
actual fun formatTimeWithSeconds(timestampMillis: Long): String = ""
|
||||
|
||||
actual fun formatDate(timestampMillis: Long): String = ""
|
||||
|
||||
actual fun formatDateTimeShort(timestampMillis: Long): String = ""
|
||||
}
|
||||
|
||||
actual fun getSystemMeasurementSystem(): MeasurementSystem = MeasurementSystem.METRIC
|
||||
|
||||
actual fun String?.isValidAddress(): Boolean = false
|
||||
|
||||
actual interface CommonParcelable
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
actual annotation class CommonParcelize actual constructor()
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
actual annotation class CommonIgnoredOnParcel actual constructor()
|
||||
|
||||
actual interface CommonParceler<T> {
|
||||
actual fun create(parcel: CommonParcel): T
|
||||
|
||||
actual fun T.write(parcel: CommonParcel, flags: Int)
|
||||
}
|
||||
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Repeatable
|
||||
actual annotation class CommonTypeParceler<T, P : CommonParceler<in T>> actual constructor()
|
||||
|
||||
actual class CommonParcel {
|
||||
actual fun readString(): String? = null
|
||||
|
||||
actual fun readInt(): Int = 0
|
||||
|
||||
actual fun readLong(): Long = 0L
|
||||
|
||||
actual fun readFloat(): Float = 0.0f
|
||||
|
||||
actual fun createByteArray(): ByteArray? = null
|
||||
|
||||
actual fun writeByteArray(b: ByteArray?) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.core.common.util
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
actual val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.core.common.util
|
||||
|
||||
/** JVM/Android implementation of string formatting. */
|
||||
actual fun formatString(pattern: String, vararg args: Any?): String = String.format(pattern, *args)
|
||||
|
|
@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
|
|||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
|
|
@ -58,7 +58,7 @@ class CommandSenderImpl(
|
|||
private val nodeManager: NodeManager,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
) : CommandSender {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
private val currentPacketId = atomic(Random(nowMillis).nextLong().absoluteValue)
|
||||
private val sessionPasskey = atomic(ByteString.EMPTY)
|
||||
override val tracerouteStartTimes = mutableMapOf<Int, Long>()
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@
|
|||
package org.meshtastic.core.data.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ignoreException
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
|
|
@ -64,7 +64,7 @@ class MeshActionHandlerImpl(
|
|||
private val notificationManager: NotificationManager,
|
||||
private val messageProcessor: Lazy<MeshMessageProcessor>,
|
||||
) : MeshActionHandler {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
|
||||
override fun start(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ package org.meshtastic.core.data.manager
|
|||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import okio.IOException
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.repository.CommandSender
|
||||
import org.meshtastic.core.repository.MeshConfigFlowManager
|
||||
|
|
@ -56,7 +56,7 @@ class MeshConfigFlowManagerImpl(
|
|||
private val commandSender: CommandSender,
|
||||
private val packetHandler: PacketHandler,
|
||||
) : MeshConfigFlowManager {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
private val configOnlyNonce = 69420
|
||||
private val nodeInfoNonce = 69421
|
||||
private val wantConfigDelay = 100L
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package org.meshtastic.core.data.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -25,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.repository.MeshConfigHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
|
|
@ -41,7 +41,7 @@ class MeshConfigHandlerImpl(
|
|||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeManager: NodeManager,
|
||||
) : MeshConfigHandler {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
|
||||
private val _localConfig = MutableStateFlow(LocalConfig())
|
||||
override val localConfig = _localConfig.asStateFlow()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
|
|||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
|
@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
|
|
@ -89,7 +89,7 @@ class MeshConnectionManagerImpl(
|
|||
private val workerManager: MeshWorkerManager,
|
||||
private val appWidgetUpdater: AppWidgetUpdater,
|
||||
) : MeshConnectionManager {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
private var sleepTimeout: Job? = null
|
||||
private var locationRequestsJob: Job? = null
|
||||
private var handshakeTimeout: Job? = null
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
|
|||
import co.touchlab.kermit.Logger
|
||||
import co.touchlab.kermit.Severity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -30,6 +29,7 @@ import okio.ByteString.Companion.toByteString
|
|||
import okio.IOException
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
|
|
@ -63,7 +63,7 @@ import org.meshtastic.core.repository.TracerouteHandler
|
|||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.critical_alert
|
||||
import org.meshtastic.core.resources.error_duty_cycle
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.getStringSuspend
|
||||
import org.meshtastic.core.resources.low_battery_message
|
||||
import org.meshtastic.core.resources.low_battery_title
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
|
|
@ -114,7 +114,7 @@ class MeshDataHandlerImpl(
|
|||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val messageFilter: MessageFilter,
|
||||
) : MeshDataHandler {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
|
||||
override fun start(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
|
|
@ -433,9 +433,13 @@ class MeshDataHandlerImpl(
|
|||
if (shouldBatteryNotificationShow(fromNum, t, myNodeNum)) {
|
||||
notificationManager.dispatch(
|
||||
Notification(
|
||||
title = getString(Res.string.low_battery_title, nextNode.user.short_name),
|
||||
title =
|
||||
getStringSuspend(
|
||||
Res.string.low_battery_title,
|
||||
nextNode.user.short_name,
|
||||
),
|
||||
message =
|
||||
getString(
|
||||
getStringSuspend(
|
||||
Res.string.low_battery_message,
|
||||
nextNode.user.long_name,
|
||||
nextNode.deviceMetrics.battery_level ?: 0,
|
||||
|
|
@ -502,7 +506,9 @@ class MeshDataHandlerImpl(
|
|||
val payload = packet.decoded?.payload ?: return
|
||||
val r = Routing.ADAPTER.decodeOrNull(payload, Logger) ?: return
|
||||
if (r.error_reason == Routing.Error.DUTY_CYCLE_LIMIT) {
|
||||
serviceRepository.setErrorMessage(getString(Res.string.error_duty_cycle), Severity.Warn)
|
||||
scope.launch {
|
||||
serviceRepository.setErrorMessage(getStringSuspend(Res.string.error_duty_cycle), Severity.Warn)
|
||||
}
|
||||
}
|
||||
handleAckNak(
|
||||
packet.decoded?.request_id ?: 0,
|
||||
|
|
@ -659,25 +665,27 @@ class MeshDataHandlerImpl(
|
|||
val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true
|
||||
val isSilent = conversationMuted || nodeMuted
|
||||
if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) {
|
||||
notificationManager.dispatch(
|
||||
Notification(
|
||||
title = getSenderName(dataPacket),
|
||||
message = dataPacket.alert ?: getString(Res.string.critical_alert),
|
||||
category = Notification.Category.Alert,
|
||||
contactKey = contactKey,
|
||||
),
|
||||
)
|
||||
scope.launch {
|
||||
notificationManager.dispatch(
|
||||
Notification(
|
||||
title = getSenderName(dataPacket),
|
||||
message = dataPacket.alert ?: getStringSuspend(Res.string.critical_alert),
|
||||
category = Notification.Category.Alert,
|
||||
contactKey = contactKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if (updateNotification && !isSilent) {
|
||||
scope.handledLaunch { updateNotification(contactKey, dataPacket, isSilent) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSenderName(packet: DataPacket): String {
|
||||
private suspend fun getSenderName(packet: DataPacket): String {
|
||||
if (packet.from == DataPacket.ID_LOCAL) {
|
||||
val myId = nodeManager.getMyId()
|
||||
return nodeManager.nodeDBbyID[myId]?.user?.long_name ?: getString(Res.string.unknown_username)
|
||||
return nodeManager.nodeDBbyID[myId]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
}
|
||||
return nodeManager.nodeDBbyID[packet.from]?.user?.long_name ?: getString(Res.string.unknown_username)
|
||||
return nodeManager.nodeDBbyID[packet.from]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
}
|
||||
|
||||
private suspend fun updateNotification(contactKey: String, dataPacket: DataPacket, isSilent: Boolean) {
|
||||
|
|
@ -701,7 +709,7 @@ class MeshDataHandlerImpl(
|
|||
}
|
||||
|
||||
PortNum.WAYPOINT_APP.value -> {
|
||||
val message = getString(Res.string.waypoint_received, dataPacket.waypoint!!.name)
|
||||
val message = getStringSuspend(Res.string.waypoint_received, dataPacket.waypoint!!.name)
|
||||
notificationManager.dispatch(
|
||||
Notification(
|
||||
title = getSenderName(dataPacket),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package org.meshtastic.core.data.manager
|
|||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -28,6 +27,7 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import kotlinx.coroutines.sync.withLock
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.MeshLog
|
||||
|
|
@ -55,7 +55,7 @@ class MeshMessageProcessorImpl(
|
|||
private val router: Lazy<MeshRouter>,
|
||||
private val fromRadioDispatcher: FromRadioPacketHandler,
|
||||
) : MeshMessageProcessor {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
|
||||
private val mapsMutex = Mutex()
|
||||
private val logUuidByPacketId = mutableMapOf<Int, String>()
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ package org.meshtastic.core.data.manager
|
|||
import co.touchlab.kermit.Logger
|
||||
import co.touchlab.kermit.Severity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.network.repository.MQTTRepository
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.PacketHandler
|
||||
|
|
@ -39,7 +39,7 @@ class MqttManagerImpl(
|
|||
private val packetHandler: PacketHandler,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
) : MqttManager {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
private var mqttMessageFlow: Job? = null
|
||||
|
||||
override fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) {
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ package org.meshtastic.core.data.manager
|
|||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.repository.CommandSender
|
||||
import org.meshtastic.core.repository.NeighborInfoHandler
|
||||
|
|
@ -38,7 +38,7 @@ class NeighborInfoHandlerImpl(
|
|||
private val commandSender: CommandSender,
|
||||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
) : NeighborInfoHandler {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
|
||||
override fun start(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ import kotlinx.atomicfu.atomic
|
|||
import kotlinx.atomicfu.update
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import okio.ByteString
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceMetrics
|
||||
import org.meshtastic.core.model.EnvironmentMetrics
|
||||
|
|
@ -43,7 +43,7 @@ import org.meshtastic.core.repository.Notification
|
|||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.ServiceBroadcasts
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.getStringSuspend
|
||||
import org.meshtastic.core.resources.new_node_seen
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
|
|
@ -62,7 +62,7 @@ class NodeManagerImpl(
|
|||
private val serviceBroadcasts: ServiceBroadcasts,
|
||||
private val notificationManager: NotificationManager,
|
||||
) : NodeManager {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
|
||||
private val _nodeDBbyNodeNum = atomic(persistentMapOf<Int, Node>())
|
||||
private val _nodeDBbyID = atomic(persistentMapOf<String, Node>())
|
||||
|
|
@ -196,13 +196,15 @@ class NodeManagerImpl(
|
|||
node.copy(user = newUser, channel = channel, manuallyVerified = manuallyVerified)
|
||||
}
|
||||
if (newNode && !shouldPreserve) {
|
||||
notificationManager.dispatch(
|
||||
Notification(
|
||||
title = getString(Res.string.new_node_seen, next.user.short_name),
|
||||
message = next.user.long_name,
|
||||
category = Notification.Category.NodeEvent,
|
||||
),
|
||||
)
|
||||
scope.handledLaunch {
|
||||
notificationManager.dispatch(
|
||||
Notification(
|
||||
title = getStringSuspend(Res.string.new_node_seen, next.user.short_name),
|
||||
message = next.user.long_name,
|
||||
category = Notification.Category.NodeEvent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
|
|||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -30,6 +29,7 @@ import kotlinx.coroutines.withTimeout
|
|||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
|
|
@ -67,7 +67,7 @@ class PacketHandlerImpl(
|
|||
}
|
||||
|
||||
private var queueJob: Job? = null
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher)
|
||||
|
||||
private val queueMutex = Mutex()
|
||||
private val queuedPackets = mutableListOf<MeshPacket>()
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ package org.meshtastic.core.data.manager
|
|||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
|
|
@ -44,7 +44,7 @@ class TracerouteHandlerImpl(
|
|||
private val nodeRepository: NodeRepository,
|
||||
private val commandSender: CommandSender,
|
||||
) : TracerouteHandler {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
|
||||
override fun start(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import androidx.room3.RoomDatabase
|
|||
import androidx.room3.TypeConverters
|
||||
import androidx.room3.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.database.dao.DeviceHardwareDao
|
||||
import org.meshtastic.core.database.dao.FirmwareReleaseDao
|
||||
import org.meshtastic.core.database.dao.MeshLogDao
|
||||
|
|
@ -122,14 +122,15 @@ abstract class MeshtasticDatabase : RoomDatabase() {
|
|||
fun <T : RoomDatabase> RoomDatabase.Builder<T>.configureCommon(): RoomDatabase.Builder<T> =
|
||||
this.fallbackToDestructiveMigration(dropAllTables = false)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.setQueryCoroutineContext(Dispatchers.IO)
|
||||
.setQueryCoroutineContext(ioDispatcher)
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteTable.Entries(DeleteTable(tableName = "NodeInfo"), DeleteTable(tableName = "MyNodeInfo"))
|
||||
@DeleteTable(tableName = "NodeInfo")
|
||||
@DeleteTable(tableName = "MyNodeInfo")
|
||||
class AutoMigration12to13 : AutoMigrationSpec
|
||||
|
||||
@DeleteColumn.Entries(DeleteColumn(tableName = "packet", columnName = "reply_id"))
|
||||
@DeleteColumn(tableName = "packet", columnName = "reply_id")
|
||||
class AutoMigration29to30 : AutoMigrationSpec
|
||||
|
||||
@DeleteColumn(tableName = "packet", columnName = "retry_count")
|
||||
|
|
|
|||
|
|
@ -17,11 +17,16 @@
|
|||
package org.meshtastic.core.database
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.okio.OkioSerializer
|
||||
import androidx.datastore.core.okio.OkioStorage
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.room3.Room
|
||||
import androidx.room3.RoomDatabase
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import okio.BufferedSink
|
||||
import okio.BufferedSource
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toPath
|
||||
|
|
@ -55,11 +60,32 @@ actual fun deleteDatabase(dbName: String) {
|
|||
/** Returns the system FileSystem for iOS. */
|
||||
actual fun getFileSystem(): FileSystem = FileSystem.SYSTEM
|
||||
|
||||
private object PreferencesSerializer : OkioSerializer<Preferences> {
|
||||
override val defaultValue: Preferences
|
||||
get() = emptyPreferences()
|
||||
|
||||
override suspend fun readFrom(source: BufferedSource): Preferences {
|
||||
// iOS stub: return an empty Preferences instance instead of crashing.
|
||||
return emptyPreferences()
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: Preferences, sink: BufferedSink) {
|
||||
// iOS stub: no-op to avoid crashing on write.
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates an iOS DataStore for database preferences. */
|
||||
actual fun createDatabaseDataStore(name: String): DataStore<Preferences> {
|
||||
val dir = documentDirectory() + "/datastore"
|
||||
NSFileManager.defaultManager.createDirectoryAtPath(dir, true, null, null)
|
||||
return PreferenceDataStoreFactory.create(produceFile = { (dir + "/$name.preferences_pb").toPath().toNioPath() })
|
||||
return DataStoreFactory.create(
|
||||
storage =
|
||||
OkioStorage(
|
||||
fileSystem = FileSystem.SYSTEM,
|
||||
serializer = PreferencesSerializer,
|
||||
producePath = { (dir + "/$name.preferences_pb").toPath() },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import androidx.datastore.preferences.core.edit
|
|||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -33,6 +32,7 @@ import kotlinx.coroutines.launch
|
|||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
|
||||
const val KEY_APP_INTRO_COMPLETED = "app_intro_completed"
|
||||
const val KEY_THEME = "theme"
|
||||
|
|
@ -52,7 +52,7 @@ const val KEY_EXCLUDE_MQTT = "exclude-mqtt"
|
|||
open class UiPreferencesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore<Preferences>) :
|
||||
UiPreferences {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
||||
|
||||
// Start this flow eagerly, so app intro doesn't flash (when disabled) on cold app start.
|
||||
override val appIntroCompleted: StateFlow<Boolean> =
|
||||
|
|
|
|||
|
|
@ -17,17 +17,17 @@
|
|||
package org.meshtastic.core.datastore.di
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.koin.core.annotation.ComponentScan
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
|
||||
@Module
|
||||
@ComponentScan("org.meshtastic.core.datastore")
|
||||
class CoreDatastoreModule {
|
||||
@Single
|
||||
@Named("DataStoreScope")
|
||||
fun provideDataStoreScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
fun provideDataStoreScope(): CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,5 +29,10 @@ kotlin {
|
|||
androidResources.enable = false
|
||||
}
|
||||
|
||||
sourceSets { commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) } }
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(projects.core.common)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@ package org.meshtastic.core.di.di
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
|
||||
@Module
|
||||
class CoreDiModule {
|
||||
@Single
|
||||
fun provideCoroutineDispatchers(): CoroutineDispatchers =
|
||||
CoroutineDispatchers(io = Dispatchers.IO, main = Dispatchers.Main, default = Dispatchers.Default)
|
||||
CoroutineDispatchers(io = ioDispatcher, main = Dispatchers.Main, default = Dispatchers.Default)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "org.meshtastic.core.domain"
|
||||
|
|
|
|||
|
|
@ -19,12 +19,8 @@ package org.meshtastic.core.model.util
|
|||
import org.meshtastic.core.common.util.nowInstant
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import org.meshtastic.core.model.util.TimeConstants.HOURS_PER_DAY
|
||||
import java.text.DateFormat
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
private val DAY_DURATION = 24.hours
|
||||
|
||||
|
|
@ -53,9 +49,3 @@ fun getShortDate(time: Long): String? {
|
|||
* @param remainingMillis The remaining time in milliseconds
|
||||
* @return Pair of (days, hours), where days is Int and hours is Double
|
||||
*/
|
||||
fun formatMuteRemainingTime(remainingMillis: Long): Pair<Int, Double> {
|
||||
val duration = remainingMillis.milliseconds
|
||||
if (duration <= Duration.ZERO) return 0 to 0.0
|
||||
val totalHours = duration.toDouble(DurationUnit.HOURS)
|
||||
return (totalHours / HOURS_PER_DAY).toInt() to (totalHours % HOURS_PER_DAY)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ data class Channel(val settings: ChannelSettings = default.settings, val loraCon
|
|||
cleartextPSK
|
||||
} else {
|
||||
// Treat an index of 1 as the old channelDefaultKey and work up from there
|
||||
val bytes = channelDefaultKey.clone()
|
||||
val bytes = channelDefaultKey.copyOf()
|
||||
bytes[bytes.size - 1] = (0xff and (bytes[bytes.size - 1] + pskIndex - 1)).toByte()
|
||||
bytes.toByteString()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import org.meshtastic.core.common.util.CommonParcel
|
|||
import org.meshtastic.core.common.util.CommonParcelable
|
||||
import org.meshtastic.core.common.util.CommonParcelize
|
||||
import org.meshtastic.core.common.util.CommonTypeParceler
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.util.ByteStringParceler
|
||||
import org.meshtastic.core.model.util.ByteStringSerializer
|
||||
|
|
@ -190,7 +191,7 @@ data class DataPacket(
|
|||
// Public-key cryptography (PKC) channel index
|
||||
const val PKC_CHANNEL_INDEX = 8
|
||||
|
||||
fun nodeNumToDefaultId(n: Int): String = "!%08x".format(n)
|
||||
fun nodeNumToDefaultId(n: Int): String = formatString("!%08x", n)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun idToDefaultNodeNum(id: String?): Int? = runCatching { id?.toLong(16)?.toInt() }.getOrNull()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import okio.ByteString
|
|||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.GPSFormat
|
||||
import org.meshtastic.core.common.util.bearing
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import org.meshtastic.core.common.util.latLongToMeter
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
|
|
@ -143,20 +144,20 @@ data class Node(
|
|||
val temp =
|
||||
if ((temperature ?: 0f) != 0f) {
|
||||
if (isFahrenheit) {
|
||||
"%.1f°F".format(celsiusToFahrenheit(temperature ?: 0f))
|
||||
formatString("%.1f°F", celsiusToFahrenheit(temperature ?: 0f))
|
||||
} else {
|
||||
"%.1f°C".format(temperature)
|
||||
formatString("%.1f°C", temperature)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val humidity = if ((relative_humidity ?: 0f) != 0f) "%.0f%%".format(relative_humidity) else null
|
||||
val humidity = if ((relative_humidity ?: 0f) != 0f) formatString("%.0f%%", relative_humidity) else null
|
||||
val soilTemperatureStr =
|
||||
if ((soil_temperature ?: 0f) != 0f) {
|
||||
if (isFahrenheit) {
|
||||
"%.1f°F".format(celsiusToFahrenheit(soil_temperature ?: 0f))
|
||||
formatString("%.1f°F", celsiusToFahrenheit(soil_temperature ?: 0f))
|
||||
} else {
|
||||
"%.1f°C".format(soil_temperature)
|
||||
formatString("%.1f°C", soil_temperature)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
|
|
@ -164,12 +165,12 @@ data class Node(
|
|||
val soilMoistureRange = 0..100
|
||||
val soilMoisture =
|
||||
if ((soil_moisture ?: Int.MIN_VALUE) in soilMoistureRange && (soil_temperature ?: 0f) != 0f) {
|
||||
"%d%%".format(soil_moisture)
|
||||
formatString("%d%%", soil_moisture)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val voltage = if ((this.voltage ?: 0f) != 0f) "%.2fV".format(this.voltage) else null
|
||||
val current = if ((current ?: 0f) != 0f) "%.1fmA".format(current) else null
|
||||
val voltage = if ((this.voltage ?: 0f) != 0f) formatString("%.2fV", this.voltage) else null
|
||||
val current = if ((current ?: 0f) != 0f) formatString("%.1fmA", current) else null
|
||||
val iaq = if ((iaq ?: 0) != 0) "IAQ: $iaq" else null
|
||||
|
||||
return listOfNotNull(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
import org.meshtastic.core.model.util.TimeConstants.HOURS_PER_DAY
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
|
|
@ -46,3 +48,16 @@ fun formatUptime(seconds: Int): String {
|
|||
.joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the remaining mute time in days and hours.
|
||||
*
|
||||
* @param remainingMillis The remaining time in milliseconds
|
||||
* @return Pair of (days, hours), where days is Int and hours is Double
|
||||
*/
|
||||
fun formatMuteRemainingTime(remainingMillis: Long): Pair<Int, Double> {
|
||||
val duration = remainingMillis.milliseconds
|
||||
if (duration <= kotlin.time.Duration.ZERO) return 0 to 0.0
|
||||
val totalHours = duration.toDouble(kotlin.time.DurationUnit.HOURS)
|
||||
return (totalHours / HOURS_PER_DAY).toInt() to (totalHours % HOURS_PER_DAY)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
package org.meshtastic.core.model.util
|
||||
|
||||
import org.meshtastic.core.common.util.MeasurementSystem
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import org.meshtastic.core.common.util.getSystemMeasurementSystem
|
||||
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
|
||||
|
||||
|
|
@ -49,12 +50,15 @@ fun Int.metersIn(system: DisplayUnits): Float {
|
|||
return this.metersIn(unit)
|
||||
}
|
||||
|
||||
fun Float.toString(unit: DistanceUnit): String = if (unit in setOf(DistanceUnit.METER, DistanceUnit.FOOT)) {
|
||||
"%.0f %s"
|
||||
} else {
|
||||
"%.1f %s"
|
||||
fun Float.toString(unit: DistanceUnit): String {
|
||||
val pattern =
|
||||
if (unit in setOf(DistanceUnit.METER, DistanceUnit.FOOT)) {
|
||||
"%.0f %s"
|
||||
} else {
|
||||
"%.1f %s"
|
||||
}
|
||||
return formatString(pattern, this, unit.symbol)
|
||||
}
|
||||
.format(this, unit.symbol)
|
||||
|
||||
fun Float.toString(system: DisplayUnits): String {
|
||||
val unit =
|
||||
|
|
@ -81,14 +85,14 @@ fun Int.toDistanceString(system: DisplayUnits): String {
|
|||
|
||||
@Suppress("MagicNumber")
|
||||
fun Float.toSpeedString(system: DisplayUnits): String = if (system == DisplayUnits.METRIC) {
|
||||
"%.0f km/h".format(this * 3.6)
|
||||
formatString("%.0f km/h", this * 3.6)
|
||||
} else {
|
||||
"%.0f mph".format(this * 2.23694f)
|
||||
formatString("%.0f mph", this * 2.23694f)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun Float.toSmallDistanceString(system: DisplayUnits): String = if (system == DisplayUnits.IMPERIAL) {
|
||||
"%.2f in".format(this / 25.4f)
|
||||
formatString("%.2f in", this / 25.4f)
|
||||
} else {
|
||||
"%.0f mm".format(this)
|
||||
formatString("%.0f mm", this)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ private fun decodeSharedContactData(data: String): SharedContact {
|
|||
sanitized.decodeBase64() ?: throw IllegalArgumentException("Invalid Base64 string")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw MalformedMeshtasticUrlException(
|
||||
"Failed to Base64 decode SharedContact data ($data): ${e.javaClass.simpleName}: ${e.message}",
|
||||
"Failed to Base64 decode SharedContact data ($data): ${e::class.simpleName}: ${e.message}",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ private fun decodeSharedContactData(data: String): SharedContact {
|
|||
SharedContact.ADAPTER.decode(decodedBytes)
|
||||
} catch (e: Exception) {
|
||||
throw MalformedMeshtasticUrlException(
|
||||
"Failed to proto decode SharedContact: ${e.javaClass.simpleName}: ${e.message}",
|
||||
"Failed to proto decode SharedContact: ${e::class.simpleName}: ${e.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.core.model.util
|
||||
|
||||
/** No-op stubs for core:model on iOS. */
|
||||
actual fun getShortDateTime(time: Long): String = ""
|
||||
|
||||
actual fun platformRandomBytes(size: Int): ByteArray = ByteArray(size)
|
||||
|
||||
actual object SfppHasher {
|
||||
actual fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray = ByteArray(32)
|
||||
}
|
||||
|
|
@ -22,8 +22,6 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
android { namespace = "org.meshtastic.core.navigation" }
|
||||
|
||||
sourceSets {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright (c) 2025-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.core.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.savedstate.serialization.SavedStateConfiguration
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
|
||||
/**
|
||||
* Shared polymorphic serialization configuration for Navigation 3 saved-state support. Registers all route types used
|
||||
* across Android and Desktop navigation graphs.
|
||||
*/
|
||||
val MeshtasticNavSavedStateConfig = SavedStateConfiguration {
|
||||
serializersModule = SerializersModule {
|
||||
polymorphic(NavKey::class) {
|
||||
// Nodes
|
||||
subclass(NodesRoutes.NodesGraph::class, NodesRoutes.NodesGraph.serializer())
|
||||
subclass(NodesRoutes.Nodes::class, NodesRoutes.Nodes.serializer())
|
||||
subclass(NodesRoutes.NodeDetailGraph::class, NodesRoutes.NodeDetailGraph.serializer())
|
||||
subclass(NodesRoutes.NodeDetail::class, NodesRoutes.NodeDetail.serializer())
|
||||
|
||||
// Node detail sub-screens
|
||||
subclass(NodeDetailRoutes.DeviceMetrics::class, NodeDetailRoutes.DeviceMetrics.serializer())
|
||||
subclass(NodeDetailRoutes.NodeMap::class, NodeDetailRoutes.NodeMap.serializer())
|
||||
subclass(NodeDetailRoutes.PositionLog::class, NodeDetailRoutes.PositionLog.serializer())
|
||||
subclass(NodeDetailRoutes.EnvironmentMetrics::class, NodeDetailRoutes.EnvironmentMetrics.serializer())
|
||||
subclass(NodeDetailRoutes.SignalMetrics::class, NodeDetailRoutes.SignalMetrics.serializer())
|
||||
subclass(NodeDetailRoutes.PowerMetrics::class, NodeDetailRoutes.PowerMetrics.serializer())
|
||||
subclass(NodeDetailRoutes.TracerouteLog::class, NodeDetailRoutes.TracerouteLog.serializer())
|
||||
subclass(NodeDetailRoutes.TracerouteMap::class, NodeDetailRoutes.TracerouteMap.serializer())
|
||||
subclass(NodeDetailRoutes.HostMetricsLog::class, NodeDetailRoutes.HostMetricsLog.serializer())
|
||||
subclass(NodeDetailRoutes.PaxMetrics::class, NodeDetailRoutes.PaxMetrics.serializer())
|
||||
subclass(NodeDetailRoutes.NeighborInfoLog::class, NodeDetailRoutes.NeighborInfoLog.serializer())
|
||||
|
||||
// Conversations
|
||||
subclass(ContactsRoutes.ContactsGraph::class, ContactsRoutes.ContactsGraph.serializer())
|
||||
subclass(ContactsRoutes.Contacts::class, ContactsRoutes.Contacts.serializer())
|
||||
subclass(ContactsRoutes.Messages::class, ContactsRoutes.Messages.serializer())
|
||||
subclass(ContactsRoutes.Share::class, ContactsRoutes.Share.serializer())
|
||||
subclass(ContactsRoutes.QuickChat::class, ContactsRoutes.QuickChat.serializer())
|
||||
|
||||
// Map
|
||||
subclass(MapRoutes.Map::class, MapRoutes.Map.serializer())
|
||||
|
||||
// Firmware
|
||||
subclass(FirmwareRoutes.FirmwareGraph::class, FirmwareRoutes.FirmwareGraph.serializer())
|
||||
subclass(FirmwareRoutes.FirmwareUpdate::class, FirmwareRoutes.FirmwareUpdate.serializer())
|
||||
|
||||
// Settings
|
||||
subclass(SettingsRoutes.SettingsGraph::class, SettingsRoutes.SettingsGraph.serializer())
|
||||
subclass(SettingsRoutes.Settings::class, SettingsRoutes.Settings.serializer())
|
||||
subclass(SettingsRoutes.DeviceConfiguration::class, SettingsRoutes.DeviceConfiguration.serializer())
|
||||
subclass(SettingsRoutes.ModuleConfiguration::class, SettingsRoutes.ModuleConfiguration.serializer())
|
||||
subclass(SettingsRoutes.Administration::class, SettingsRoutes.Administration.serializer())
|
||||
|
||||
// Settings - Config routes
|
||||
subclass(SettingsRoutes.User::class, SettingsRoutes.User.serializer())
|
||||
subclass(SettingsRoutes.ChannelConfig::class, SettingsRoutes.ChannelConfig.serializer())
|
||||
subclass(SettingsRoutes.Device::class, SettingsRoutes.Device.serializer())
|
||||
subclass(SettingsRoutes.Position::class, SettingsRoutes.Position.serializer())
|
||||
subclass(SettingsRoutes.Power::class, SettingsRoutes.Power.serializer())
|
||||
subclass(SettingsRoutes.Network::class, SettingsRoutes.Network.serializer())
|
||||
subclass(SettingsRoutes.Display::class, SettingsRoutes.Display.serializer())
|
||||
subclass(SettingsRoutes.LoRa::class, SettingsRoutes.LoRa.serializer())
|
||||
subclass(SettingsRoutes.Bluetooth::class, SettingsRoutes.Bluetooth.serializer())
|
||||
subclass(SettingsRoutes.Security::class, SettingsRoutes.Security.serializer())
|
||||
|
||||
// Settings - Module routes
|
||||
subclass(SettingsRoutes.MQTT::class, SettingsRoutes.MQTT.serializer())
|
||||
subclass(SettingsRoutes.Serial::class, SettingsRoutes.Serial.serializer())
|
||||
subclass(SettingsRoutes.ExtNotification::class, SettingsRoutes.ExtNotification.serializer())
|
||||
subclass(SettingsRoutes.StoreForward::class, SettingsRoutes.StoreForward.serializer())
|
||||
subclass(SettingsRoutes.RangeTest::class, SettingsRoutes.RangeTest.serializer())
|
||||
subclass(SettingsRoutes.Telemetry::class, SettingsRoutes.Telemetry.serializer())
|
||||
subclass(SettingsRoutes.CannedMessage::class, SettingsRoutes.CannedMessage.serializer())
|
||||
subclass(SettingsRoutes.Audio::class, SettingsRoutes.Audio.serializer())
|
||||
subclass(SettingsRoutes.RemoteHardware::class, SettingsRoutes.RemoteHardware.serializer())
|
||||
subclass(SettingsRoutes.NeighborInfo::class, SettingsRoutes.NeighborInfo.serializer())
|
||||
subclass(SettingsRoutes.AmbientLighting::class, SettingsRoutes.AmbientLighting.serializer())
|
||||
subclass(SettingsRoutes.DetectionSensor::class, SettingsRoutes.DetectionSensor.serializer())
|
||||
subclass(SettingsRoutes.Paxcounter::class, SettingsRoutes.Paxcounter.serializer())
|
||||
subclass(SettingsRoutes.StatusMessage::class, SettingsRoutes.StatusMessage.serializer())
|
||||
subclass(SettingsRoutes.TrafficManagement::class, SettingsRoutes.TrafficManagement.serializer())
|
||||
subclass(SettingsRoutes.TAK::class, SettingsRoutes.TAK.serializer())
|
||||
|
||||
// Settings - Advanced routes
|
||||
subclass(SettingsRoutes.CleanNodeDb::class, SettingsRoutes.CleanNodeDb.serializer())
|
||||
subclass(SettingsRoutes.DebugPanel::class, SettingsRoutes.DebugPanel.serializer())
|
||||
subclass(SettingsRoutes.About::class, SettingsRoutes.About.serializer())
|
||||
subclass(SettingsRoutes.FilterSettings::class, SettingsRoutes.FilterSettings.serializer())
|
||||
|
||||
// Channels
|
||||
subclass(ChannelsRoutes.ChannelsGraph::class, ChannelsRoutes.ChannelsGraph.serializer())
|
||||
subclass(ChannelsRoutes.Channels::class, ChannelsRoutes.Channels.serializer())
|
||||
|
||||
// Connections
|
||||
subclass(ConnectionsRoutes.ConnectionsGraph::class, ConnectionsRoutes.ConnectionsGraph.serializer())
|
||||
subclass(ConnectionsRoutes.Connections::class, ConnectionsRoutes.Connections.serializer())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,6 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "org.meshtastic.core.network"
|
||||
|
|
|
|||
|
|
@ -313,8 +313,8 @@ class MockInterface(private val service: RadioInterfaceService, val address: Str
|
|||
user =
|
||||
User(
|
||||
id = DataPacket.nodeNumToDefaultId(numIn),
|
||||
long_name = "Sim " + Integer.toHexString(numIn),
|
||||
short_name = getInitials("Sim " + Integer.toHexString(numIn)),
|
||||
long_name = "Sim " + numIn.toString(16),
|
||||
short_name = getInitials("Sim " + numIn.toString(16)),
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
),
|
||||
position =
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "org.meshtastic.core.nfc"
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
android {
|
||||
namespace = "org.meshtastic.core.prefs"
|
||||
androidResources.enable = false
|
||||
|
|
|
|||
|
|
@ -24,9 +24,6 @@ plugins {
|
|||
apply(from = rootProject.file("gradle/publishing.gradle.kts"))
|
||||
|
||||
kotlin {
|
||||
// Keep jvm() for desktop/server consumers
|
||||
jvm()
|
||||
|
||||
// Override minSdk for ATAK compatibility (standard is 26)
|
||||
android { minSdk = 21 }
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android { androidResources.enable = false }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.core.repository
|
||||
|
||||
/** No-op stub for Location on iOS. */
|
||||
actual class Location
|
||||
|
|
@ -29,7 +29,10 @@ kotlin {
|
|||
withHostTest { isIncludeAndroidResources = true }
|
||||
}
|
||||
|
||||
sourceSets { commonTest.dependencies { implementation(kotlin("test")) } }
|
||||
sourceSets {
|
||||
commonMain.dependencies { implementation(projects.core.common) }
|
||||
commonTest.dependencies { implementation(kotlin("test")) }
|
||||
}
|
||||
}
|
||||
|
||||
compose.resources {
|
||||
|
|
|
|||
|
|
@ -25,12 +25,22 @@ fun getString(stringResource: StringResource): String = runBlocking { composeGet
|
|||
|
||||
/** Retrieves a formatted string from the [StringResource] in a blocking manner. */
|
||||
fun getString(stringResource: StringResource, vararg formatArgs: Any): String = runBlocking {
|
||||
val pattern = composeGetString(stringResource)
|
||||
if (formatArgs.isNotEmpty()) {
|
||||
val resolvedArgs =
|
||||
formatArgs
|
||||
.map { arg ->
|
||||
if (arg is StringResource) {
|
||||
composeGetString(arg)
|
||||
} else {
|
||||
arg
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
|
||||
if (resolvedArgs.isNotEmpty()) {
|
||||
@Suppress("SpreadOperator")
|
||||
pattern.format(*formatArgs)
|
||||
composeGetString(stringResource, *resolvedArgs)
|
||||
} else {
|
||||
pattern
|
||||
composeGetString(stringResource)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,11 +60,10 @@ suspend fun getStringSuspend(stringResource: StringResource, vararg formatArgs:
|
|||
}
|
||||
.toTypedArray()
|
||||
|
||||
val pattern = composeGetString(stringResource)
|
||||
return if (resolvedArgs.isNotEmpty()) {
|
||||
@Suppress("SpreadOperator")
|
||||
pattern.format(*resolvedArgs)
|
||||
composeGetString(stringResource, *resolvedArgs)
|
||||
} else {
|
||||
pattern
|
||||
composeGetString(stringResource)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "org.meshtastic.core.service"
|
||||
|
|
@ -45,6 +43,7 @@ kotlin {
|
|||
implementation(projects.core.proto)
|
||||
|
||||
implementation(libs.jetbrains.lifecycle.runtime)
|
||||
implementation(libs.kotlinx.atomicfu)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kermit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import kotlinx.coroutines.flow.catch
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.koin.core.annotation.Named
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
|
|
@ -100,7 +102,7 @@ class SharedRadioInterfaceService(
|
|||
private var radioIf: RadioTransport? = null
|
||||
private var isStarted = false
|
||||
|
||||
@Volatile private var listenersInitialized = false
|
||||
private val listenersInitialized = kotlinx.atomicfu.atomic(false)
|
||||
private var heartbeatJob: kotlinx.coroutines.Job? = null
|
||||
private var lastHeartbeatMillis = 0L
|
||||
|
||||
|
|
@ -108,42 +110,46 @@ class SharedRadioInterfaceService(
|
|||
private const val HEARTBEAT_INTERVAL_MILLIS = 30 * 1000L
|
||||
}
|
||||
|
||||
private val initLock = Mutex()
|
||||
|
||||
private fun initStateListeners() {
|
||||
if (listenersInitialized) return
|
||||
synchronized(this) {
|
||||
if (listenersInitialized) return
|
||||
listenersInitialized = true
|
||||
if (listenersInitialized.value) return
|
||||
processLifecycle.coroutineScope.launch {
|
||||
initLock.withLock {
|
||||
if (listenersInitialized.value) return@withLock
|
||||
listenersInitialized.value = true
|
||||
|
||||
radioPrefs.devAddr
|
||||
.onEach { addr ->
|
||||
if (_currentDeviceAddressFlow.value != addr) {
|
||||
_currentDeviceAddressFlow.value = addr
|
||||
startInterface()
|
||||
radioPrefs.devAddr
|
||||
.onEach { addr ->
|
||||
if (_currentDeviceAddressFlow.value != addr) {
|
||||
_currentDeviceAddressFlow.value = addr
|
||||
startInterface()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
|
||||
bluetoothRepository.state
|
||||
.onEach { state ->
|
||||
if (state.enabled) {
|
||||
startInterface()
|
||||
} else if (getBondedDeviceAddress()?.startsWith(InterfaceId.BLUETOOTH.id) == true) {
|
||||
stopInterface()
|
||||
bluetoothRepository.state
|
||||
.onEach { state ->
|
||||
if (state.enabled) {
|
||||
startInterface()
|
||||
} else if (getBondedDeviceAddress()?.startsWith(InterfaceId.BLUETOOTH.id) == true) {
|
||||
stopInterface()
|
||||
}
|
||||
}
|
||||
}
|
||||
.catch { Logger.e(it) { "bluetoothRepository.state flow crashed!" } }
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
.catch { Logger.e(it) { "bluetoothRepository.state flow crashed!" } }
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
|
||||
networkRepository.networkAvailable
|
||||
.onEach { state ->
|
||||
if (state) {
|
||||
startInterface()
|
||||
} else if (getBondedDeviceAddress()?.startsWith(InterfaceId.TCP.id) == true) {
|
||||
stopInterface()
|
||||
networkRepository.networkAvailable
|
||||
.onEach { state ->
|
||||
if (state) {
|
||||
startInterface()
|
||||
} else if (getBondedDeviceAddress()?.startsWith(InterfaceId.TCP.id) == true) {
|
||||
stopInterface()
|
||||
}
|
||||
}
|
||||
}
|
||||
.catch { Logger.e(it) { "networkRepository.networkAvailable flow crashed!" } }
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
.catch { Logger.e(it) { "networkRepository.networkAvailable flow crashed!" } }
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@
|
|||
plugins { alias(libs.plugins.meshtastic.kmp.library) }
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "org.meshtastic.core.testing"
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ plugins {
|
|||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
android {
|
||||
namespace = "org.meshtastic.core.ui"
|
||||
androidResources.enable = false
|
||||
|
|
@ -48,13 +46,15 @@ kotlin {
|
|||
implementation(libs.compose.multiplatform.materialIconsExtended)
|
||||
implementation(libs.compose.multiplatform.ui)
|
||||
implementation(libs.compose.multiplatform.foundation)
|
||||
implementation(libs.compose.multiplatform.ui.tooling)
|
||||
api(libs.compose.multiplatform.ui.tooling.preview)
|
||||
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
implementation(libs.qrcode.kotlin)
|
||||
}
|
||||
|
||||
val jvmAndroidMain by getting { dependencies { implementation(libs.compose.multiplatform.ui.tooling) } }
|
||||
|
||||
androidMain.dependencies { implementation(libs.androidx.activity.compose) }
|
||||
|
||||
commonTest.dependencies {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.painter.ColorPainter
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
@Composable
|
||||
fun <T : Enum<T>> DropDownPreference(
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ fun EditTextPreference(
|
|||
isError = isError,
|
||||
onValueChange = {
|
||||
if (maxSize > 0) {
|
||||
if (it.toByteArray().size <= maxSize) {
|
||||
if (it.encodeToByteArray().size <= maxSize) {
|
||||
onValueChanged(it)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -255,7 +255,7 @@ fun EditTextPreference(
|
|||
if (maxSize > 0 && isFocused) {
|
||||
Box(contentAlignment = Alignment.BottomEnd, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "${value.toByteArray().size}/$maxSize",
|
||||
text = "${value.encodeToByteArray().size}/$maxSize",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(end = 8.dp, bottom = 4.dp),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.bad
|
||||
import org.meshtastic.core.resources.fair
|
||||
|
|
@ -153,7 +154,7 @@ fun Snr(snr: Float, modifier: Modifier = Modifier) {
|
|||
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = "%s %.2fdB".format(stringResource(Res.string.snr), snr),
|
||||
text = formatString("%s %.2fdB", stringResource(Res.string.snr), snr),
|
||||
color = color,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
|
|
@ -171,7 +172,7 @@ fun Rssi(rssi: Int, modifier: Modifier = Modifier) {
|
|||
}
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = "%s %ddBm".format(stringResource(Res.string.rssi), rssi),
|
||||
text = formatString("%s %ddBm", stringResource(Res.string.rssi), rssi),
|
||||
color = color,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.unknown
|
||||
import org.meshtastic.core.ui.icon.BatteryEmpty
|
||||
|
|
@ -60,7 +61,7 @@ fun MaterialBatteryInfo(
|
|||
voltage: Float? = null,
|
||||
contentColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
val levelString = FORMAT.format(level)
|
||||
val levelString = formatString(FORMAT, level)
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
|
|
@ -130,7 +131,7 @@ fun MaterialBatteryInfo(
|
|||
?.takeIf { it > 0 }
|
||||
?.let {
|
||||
Text(
|
||||
text = "%.2fV".format(it),
|
||||
text = formatString("%.2fV", it),
|
||||
color = contentColor.copy(alpha = 0.8f),
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@
|
|||
*/
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
/**
|
||||
* Event emitted when a user re-presses a bottom navigation destination that should trigger a scroll-to-top behaviour on
|
||||
* the corresponding screen.
|
||||
|
|
@ -25,3 +29,5 @@ sealed class ScrollToTopEvent {
|
|||
|
||||
data object ConversationsTabPressed : ScrollToTopEvent()
|
||||
}
|
||||
|
||||
@Composable fun rememberScrollToTopEvents(): MutableSharedFlow<ScrollToTopEvent> = remember { MutableSharedFlow() }
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.signal_quality
|
||||
|
|
@ -63,7 +64,7 @@ fun SignalInfo(
|
|||
tint = signalColor,
|
||||
)
|
||||
Text(
|
||||
text = "%.1fdB · %ddBm · %s".format(node.snr, node.rssi, stringResource(quality.nameRes)),
|
||||
text = formatString("%.1fdB · %ddBm · %s", node.snr, node.rssi, stringResource(quality.nameRes)),
|
||||
style =
|
||||
MaterialTheme.typography.labelSmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
|
|
@ -297,7 +297,7 @@ fun ScannedQrCodeDialog(
|
|||
}
|
||||
}
|
||||
|
||||
@PreviewScreenSizes
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun ScannedQrCodeDialogPreview() {
|
||||
ScannedQrCodeDialog(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.core.ui.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable actual fun rememberTimeTickWithLifecycle(): Long = 0L
|
||||
|
||||
internal actual fun <T : Enum<T>> enumEntriesOf(selectedItem: T): List<T> = emptyList()
|
||||
|
||||
internal actual fun Enum<*>.isDeprecatedEnumEntry(): Boolean = false
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.core.ui.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable actual fun dynamicColorScheme(darkTheme: Boolean): ColorScheme? = null
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.core.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
actual fun createClipEntry(text: String, label: String): ClipEntry =
|
||||
throw UnsupportedOperationException("ClipEntry instantiation not supported on iOS stub")
|
||||
|
||||
actual fun annotatedStringFromHtml(html: String, linkStyles: TextLinkStyles?): AnnotatedString = AnnotatedString(html)
|
||||
|
||||
@Composable actual fun rememberOpenNfcSettings(): () -> Unit = {}
|
||||
|
||||
@Composable actual fun rememberShowToast(): suspend (String) -> Unit = { _ -> }
|
||||
|
||||
@Composable actual fun rememberShowToastResource(): suspend (StringResource) -> Unit = { _ -> }
|
||||
|
||||
@Composable actual fun rememberOpenMap(): (latitude: Double, longitude: Double, label: String) -> Unit = { _, _, _ -> }
|
||||
|
||||
@Composable actual fun rememberOpenUrl(): (url: String) -> Unit = { _ -> }
|
||||
|
||||
@Composable actual fun SetScreenBrightness(brightness: Float) {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue