feat: Implement iOS support and unify Compose Multiplatform infrastructure (#4876)

This commit is contained in:
James Rich 2026-03-21 18:19:13 -05:00 committed by GitHub
parent f04924ded5
commit d136b162a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
170 changed files with 2208 additions and 2432 deletions

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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()

View file

@ -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(

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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}",
)
}
}

View file

@ -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)
}