mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: introduce Desktop target and expand Kotlin Multiplatform (KMP) architecture (#4761)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
f4364cff9a
commit
ac6bb5479b
386 changed files with 17089 additions and 4590 deletions
|
|
@ -24,7 +24,6 @@ import java.text.DateFormat
|
|||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
private val DAY_DURATION = 24.hours
|
||||
|
|
@ -48,51 +47,6 @@ fun getShortDate(time: Long): String? {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a short string representing the time if it's within the last 24 hours, otherwise returns a combined short
|
||||
* date/time string.
|
||||
*
|
||||
* @param time The time in milliseconds
|
||||
* @return Formatted date/time string
|
||||
*/
|
||||
fun getShortDateTime(time: Long): String {
|
||||
val instant = time.toInstant()
|
||||
val isWithin24Hours = (nowInstant - instant) <= DAY_DURATION
|
||||
|
||||
return if (isWithin24Hours) {
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(instant.toDate())
|
||||
} else {
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(instant.toDate())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration in seconds as a human-readable uptime string (e.g., "1d 2h 3m 4s").
|
||||
*
|
||||
* @param seconds The duration in seconds.
|
||||
* @return A formatted uptime string.
|
||||
*/
|
||||
fun formatUptime(seconds: Int): String = formatUptime(seconds.toLong())
|
||||
|
||||
/**
|
||||
* Formats a duration in seconds as a human-readable uptime string (e.g., "1d 2h 3m 4s").
|
||||
*
|
||||
* @param seconds The duration in seconds.
|
||||
* @return A formatted uptime string.
|
||||
*/
|
||||
private fun formatUptime(seconds: Long): String {
|
||||
if (seconds == 0L) return "0s"
|
||||
return seconds.seconds.toComponents { days, hours, minutes, secs, _ ->
|
||||
listOfNotNull(
|
||||
"${days}d".takeIf { days > 0 },
|
||||
"${hours}h".takeIf { hours > 0 },
|
||||
"${minutes}m".takeIf { minutes > 0 },
|
||||
"${secs}s".takeIf { secs > 0 },
|
||||
)
|
||||
.joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the remaining mute time in days and hours.
|
||||
*
|
||||
|
|
@ -80,7 +80,6 @@ data class Channel(val settings: ChannelSettings = default.settings, val loraCon
|
|||
ModemPreset.LONG_MODERATE -> "LongMod"
|
||||
ModemPreset.VERY_LONG_SLOW -> "VLongSlow"
|
||||
ModemPreset.LONG_TURBO -> "LongTurbo"
|
||||
else -> "Invalid"
|
||||
}
|
||||
} else {
|
||||
"Custom"
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ internal fun LoRaConfig.channelNum(primaryName: String): Int = when {
|
|||
}
|
||||
|
||||
internal fun LoRaConfig.radioFreq(channelNum: Int): Float {
|
||||
if ((override_frequency ?: 0f) != 0f) return (override_frequency ?: 0f) + (frequency_offset ?: 0f)
|
||||
if (override_frequency != 0f) return override_frequency + frequency_offset
|
||||
val regionInfo = RegionInfo.fromRegionCode(region)
|
||||
return if (regionInfo != null) {
|
||||
(regionInfo.freqStart + bandwidth(regionInfo) / 2) + (channelNum - 1) * bandwidth(regionInfo)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
/** Represent the different ways a device can connect to the client. */
|
||||
enum class DeviceType {
|
||||
BLE,
|
||||
TCP,
|
||||
USB,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromAddress(address: String): DeviceType? = when (address.firstOrNull()) {
|
||||
'x' -> BLE
|
||||
's' -> USB
|
||||
't' -> TCP
|
||||
'm' -> USB // Treat mock as USB for UI purposes
|
||||
'n' -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.MyNodeInfo
|
||||
import org.meshtastic.proto.NodeInfo
|
||||
import org.meshtastic.proto.Position
|
||||
|
||||
/**
|
||||
* Represents a log entry in shared repository/domain code.
|
||||
*
|
||||
* Logs are used for auditing radio traffic, telemetry history, and debugging.
|
||||
*/
|
||||
@Suppress("EmptyCatchBlock", "SwallowedException", "ConstructorParameterNaming")
|
||||
data class MeshLog(
|
||||
val uuid: String,
|
||||
val message_type: String,
|
||||
val received_date: Long,
|
||||
val raw_message: String,
|
||||
val fromNum: Int = 0,
|
||||
val portNum: Int = 0,
|
||||
val fromRadio: FromRadio = FromRadio(),
|
||||
) {
|
||||
val meshPacket = fromRadio.packet
|
||||
|
||||
val nodeInfo: NodeInfo?
|
||||
get() = fromRadio.node_info
|
||||
|
||||
val myNodeInfo: MyNodeInfo?
|
||||
get() = fromRadio.my_info
|
||||
|
||||
val position: Position?
|
||||
get() =
|
||||
fromRadio.packet?.decoded?.payload?.let {
|
||||
if (fromRadio.packet?.decoded?.portnum == org.meshtastic.proto.PortNum.POSITION_APP) {
|
||||
Position.ADAPTER.decodeOrNull(it, Logger)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: nodeInfo?.position
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The node number used to represent the local node in the logs.
|
||||
*
|
||||
* Using 0 instead of the actual node number ensures log continuity even if the radio hardware or local ID
|
||||
* changes.
|
||||
*/
|
||||
const val NODE_NUM_LOCAL = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ data class Node(
|
|||
get() = user.hw_model == HardwareModel.UNSET
|
||||
|
||||
val hasPKC
|
||||
get() = (publicKey ?: user.public_key)?.size?.let { it > 0 } == true
|
||||
get() = (publicKey ?: user.public_key).size > 0
|
||||
|
||||
val mismatchKey
|
||||
get() = (publicKey ?: user.public_key) == ERROR_BYTE_STRING
|
||||
|
|
@ -184,8 +184,7 @@ data class Node(
|
|||
)
|
||||
}
|
||||
|
||||
private fun Paxcount.getDisplayString() =
|
||||
"PAX: ${(ble ?: 0) + (wifi ?: 0)} (B:${ble ?: 0}/W:${wifi ?: 0})".takeIf { (ble ?: 0) != 0 || (wifi ?: 0) != 0 }
|
||||
private fun Paxcount.getDisplayString() = "PAX: ${ble + wifi} (B:$ble/W:$wifi)".takeIf { ble != 0 || wifi != 0 }
|
||||
|
||||
fun getTelemetryStrings(isFahrenheit: Boolean = false): List<String> =
|
||||
environmentMetrics.getDisplayStrings(isFahrenheit)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ data class MeshUser(
|
|||
/** Create our model object from a protobuf. */
|
||||
constructor(
|
||||
p: org.meshtastic.proto.User,
|
||||
) : this(p.id, p.long_name ?: "", p.short_name ?: "", p.hw_model, p.is_licensed, p.role.value)
|
||||
) : this(p.id, p.long_name, p.short_name, p.hw_model, p.is_licensed, p.role.value)
|
||||
|
||||
/**
|
||||
* a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot or null
|
||||
|
|
@ -100,10 +100,10 @@ data class Position(
|
|||
degD(position.longitude_i ?: 0),
|
||||
position.altitude ?: 0,
|
||||
if (position.time != 0) position.time else defaultTime,
|
||||
position.sats_in_view ?: 0,
|
||||
position.sats_in_view,
|
||||
position.ground_speed ?: 0,
|
||||
position.ground_track ?: 0,
|
||||
position.precision_bits ?: 0,
|
||||
position.precision_bits,
|
||||
)
|
||||
|
||||
// / @return distance in meters to some other node (or null if unknown)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.model.util
|
||||
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Returns a short string representing the time if it's within the last 24 hours, otherwise returns a combined short
|
||||
* date/time string.
|
||||
*
|
||||
* @param time The time in milliseconds
|
||||
* @return Formatted date/time string
|
||||
*/
|
||||
expect fun getShortDateTime(time: Long): String
|
||||
|
||||
/**
|
||||
* Formats a duration in seconds as a human-readable uptime string (e.g., "1d 2h 3m 4s").
|
||||
*
|
||||
* @param seconds The duration in seconds.
|
||||
* @return A formatted uptime string.
|
||||
*/
|
||||
fun formatUptime(seconds: Int): String {
|
||||
val secs = seconds.toLong()
|
||||
if (secs == 0L) return "0s"
|
||||
return secs.seconds.toComponents { days, hours, minutes, s, _ ->
|
||||
listOfNotNull(
|
||||
"${days}d".takeIf { days > 0 },
|
||||
"${hours}h".takeIf { hours > 0 },
|
||||
"${minutes}m".takeIf { minutes > 0 },
|
||||
"${s}s".takeIf { s > 0 },
|
||||
)
|
||||
.joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
|
@ -16,4 +16,11 @@
|
|||
*/
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
expect val isDebug: Boolean
|
||||
/**
|
||||
* Whether the app is running in debug mode.
|
||||
*
|
||||
* This is a compile-time constant for the shared module. For runtime debug detection, use
|
||||
* [org.meshtastic.core.common.BuildConfigProvider.isDebug] from DI instead.
|
||||
*/
|
||||
@Suppress("ktlint:standard:property-naming", "TopLevelPropertyNaming")
|
||||
const val isDebug: Boolean = false
|
||||
|
|
|
|||
|
|
@ -16,4 +16,7 @@
|
|||
*/
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
actual val isDebug: Boolean = false
|
||||
/** Computes SFPP (Store-Forward-Plus-Plus) message hashes for deduplication. */
|
||||
expect object SfppHasher {
|
||||
fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray
|
||||
}
|
||||
|
|
@ -27,4 +27,5 @@ object TimeConstants {
|
|||
val TWO_DAYS = 2.days
|
||||
|
||||
const val HOURS_PER_DAY = 24
|
||||
const val MS_PER_SEC = 1000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.model.util
|
||||
|
||||
import org.meshtastic.core.common.util.nowInstant
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import java.text.DateFormat
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
private val DAY_DURATION = 24.hours
|
||||
|
||||
/**
|
||||
* Returns a short string representing the time if it's within the last 24 hours, otherwise returns a combined short
|
||||
* date/time string.
|
||||
*
|
||||
* @param time The time in milliseconds
|
||||
* @return Formatted date/time string
|
||||
*/
|
||||
actual fun getShortDateTime(time: Long): String {
|
||||
val instant = time.toInstant()
|
||||
val isWithin24Hours = (nowInstant - instant) <= DAY_DURATION
|
||||
|
||||
return if (isWithin24Hours) {
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(instant.toDate())
|
||||
} else {
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(instant.toDate())
|
||||
}
|
||||
}
|
||||
|
|
@ -20,11 +20,11 @@ import java.nio.ByteBuffer
|
|||
import java.nio.ByteOrder
|
||||
import java.security.MessageDigest
|
||||
|
||||
object SfppHasher {
|
||||
actual object SfppHasher {
|
||||
private const val HASH_SIZE = 16
|
||||
private const val INT_BYTES = 4
|
||||
|
||||
fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray {
|
||||
actual fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.update(encryptedPayload)
|
||||
digest.update(ByteBuffer.allocate(INT_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(to).array())
|
||||
Loading…
Add table
Add a link
Reference in a new issue