mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Modularize more models/utils (#3182)
This commit is contained in:
parent
5bb3f73e0d
commit
4eba3e9daf
80 changed files with 656 additions and 629 deletions
|
|
@ -1,246 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Generic [Parcel.readParcelable] Android 13 compatibility extension.
|
||||
*/
|
||||
private inline fun <reified T : Parcelable> Parcel.readParcelableCompat(loader: ClassLoader?): T? {
|
||||
return if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
@Suppress("DEPRECATION")
|
||||
readParcelable(loader)
|
||||
} else {
|
||||
readParcelable(loader, T::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
enum class MessageStatus : Parcelable {
|
||||
UNKNOWN, // Not set for this message
|
||||
RECEIVED, // Came in from the mesh
|
||||
QUEUED, // Waiting to send to the mesh as soon as we connect to the device
|
||||
ENROUTE, // Delivered to the radio, but no ACK or NAK received
|
||||
DELIVERED, // We received an ack
|
||||
ERROR // We received back a nak, message not delivered
|
||||
}
|
||||
|
||||
/**
|
||||
* A parcelable version of the protobuf MeshPacket + Data subpacket.
|
||||
*/
|
||||
@Serializable
|
||||
data class DataPacket(
|
||||
var to: String? = ID_BROADCAST, // a nodeID string, or ID_BROADCAST for broadcast
|
||||
val bytes: ByteArray?,
|
||||
val dataType: Int, // A port number for this packet (formerly called DataType, see portnums.proto for new usage instructions)
|
||||
var from: String? = ID_LOCAL, // a nodeID string, or ID_LOCAL for localhost
|
||||
var time: Long = System.currentTimeMillis(), // msecs since 1970
|
||||
var id: Int = 0, // 0 means unassigned
|
||||
var status: MessageStatus? = MessageStatus.UNKNOWN,
|
||||
var hopLimit: Int = 0,
|
||||
var channel: Int = 0, // channel index
|
||||
var wantAck: Boolean = true, // If true, the receiver should send an ack back
|
||||
var hopStart: Int = 0,
|
||||
var snr: Float = 0f,
|
||||
var rssi: Int = 0,
|
||||
var replyId: Int? = null // If this is a reply to a previous message, this is the ID of that message
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* If there was an error with this message, this string describes what was wrong.
|
||||
*/
|
||||
var errorMessage: String? = null
|
||||
|
||||
/**
|
||||
* Syntactic sugar to make it easy to create text messages
|
||||
*/
|
||||
constructor(to: String?, channel: Int, text: String, replyId: Int? = null) : this(
|
||||
to = to,
|
||||
bytes = text.encodeToByteArray(),
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
channel = channel,
|
||||
replyId = replyId ?: 0
|
||||
)
|
||||
|
||||
/**
|
||||
* If this is a text message, return the string, otherwise null
|
||||
*/
|
||||
val text: String?
|
||||
get() = if (dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) {
|
||||
bytes?.decodeToString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val alert: String?
|
||||
get() = if (dataType == Portnums.PortNum.ALERT_APP_VALUE) {
|
||||
bytes?.decodeToString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
constructor(to: String?, channel: Int, waypoint: MeshProtos.Waypoint) : this(
|
||||
to = to,
|
||||
bytes = waypoint.toByteArray(),
|
||||
dataType = Portnums.PortNum.WAYPOINT_APP_VALUE,
|
||||
channel = channel,
|
||||
)
|
||||
|
||||
val waypoint: MeshProtos.Waypoint?
|
||||
get() = if (dataType == Portnums.PortNum.WAYPOINT_APP_VALUE) {
|
||||
MeshProtos.Waypoint.parseFrom(bytes)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val hopsAway: Int
|
||||
get() = if (hopStart == 0 || hopLimit > hopStart) -1 else hopStart - hopLimit
|
||||
|
||||
// Autogenerated comparision, because we have a byte array
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString(),
|
||||
parcel.createByteArray(),
|
||||
parcel.readInt(),
|
||||
parcel.readString(),
|
||||
parcel.readLong(),
|
||||
parcel.readInt(),
|
||||
parcel.readParcelableCompat(MessageStatus::class.java.classLoader),
|
||||
parcel.readInt(),
|
||||
parcel.readInt(),
|
||||
parcel.readInt() == 1,
|
||||
parcel.readInt(),
|
||||
parcel.readFloat(),
|
||||
parcel.readInt(),
|
||||
parcel.readInt().let { if (it == 0) null else it }
|
||||
)
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as DataPacket
|
||||
|
||||
if (from != other.from) return false
|
||||
if (to != other.to) return false
|
||||
if (channel != other.channel) return false
|
||||
if (time != other.time) return false
|
||||
if (id != other.id) return false
|
||||
if (dataType != other.dataType) return false
|
||||
if (!bytes!!.contentEquals(other.bytes!!)) return false
|
||||
if (status != other.status) return false
|
||||
if (hopLimit != other.hopLimit) return false
|
||||
if (wantAck != other.wantAck) return false
|
||||
if (hopStart != other.hopStart) return false
|
||||
if (snr != other.snr) return false
|
||||
if (rssi != other.rssi) return false
|
||||
if (replyId != other.replyId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = from.hashCode()
|
||||
result = 31 * result + to.hashCode()
|
||||
result = 31 * result + time.hashCode()
|
||||
result = 31 * result + id
|
||||
result = 31 * result + dataType
|
||||
result = 31 * result + bytes!!.contentHashCode()
|
||||
result = 31 * result + status.hashCode()
|
||||
result = 31 * result + hopLimit
|
||||
result = 31 * result + channel
|
||||
result = 31 * result + wantAck.hashCode()
|
||||
result = 31 * result + hopStart
|
||||
result = 31 * result + snr.hashCode()
|
||||
result = 31 * result + rssi
|
||||
result = 31 * result + replyId.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(to)
|
||||
parcel.writeByteArray(bytes)
|
||||
parcel.writeInt(dataType)
|
||||
parcel.writeString(from)
|
||||
parcel.writeLong(time)
|
||||
parcel.writeInt(id)
|
||||
parcel.writeParcelable(status, flags)
|
||||
parcel.writeInt(hopLimit)
|
||||
parcel.writeInt(channel)
|
||||
parcel.writeInt(if (wantAck) 1 else 0)
|
||||
parcel.writeInt(hopStart)
|
||||
parcel.writeFloat(snr)
|
||||
parcel.writeInt(rssi)
|
||||
parcel.writeInt(replyId ?: 0)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Update our object from our parcel (used for inout parameters
|
||||
fun readFromParcel(parcel: Parcel) {
|
||||
to = parcel.readString()
|
||||
parcel.createByteArray()
|
||||
parcel.readInt()
|
||||
from = parcel.readString()
|
||||
time = parcel.readLong()
|
||||
id = parcel.readInt()
|
||||
status = parcel.readParcelableCompat(MessageStatus::class.java.classLoader)
|
||||
hopLimit = parcel.readInt()
|
||||
channel = parcel.readInt()
|
||||
wantAck = parcel.readInt() == 1
|
||||
hopStart = parcel.readInt()
|
||||
snr = parcel.readFloat()
|
||||
rssi = parcel.readInt()
|
||||
replyId = parcel.readInt().let { if (it == 0) null else it }
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<DataPacket> {
|
||||
// Special node IDs that can be used for sending messages
|
||||
|
||||
/** the Node ID for broadcast destinations */
|
||||
const val ID_BROADCAST = "^all"
|
||||
|
||||
/** The Node ID for the local node - used for from when sender doesn't know our local node ID */
|
||||
const val ID_LOCAL = "^local"
|
||||
|
||||
// special broadcast address
|
||||
const val NODENUM_BROADCAST = (0xffffffff).toInt()
|
||||
|
||||
// Public-key cryptography (PKC) channel index
|
||||
const val PKC_CHANNEL_INDEX = 8
|
||||
|
||||
fun nodeNumToDefaultId(n: Int): String = "!%08x".format(n)
|
||||
fun idToDefaultNodeNum(id: String?): Int? =
|
||||
runCatching { id?.toLong(16)?.toInt() }.getOrNull()
|
||||
|
||||
override fun createFromParcel(parcel: Parcel): DataPacket {
|
||||
return DataPacket(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<DataPacket?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
// MyNodeInfo sent via special protobuf from radio
|
||||
@Parcelize
|
||||
data class MyNodeInfo(
|
||||
val myNodeNum: Int,
|
||||
val hasGPS: Boolean,
|
||||
val model: String?,
|
||||
val firmwareVersion: String?,
|
||||
val couldUpdate: Boolean, // this application contains a software load we _could_ install if you want
|
||||
val shouldUpdate: Boolean, // this device has old firmware
|
||||
val currentPacketId: Long,
|
||||
val messageTimeoutMsec: Int,
|
||||
val minAppVersion: Int,
|
||||
val maxChannels: Int,
|
||||
val hasWifi: Boolean,
|
||||
val channelUtilization: Float,
|
||||
val airUtilTx: Float,
|
||||
val deviceId: String?,
|
||||
) : Parcelable {
|
||||
/** A human readable description of the software/hardware version */
|
||||
val firmwareString: String get() = "$model $firmwareVersion"
|
||||
}
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Parcelable
|
||||
import com.geeksville.mesh.util.anonymize
|
||||
import com.geeksville.mesh.util.bearing
|
||||
import com.geeksville.mesh.util.latLongToMeter
|
||||
import com.geeksville.mesh.util.onlineTimeThreshold
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
//
|
||||
// model objects that directly map to the corresponding protobufs
|
||||
//
|
||||
|
||||
@Parcelize
|
||||
data class MeshUser(
|
||||
val id: String,
|
||||
val longName: String,
|
||||
val shortName: String,
|
||||
val hwModel: MeshProtos.HardwareModel,
|
||||
val isLicensed: Boolean = false,
|
||||
val role: Int = 0,
|
||||
) : Parcelable {
|
||||
|
||||
override fun toString(): String = "MeshUser(id=${id.anonymize}, " +
|
||||
"longName=${longName.anonymize}, " +
|
||||
"shortName=${shortName.anonymize}, " +
|
||||
"hwModel=$hwModelString, " +
|
||||
"isLicensed=$isLicensed, " +
|
||||
"role=$role)"
|
||||
|
||||
/** Create our model object from a protobuf. */
|
||||
constructor(p: MeshProtos.User) : this(p.id, p.longName, p.shortName, p.hwModel, p.isLicensed, p.roleValue)
|
||||
|
||||
/**
|
||||
* a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot or null
|
||||
* if unset
|
||||
*/
|
||||
val hwModelString: String?
|
||||
get() =
|
||||
if (hwModel == MeshProtos.HardwareModel.UNSET) {
|
||||
null
|
||||
} else {
|
||||
hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Position(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Int,
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val satellitesInView: Int = 0,
|
||||
val groundSpeed: Int = 0,
|
||||
val groundTrack: Int = 0, // "heading"
|
||||
val precisionBits: Int = 0,
|
||||
) : Parcelable {
|
||||
|
||||
companion object {
|
||||
// / Convert to a double representation of degrees
|
||||
fun degD(i: Int) = i * 1e-7
|
||||
|
||||
fun degI(d: Double) = (d * 1e7).toInt()
|
||||
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create our model object from a protobuf. If time is unspecified in the protobuf, the provided default time will
|
||||
* be used.
|
||||
*/
|
||||
constructor(
|
||||
position: MeshProtos.Position,
|
||||
defaultTime: Int = currentTime(),
|
||||
) : this(
|
||||
// We prefer the int version of lat/lon but if not available use the depreciated legacy version
|
||||
degD(position.latitudeI),
|
||||
degD(position.longitudeI),
|
||||
position.altitude,
|
||||
if (position.time != 0) position.time else defaultTime,
|
||||
position.satsInView,
|
||||
position.groundSpeed,
|
||||
position.groundTrack,
|
||||
position.precisionBits,
|
||||
)
|
||||
|
||||
// / @return distance in meters to some other node (or null if unknown)
|
||||
fun distance(o: Position) = latLongToMeter(latitude, longitude, o.latitude, o.longitude)
|
||||
|
||||
// / @return bearing to the other position in degrees
|
||||
fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude)
|
||||
|
||||
// If GPS gives a crap position don't crash our app
|
||||
fun isValid(): Boolean = latitude != 0.0 &&
|
||||
longitude != 0.0 &&
|
||||
(latitude >= -90 && latitude <= 90.0) &&
|
||||
(longitude >= -180 && longitude <= 180)
|
||||
|
||||
override fun toString(): String =
|
||||
"Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=$time)"
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class DeviceMetrics(
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val batteryLevel: Int = 0,
|
||||
val voltage: Float,
|
||||
val channelUtilization: Float,
|
||||
val airUtilTx: Float,
|
||||
val uptimeSeconds: Int,
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
}
|
||||
|
||||
/** Create our model object from a protobuf. */
|
||||
constructor(
|
||||
p: TelemetryProtos.DeviceMetrics,
|
||||
telemetryTime: Int = currentTime(),
|
||||
) : this(telemetryTime, p.batteryLevel, p.voltage, p.channelUtilization, p.airUtilTx, p.uptimeSeconds)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class EnvironmentMetrics(
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val temperature: Float?,
|
||||
val relativeHumidity: Float?,
|
||||
val soilTemperature: Float?,
|
||||
val soilMoisture: Int?,
|
||||
val barometricPressure: Float?,
|
||||
val gasResistance: Float?,
|
||||
val voltage: Float?,
|
||||
val current: Float?,
|
||||
val iaq: Int?,
|
||||
val lux: Float? = null,
|
||||
val uvLux: Float? = null,
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
|
||||
fun fromTelemetryProto(proto: TelemetryProtos.EnvironmentMetrics, time: Int): EnvironmentMetrics =
|
||||
EnvironmentMetrics(
|
||||
temperature = proto.temperature.takeIf { proto.hasTemperature() && !it.isNaN() },
|
||||
relativeHumidity =
|
||||
proto.relativeHumidity.takeIf { proto.hasRelativeHumidity() && !it.isNaN() && it != 0.0f },
|
||||
soilTemperature = proto.soilTemperature.takeIf { proto.hasSoilTemperature() && !it.isNaN() },
|
||||
soilMoisture = proto.soilMoisture.takeIf { proto.hasSoilMoisture() && it != Int.MIN_VALUE },
|
||||
barometricPressure = proto.barometricPressure.takeIf { proto.hasBarometricPressure() && !it.isNaN() },
|
||||
gasResistance = proto.gasResistance.takeIf { proto.hasGasResistance() && !it.isNaN() },
|
||||
voltage = proto.voltage.takeIf { proto.hasVoltage() && !it.isNaN() },
|
||||
current = proto.current.takeIf { proto.hasCurrent() && !it.isNaN() },
|
||||
iaq = proto.iaq.takeIf { proto.hasIaq() && it != Int.MIN_VALUE },
|
||||
lux = proto.lux.takeIf { proto.hasLux() && !it.isNaN() },
|
||||
uvLux = proto.uvLux.takeIf { proto.hasUvLux() && !it.isNaN() },
|
||||
time = time,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class NodeInfo(
|
||||
val num: Int, // This is immutable, and used as a key
|
||||
var user: MeshUser? = null,
|
||||
var position: Position? = null,
|
||||
var snr: Float = Float.MAX_VALUE,
|
||||
var rssi: Int = Int.MAX_VALUE,
|
||||
var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
|
||||
var deviceMetrics: DeviceMetrics? = null,
|
||||
var channel: Int = 0,
|
||||
var environmentMetrics: EnvironmentMetrics? = null,
|
||||
var hopsAway: Int = 0,
|
||||
) : Parcelable {
|
||||
|
||||
val colors: Pair<Int, Int>
|
||||
get() { // returns foreground and background @ColorInt for each 'num'
|
||||
val r = (num and 0xFF0000) shr 16
|
||||
val g = (num and 0x00FF00) shr 8
|
||||
val b = num and 0x0000FF
|
||||
val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
|
||||
return (if (brightness > 0.5) Color.BLACK else Color.WHITE) to Color.rgb(r, g, b)
|
||||
}
|
||||
|
||||
val batteryLevel
|
||||
get() = deviceMetrics?.batteryLevel
|
||||
|
||||
val voltage
|
||||
get() = deviceMetrics?.voltage
|
||||
|
||||
val batteryStr
|
||||
get() = if (batteryLevel in 1..100) String.format("%d%%", batteryLevel) else ""
|
||||
|
||||
/** true if the device was heard from recently */
|
||||
val isOnline: Boolean
|
||||
get() {
|
||||
return lastHeard > onlineTimeThreshold()
|
||||
}
|
||||
|
||||
// / return the position if it is valid, else null
|
||||
val validPosition: Position?
|
||||
get() {
|
||||
return position?.takeIf { it.isValid() }
|
||||
}
|
||||
|
||||
// / @return distance in meters to some other node (or null if unknown)
|
||||
fun distance(o: NodeInfo?): Int? {
|
||||
val p = validPosition
|
||||
val op = o?.validPosition
|
||||
return if (p != null && op != null) p.distance(op).toInt() else null
|
||||
}
|
||||
|
||||
// / @return bearing to the other position in degrees
|
||||
fun bearing(o: NodeInfo?): Int? {
|
||||
val p = validPosition
|
||||
val op = o?.validPosition
|
||||
return if (p != null && op != null) p.bearing(op).toInt() else null
|
||||
}
|
||||
|
||||
// / @return a nice human readable string for the distance, or null for unknown
|
||||
fun distanceStr(o: NodeInfo?, prefUnits: Int = 0) = distance(o)?.let { dist ->
|
||||
when {
|
||||
dist == 0 -> null // same point
|
||||
prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist < 1000 ->
|
||||
"%.0f m".format(dist.toDouble())
|
||||
prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist >= 1000 ->
|
||||
"%.1f km".format(dist / 1000.0)
|
||||
prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist < 1609 ->
|
||||
"%.0f ft".format(dist.toDouble() * 3.281)
|
||||
prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist >= 1609 ->
|
||||
"%.1f mi".format(dist / 1609.34)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
package com.geeksville.mesh.database
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
|
|
@ -26,6 +25,7 @@ import com.geeksville.mesh.android.Logging
|
|||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class Converters : Logging {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ package com.geeksville.mesh.database
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
|
|
@ -28,7 +27,6 @@ import com.geeksville.mesh.database.entity.MyNodeEntity
|
|||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.NodeSortOption
|
||||
import com.geeksville.mesh.util.onlineTimeThreshold
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
|
@ -40,6 +38,8 @@ import kotlinx.coroutines.flow.mapLatest
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package com.geeksville.mesh.database
|
||||
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.Portnums.PortNum
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
|
|
@ -29,66 +27,52 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import javax.inject.Inject
|
||||
|
||||
class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Lazy<PacketDao>) {
|
||||
private val packetDao by lazy {
|
||||
packetDaoLazy.get()
|
||||
}
|
||||
private val packetDao by lazy { packetDaoLazy.get() }
|
||||
|
||||
fun getWaypoints(): Flow<List<Packet>> = packetDao.getAllPackets(PortNum.WAYPOINT_APP_VALUE)
|
||||
|
||||
fun getContacts(): Flow<Map<String, Packet>> = packetDao.getContactKeys()
|
||||
|
||||
suspend fun getMessageCount(contact: String): Int = withContext(Dispatchers.IO) {
|
||||
packetDao.getMessageCount(contact)
|
||||
}
|
||||
suspend fun getMessageCount(contact: String): Int =
|
||||
withContext(Dispatchers.IO) { packetDao.getMessageCount(contact) }
|
||||
|
||||
suspend fun getUnreadCount(contact: String): Int = withContext(Dispatchers.IO) {
|
||||
packetDao.getUnreadCount(contact)
|
||||
}
|
||||
suspend fun getUnreadCount(contact: String): Int = withContext(Dispatchers.IO) { packetDao.getUnreadCount(contact) }
|
||||
|
||||
suspend fun clearUnreadCount(contact: String, timestamp: Long) = withContext(Dispatchers.IO) {
|
||||
packetDao.clearUnreadCount(contact, timestamp)
|
||||
}
|
||||
suspend fun clearUnreadCount(contact: String, timestamp: Long) =
|
||||
withContext(Dispatchers.IO) { packetDao.clearUnreadCount(contact, timestamp) }
|
||||
|
||||
suspend fun getQueuedPackets(): List<DataPacket>? = withContext(Dispatchers.IO) {
|
||||
packetDao.getQueuedPackets()
|
||||
}
|
||||
suspend fun getQueuedPackets(): List<DataPacket>? = withContext(Dispatchers.IO) { packetDao.getQueuedPackets() }
|
||||
|
||||
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) {
|
||||
packetDao.insert(packet)
|
||||
}
|
||||
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) { packetDao.insert(packet) }
|
||||
|
||||
suspend fun getMessagesFrom(contact: String, getNode: suspend (String?) -> Node) =
|
||||
withContext(Dispatchers.IO) {
|
||||
packetDao.getMessagesFrom(contact).mapLatest { packets ->
|
||||
packets.map { packet ->
|
||||
val message = packet.toMessage(getNode)
|
||||
message.replyId.takeIf { it != null && it != 0 }
|
||||
?.let { getPacketByPacketId(it) }
|
||||
?.toMessage(getNode)
|
||||
?.let { originalMessage -> message.copy(originalMessage = originalMessage) }
|
||||
?: message
|
||||
}
|
||||
suspend fun getMessagesFrom(contact: String, getNode: suspend (String?) -> Node) = withContext(Dispatchers.IO) {
|
||||
packetDao.getMessagesFrom(contact).mapLatest { packets ->
|
||||
packets.map { packet ->
|
||||
val message = packet.toMessage(getNode)
|
||||
message.replyId
|
||||
.takeIf { it != null && it != 0 }
|
||||
?.let { getPacketByPacketId(it) }
|
||||
?.toMessage(getNode)
|
||||
?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) = withContext(Dispatchers.IO) {
|
||||
packetDao.updateMessageStatus(d, m)
|
||||
}
|
||||
|
||||
suspend fun updateMessageId(d: DataPacket, id: Int) = withContext(Dispatchers.IO) {
|
||||
packetDao.updateMessageId(d, id)
|
||||
}
|
||||
suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) =
|
||||
withContext(Dispatchers.IO) { packetDao.updateMessageStatus(d, m) }
|
||||
|
||||
suspend fun getPacketById(requestId: Int) = withContext(Dispatchers.IO) {
|
||||
packetDao.getPacketById(requestId)
|
||||
}
|
||||
suspend fun updateMessageId(d: DataPacket, id: Int) =
|
||||
withContext(Dispatchers.IO) { packetDao.updateMessageId(d, id) }
|
||||
|
||||
suspend fun getPacketByPacketId(packetId: Int) = withContext(Dispatchers.IO) {
|
||||
packetDao.getPacketByPacketId(packetId)
|
||||
}
|
||||
suspend fun getPacketById(requestId: Int) = withContext(Dispatchers.IO) { packetDao.getPacketById(requestId) }
|
||||
|
||||
suspend fun getPacketByPacketId(packetId: Int) =
|
||||
withContext(Dispatchers.IO) { packetDao.getPacketByPacketId(packetId) }
|
||||
|
||||
suspend fun deleteMessages(uuidList: List<Long>) = withContext(Dispatchers.IO) {
|
||||
for (chunk in uuidList.chunked(500)) { // limit number of UUIDs per query
|
||||
|
|
@ -96,37 +80,24 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun deleteContacts(contactList: List<String>) = withContext(Dispatchers.IO) {
|
||||
packetDao.deleteContacts(contactList)
|
||||
}
|
||||
suspend fun deleteContacts(contactList: List<String>) =
|
||||
withContext(Dispatchers.IO) { packetDao.deleteContacts(contactList) }
|
||||
|
||||
suspend fun deleteWaypoint(id: Int) = withContext(Dispatchers.IO) {
|
||||
packetDao.deleteWaypoint(id)
|
||||
}
|
||||
suspend fun deleteWaypoint(id: Int) = withContext(Dispatchers.IO) { packetDao.deleteWaypoint(id) }
|
||||
|
||||
suspend fun delete(packet: Packet) = withContext(Dispatchers.IO) {
|
||||
packetDao.delete(packet)
|
||||
}
|
||||
suspend fun delete(packet: Packet) = withContext(Dispatchers.IO) { packetDao.delete(packet) }
|
||||
|
||||
suspend fun update(packet: Packet) = withContext(Dispatchers.IO) {
|
||||
packetDao.update(packet)
|
||||
}
|
||||
suspend fun update(packet: Packet) = withContext(Dispatchers.IO) { packetDao.update(packet) }
|
||||
|
||||
fun getContactSettings(): Flow<Map<String, ContactSettings>> = packetDao.getContactSettings()
|
||||
|
||||
suspend fun getContactSettings(contact: String) = withContext(Dispatchers.IO) {
|
||||
packetDao.getContactSettings(contact) ?: ContactSettings(contact)
|
||||
}
|
||||
suspend fun getContactSettings(contact: String) =
|
||||
withContext(Dispatchers.IO) { packetDao.getContactSettings(contact) ?: ContactSettings(contact) }
|
||||
|
||||
suspend fun setMuteUntil(contacts: List<String>, until: Long) = withContext(Dispatchers.IO) {
|
||||
packetDao.setMuteUntil(contacts, until)
|
||||
}
|
||||
suspend fun setMuteUntil(contacts: List<String>, until: Long) =
|
||||
withContext(Dispatchers.IO) { packetDao.setMuteUntil(contacts, until) }
|
||||
|
||||
suspend fun insertReaction(reaction: ReactionEntity) = withContext(Dispatchers.IO) {
|
||||
packetDao.insert(reaction)
|
||||
}
|
||||
suspend fun insertReaction(reaction: ReactionEntity) = withContext(Dispatchers.IO) { packetDao.insert(reaction) }
|
||||
|
||||
suspend fun clearPacketDB() = withContext(Dispatchers.IO) {
|
||||
packetDao.deleteAll()
|
||||
}
|
||||
suspend fun clearPacketDB() = withContext(Dispatchers.IO) { packetDao.deleteAll() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ import androidx.room.Query
|
|||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.PacketEntity
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
|
||||
@Dao
|
||||
interface PacketDao {
|
||||
|
|
@ -40,7 +40,7 @@ interface PacketDao {
|
|||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = :portNum
|
||||
ORDER BY received_time ASC
|
||||
"""
|
||||
""",
|
||||
)
|
||||
fun getAllPackets(portNum: Int): Flow<List<Packet>>
|
||||
|
||||
|
|
@ -50,16 +50,22 @@ interface PacketDao {
|
|||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1
|
||||
ORDER BY received_time DESC
|
||||
"""
|
||||
""",
|
||||
)
|
||||
fun getContactKeys(): Flow<Map<@MapColumn(columnName = "contact_key") String, Packet>>
|
||||
fun getContactKeys(): Flow<
|
||||
Map<
|
||||
@MapColumn(columnName = "contact_key")
|
||||
String,
|
||||
Packet,
|
||||
>,
|
||||
>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact
|
||||
"""
|
||||
""",
|
||||
)
|
||||
suspend fun getMessageCount(contact: String): Int
|
||||
|
||||
|
|
@ -68,7 +74,7 @@ interface PacketDao {
|
|||
SELECT COUNT(*) FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact AND read = 0
|
||||
"""
|
||||
""",
|
||||
)
|
||||
suspend fun getUnreadCount(contact: String): Int
|
||||
|
||||
|
|
@ -78,12 +84,11 @@ interface PacketDao {
|
|||
SET read = 1
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact AND read = 0 AND received_time <= :timestamp
|
||||
"""
|
||||
""",
|
||||
)
|
||||
suspend fun clearUnreadCount(contact: String, timestamp: Long)
|
||||
|
||||
@Upsert
|
||||
suspend fun insert(packet: Packet)
|
||||
@Upsert suspend fun insert(packet: Packet)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
|
|
@ -92,7 +97,7 @@ interface PacketDao {
|
|||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact
|
||||
ORDER BY received_time DESC
|
||||
"""
|
||||
""",
|
||||
)
|
||||
fun getMessagesFrom(contact: String): Flow<List<PacketEntity>>
|
||||
|
||||
|
|
@ -101,7 +106,7 @@ interface PacketDao {
|
|||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND data = :data
|
||||
"""
|
||||
""",
|
||||
)
|
||||
suspend fun findDataPacket(data: DataPacket): Packet?
|
||||
|
||||
|
|
@ -113,16 +118,16 @@ interface PacketDao {
|
|||
DELETE FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND contact_key IN (:contactList)
|
||||
"""
|
||||
""",
|
||||
)
|
||||
suspend fun deleteContacts(contactList: List<String>)
|
||||
|
||||
@Query("DELETE FROM packet WHERE uuid=:uuid")
|
||||
suspend fun _delete(uuid: Long)
|
||||
suspend fun delete(uuid: Long)
|
||||
|
||||
@Transaction
|
||||
suspend fun delete(packet: Packet) {
|
||||
_delete(packet.uuid)
|
||||
delete(packet.uuid)
|
||||
}
|
||||
|
||||
@Query("SELECT packet_id FROM packet WHERE uuid IN (:uuidList)")
|
||||
|
|
@ -140,8 +145,7 @@ interface PacketDao {
|
|||
deletePackets(uuidList)
|
||||
}
|
||||
|
||||
@Update
|
||||
suspend fun update(packet: Packet)
|
||||
@Update suspend fun update(packet: Packet)
|
||||
|
||||
@Transaction
|
||||
suspend fun updateMessageStatus(data: DataPacket, m: MessageStatus) {
|
||||
|
|
@ -160,7 +164,7 @@ interface PacketDao {
|
|||
SELECT data FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
ORDER BY received_time ASC
|
||||
"""
|
||||
""",
|
||||
)
|
||||
suspend fun getDataPackets(): List<DataPacket>
|
||||
|
||||
|
|
@ -171,7 +175,7 @@ interface PacketDao {
|
|||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND packet_id = :requestId
|
||||
ORDER BY received_time DESC
|
||||
"""
|
||||
""",
|
||||
)
|
||||
suspend fun getPacketById(requestId: Int): Packet?
|
||||
|
||||
|
|
@ -180,8 +184,7 @@ interface PacketDao {
|
|||
suspend fun getPacketByPacketId(packetId: Int): PacketEntity?
|
||||
|
||||
@Transaction
|
||||
suspend fun getQueuedPackets(): List<DataPacket>? =
|
||||
getDataPackets().filter { it.status == MessageStatus.QUEUED }
|
||||
suspend fun getQueuedPackets(): List<DataPacket>? = getDataPackets().filter { it.status == MessageStatus.QUEUED }
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
|
@ -189,7 +192,7 @@ interface PacketDao {
|
|||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 8
|
||||
ORDER BY received_time ASC
|
||||
"""
|
||||
""",
|
||||
)
|
||||
suspend fun getAllWaypoints(): List<Packet>
|
||||
|
||||
|
|
@ -200,25 +203,30 @@ interface PacketDao {
|
|||
}
|
||||
|
||||
@Query("SELECT * FROM contact_settings")
|
||||
fun getContactSettings(): Flow<Map<@MapColumn(columnName = "contact_key") String, ContactSettings>>
|
||||
fun getContactSettings(): Flow<
|
||||
Map<
|
||||
@MapColumn(columnName = "contact_key")
|
||||
String,
|
||||
ContactSettings,
|
||||
>,
|
||||
>
|
||||
|
||||
@Query("SELECT * FROM contact_settings WHERE contact_key = :contact")
|
||||
suspend fun getContactSettings(contact: String): ContactSettings?
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertContactSettings(contacts: List<ContactSettings>)
|
||||
@Upsert suspend fun upsertContactSettings(contacts: List<ContactSettings>)
|
||||
|
||||
@Transaction
|
||||
suspend fun setMuteUntil(contacts: List<String>, until: Long) {
|
||||
val contactList = contacts.map { contact ->
|
||||
getContactSettings(contact)?.copy(muteUntil = until)
|
||||
?: ContactSettings(contact_key = contact, muteUntil = until)
|
||||
}
|
||||
val contactList =
|
||||
contacts.map { contact ->
|
||||
getContactSettings(contact)?.copy(muteUntil = until)
|
||||
?: ContactSettings(contact_key = contact, muteUntil = until)
|
||||
}
|
||||
upsertContactSettings(contactList)
|
||||
}
|
||||
|
||||
@Upsert
|
||||
suspend fun insert(reaction: ReactionEntity)
|
||||
@Upsert suspend fun insert(reaction: ReactionEntity)
|
||||
|
||||
@Query("DELETE FROM packet")
|
||||
suspend fun deleteAll()
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import androidx.room.Entity
|
|||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.network.model.NetworkDeviceHardware
|
||||
import org.meshtastic.core.model.NetworkDeviceHardware
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "device_hardware")
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ package com.geeksville.mesh.database.entity
|
|||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.meshtastic.core.network.model.NetworkFirmwareRelease
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.model.NetworkFirmwareRelease
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "firmware_release")
|
||||
|
|
|
|||
|
|
@ -19,12 +19,11 @@ package com.geeksville.mesh.database.entity
|
|||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
|
||||
@Entity(tableName = "my_node")
|
||||
data class MyNodeEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val myNodeNum: Int,
|
||||
@PrimaryKey(autoGenerate = false) val myNodeNum: Int,
|
||||
val model: String?,
|
||||
val firmwareVersion: String?,
|
||||
val couldUpdate: Boolean, // this application contains a software load we _could_ install if you want
|
||||
|
|
@ -37,7 +36,8 @@ data class MyNodeEntity(
|
|||
val deviceId: String? = "unknown",
|
||||
) {
|
||||
/** A human readable description of the software/hardware version */
|
||||
val firmwareString: String get() = "$model $firmwareVersion"
|
||||
val firmwareString: String
|
||||
get() = "$model $firmwareVersion"
|
||||
|
||||
fun toMyNodeInfo() = MyNodeInfo(
|
||||
myNodeNum = myNodeNum,
|
||||
|
|
|
|||
|
|
@ -23,19 +23,19 @@ import androidx.room.Entity
|
|||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
import com.geeksville.mesh.DeviceMetrics
|
||||
import com.geeksville.mesh.EnvironmentMetrics
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MeshUser
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.util.onlineTimeThreshold
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.kotlin.isNotEmpty
|
||||
import org.meshtastic.core.model.DeviceMetrics
|
||||
import org.meshtastic.core.model.EnvironmentMetrics
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
|
||||
data class NodeWithRelations(
|
||||
@Embedded val node: NodeEntity,
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ import androidx.room.Entity
|
|||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos.User
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.util.getShortDateTime
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.getShortDateTime
|
||||
|
||||
data class PacketEntity(
|
||||
@Embedded val packet: Packet,
|
||||
|
|
@ -52,20 +52,15 @@ data class PacketEntity(
|
|||
packetId = packetId,
|
||||
emojis = reactions.toReaction(getNode),
|
||||
replyId = data.replyId,
|
||||
viaMqtt = node.viaMqtt
|
||||
viaMqtt = node.viaMqtt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
tableName = "packet",
|
||||
indices = [
|
||||
Index(value = ["myNodeNum"]),
|
||||
Index(value = ["port_num"]),
|
||||
Index(value = ["contact_key"]),
|
||||
]
|
||||
indices = [Index(value = ["myNodeNum"]), Index(value = ["port_num"]), Index(value = ["contact_key"])],
|
||||
)
|
||||
|
||||
data class Packet(
|
||||
@PrimaryKey(autoGenerate = true) val uuid: Long,
|
||||
@ColumnInfo(name = "myNodeNum", defaultValue = "0") val myNodeNum: Int,
|
||||
|
|
@ -83,26 +78,17 @@ data class Packet(
|
|||
)
|
||||
|
||||
@Entity(tableName = "contact_settings")
|
||||
data class ContactSettings(
|
||||
@PrimaryKey val contact_key: String,
|
||||
val muteUntil: Long = 0L,
|
||||
) {
|
||||
val isMuted get() = System.currentTimeMillis() <= muteUntil
|
||||
data class ContactSettings(@PrimaryKey val contact_key: String, val muteUntil: Long = 0L) {
|
||||
val isMuted
|
||||
get() = System.currentTimeMillis() <= muteUntil
|
||||
}
|
||||
|
||||
data class Reaction(
|
||||
val replyId: Int,
|
||||
val user: User,
|
||||
val emoji: String,
|
||||
val timestamp: Long,
|
||||
)
|
||||
data class Reaction(val replyId: Int, val user: User, val emoji: String, val timestamp: Long)
|
||||
|
||||
@Entity(
|
||||
tableName = "reactions",
|
||||
primaryKeys = ["reply_id", "user_id", "emoji"],
|
||||
indices = [
|
||||
Index(value = ["reply_id"]),
|
||||
],
|
||||
indices = [Index(value = ["reply_id"])],
|
||||
)
|
||||
data class ReactionEntity(
|
||||
@ColumnInfo(name = "reply_id") val replyId: Int,
|
||||
|
|
@ -111,15 +97,8 @@ data class ReactionEntity(
|
|||
val timestamp: Long,
|
||||
)
|
||||
|
||||
private suspend fun ReactionEntity.toReaction(
|
||||
getNode: suspend (userId: String?) -> Node
|
||||
) = Reaction(
|
||||
replyId = replyId,
|
||||
user = getNode(userId).user,
|
||||
emoji = emoji,
|
||||
timestamp = timestamp,
|
||||
)
|
||||
private suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) -> Node) =
|
||||
Reaction(replyId = replyId, user = getNode(userId).user, emoji = emoji, timestamp = timestamp)
|
||||
|
||||
private suspend fun List<ReactionEntity>.toReaction(
|
||||
getNode: suspend (userId: String?) -> Node
|
||||
) = this.map { it.toReaction(getNode) }
|
||||
private suspend fun List<ReactionEntity>.toReaction(getNode: suspend (userId: String?) -> Node) =
|
||||
this.map { it.toReaction(getNode) }
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
|||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.service.ServiceRepository
|
||||
import com.geeksville.mesh.util.anonymize
|
||||
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -52,6 +51,7 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.datastore.model.RecentAddress
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.strings.R
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.model
|
||||
|
||||
import com.geeksville.mesh.android.Logging
|
||||
|
||||
/**
|
||||
* Provide structured access to parse and compare device version strings
|
||||
*/
|
||||
data class DeviceVersion(val asString: String) : Comparable<DeviceVersion>, Logging {
|
||||
|
||||
val asInt
|
||||
get() = try {
|
||||
verStringToInt(asString)
|
||||
} catch (e: Exception) {
|
||||
warn("Exception while parsing version '$asString', assuming version 0")
|
||||
0
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a version string of the form 1.23.57 to a comparable integer of
|
||||
* the form 12357.
|
||||
*
|
||||
* Or throw an exception if the string can not be parsed
|
||||
*/
|
||||
private fun verStringToInt(s: String): Int {
|
||||
// Allow 1 to two digits per match
|
||||
val match =
|
||||
Regex("(\\d{1,2}).(\\d{1,2}).(\\d{1,2})").find(s)
|
||||
?: throw Exception("Can't parse version $s")
|
||||
val (major, minor, build) = match.destructured
|
||||
return major.toInt() * 10000 + minor.toInt() * 100 + build.toInt()
|
||||
}
|
||||
|
||||
override fun compareTo(other: DeviceVersion): Int = asInt - other.asInt
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ import com.geeksville.mesh.ui.common.theme.GraphColors.Pink
|
|||
import com.geeksville.mesh.ui.common.theme.GraphColors.Purple
|
||||
import com.geeksville.mesh.ui.common.theme.GraphColors.Red
|
||||
import com.geeksville.mesh.ui.common.theme.GraphColors.Yellow
|
||||
import com.geeksville.mesh.util.UnitConversions
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
enum class Environment(val color: Color) {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ package com.geeksville.mesh.model
|
|||
|
||||
import androidx.annotation.StringRes
|
||||
import com.geeksville.mesh.MeshProtos.Routing
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.navigation.toRoute
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.MeshProtos.Position
|
||||
|
|
@ -59,6 +58,7 @@ import kotlinx.coroutines.flow.toList
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ import com.geeksville.mesh.TelemetryProtos.DeviceMetrics
|
|||
import com.geeksville.mesh.TelemetryProtos.EnvironmentMetrics
|
||||
import com.geeksville.mesh.TelemetryProtos.PowerMetrics
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.util.GPSFormat
|
||||
import com.geeksville.mesh.util.UnitConversions.celsiusToFahrenheit
|
||||
import com.geeksville.mesh.util.latLongToMeter
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.kotlin.isNotEmpty
|
||||
import org.meshtastic.core.model.util.GPSFormat
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.latLongToMeter
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
data class Node(
|
||||
|
|
@ -114,7 +114,7 @@ data class Node(
|
|||
// @return bearing to the other position in degrees
|
||||
fun bearing(o: Node?): Int? = when {
|
||||
validPosition == null || o?.validPosition == null -> null
|
||||
else -> com.geeksville.mesh.util.bearing(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||
else -> org.meshtastic.core.model.util.bearing(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||
}
|
||||
|
||||
fun gpsString(): String = GPSFormat.toDec(latitude, longitude)
|
||||
|
|
|
|||
|
|
@ -32,12 +32,10 @@ import com.geeksville.mesh.AppOnlyProtos
|
|||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.ConfigProtos.Config
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.channel
|
||||
import com.geeksville.mesh.channelSet
|
||||
|
|
@ -60,7 +58,6 @@ import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
|||
import com.geeksville.mesh.service.MeshServiceNotifications
|
||||
import com.geeksville.mesh.service.ServiceAction
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import com.geeksville.mesh.util.getShortDate
|
||||
import com.geeksville.mesh.util.safeNumber
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -82,7 +79,10 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.getShortDate
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import org.meshtastic.core.strings.R
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import android.app.Application
|
|||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import org.meshtastic.core.network.model.NetworkDeviceHardware
|
||||
import org.meshtastic.core.model.NetworkDeviceHardware
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeviceHardwareJsonDataSource @Inject constructor(private val application: Application) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
|||
import com.geeksville.mesh.database.entity.asEntity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.network.model.NetworkDeviceHardware
|
||||
import org.meshtastic.core.model.NetworkDeviceHardware
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeviceHardwareLocalDataSource
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import android.app.Application
|
|||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import org.meshtastic.core.network.model.NetworkFirmwareReleases
|
||||
import org.meshtastic.core.model.NetworkFirmwareReleases
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirmwareReleaseJsonDataSource @Inject constructor(private val application: Application) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import com.geeksville.mesh.database.entity.asEntity
|
|||
import dagger.Lazy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.network.model.NetworkFirmwareRelease
|
||||
import org.meshtastic.core.model.NetworkFirmwareRelease
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirmwareReleaseLocalDataSource @Inject constructor(private val firmwareReleaseDaoLazy: Lazy<FirmwareReleaseDao>) {
|
||||
|
|
|
|||
|
|
@ -18,19 +18,19 @@
|
|||
package com.geeksville.mesh.repository.bluetooth
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import com.geeksville.mesh.util.anonymize
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
|
||||
/**
|
||||
* A snapshot in time of the state of the bluetooth subsystem.
|
||||
*/
|
||||
/** A snapshot in time of the state of the bluetooth subsystem. */
|
||||
data class BluetoothState(
|
||||
/** Whether we have adequate permissions to query bluetooth state */
|
||||
val hasPermissions: Boolean = false,
|
||||
/** If we have adequate permissions and bluetooth is enabled */
|
||||
val enabled: Boolean = false,
|
||||
/** If enabled, a list of the currently bonded devices */
|
||||
val bondedDevices: List<BluetoothDevice> = emptyList()
|
||||
val bondedDevices: List<BluetoothDevice> = emptyList(),
|
||||
) {
|
||||
override fun toString(): String =
|
||||
"BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map { it.anonymize }})"
|
||||
"BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map {
|
||||
it.anonymize
|
||||
}})"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import com.geeksville.mesh.service.BLEConnectionClosing
|
|||
import com.geeksville.mesh.service.BLEException
|
||||
import com.geeksville.mesh.service.RadioNotConnectedException
|
||||
import com.geeksville.mesh.service.SafeBluetooth
|
||||
import com.geeksville.mesh.util.anonymize
|
||||
import com.geeksville.mesh.util.exceptionReporter
|
||||
import com.geeksville.mesh.util.ignoreException
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -37,6 +36,7 @@ import dagger.assisted.AssistedInject
|
|||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import java.lang.reflect.Method
|
||||
import java.util.UUID
|
||||
|
||||
|
|
|
|||
|
|
@ -19,24 +19,22 @@ package com.geeksville.mesh.repository.radio
|
|||
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||
import com.geeksville.mesh.util.anonymize
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Bluetooth backend implementation.
|
||||
*/
|
||||
class BluetoothInterfaceSpec @Inject constructor(
|
||||
/** Bluetooth backend implementation. */
|
||||
class BluetoothInterfaceSpec
|
||||
@Inject
|
||||
constructor(
|
||||
private val factory: BluetoothInterfaceFactory,
|
||||
private val bluetoothRepository: BluetoothRepository,
|
||||
) : InterfaceSpec<BluetoothInterface>, Logging {
|
||||
override fun createInterface(rest: String): BluetoothInterface {
|
||||
return factory.create(rest)
|
||||
}
|
||||
) : InterfaceSpec<BluetoothInterface>,
|
||||
Logging {
|
||||
override fun createInterface(rest: String): BluetoothInterface = factory.create(rest)
|
||||
|
||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||
override fun addressValid(rest: String): Boolean {
|
||||
val allPaired = bluetoothRepository.state.value.bondedDevices
|
||||
.map { it.address }.toSet()
|
||||
val allPaired = bluetoothRepository.state.value.bondedDevices.map { it.address }.toSet()
|
||||
return if (!allPaired.contains(rest)) {
|
||||
warn("Ignoring stale bond to ${rest.anonymize}")
|
||||
false
|
||||
|
|
|
|||
|
|
@ -21,10 +21,8 @@ import com.geeksville.mesh.AdminProtos
|
|||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.ConfigKt
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.channel
|
||||
|
|
@ -39,6 +37,8 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.delay
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Position
|
||||
import kotlin.random.Random
|
||||
|
||||
private val defaultLoRaConfig =
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import com.geeksville.mesh.concurrent.handledLaunch
|
|||
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository
|
||||
import com.geeksville.mesh.service.ConnectionState
|
||||
import com.geeksville.mesh.util.anonymize
|
||||
import com.geeksville.mesh.util.ignoreException
|
||||
import com.geeksville.mesh.util.toRemoteExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -48,6 +47,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.prefs.radio.RadioPrefs
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import com.geeksville.mesh.BuildConfig
|
|||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.DeviceUIProtos
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
|
|
@ -42,14 +41,9 @@ import com.geeksville.mesh.MeshProtos
|
|||
import com.geeksville.mesh.MeshProtos.FromRadio.PayloadVariantCase
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.MeshProtos.ToRadio
|
||||
import com.geeksville.mesh.MeshUser
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.ModuleConfigProtos
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.StoreAndForwardProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.TelemetryProtos.LocalStats
|
||||
|
|
@ -68,7 +62,6 @@ import com.geeksville.mesh.database.entity.NodeEntity
|
|||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
import com.geeksville.mesh.fromRadio
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.position
|
||||
|
|
@ -78,10 +71,7 @@ import com.geeksville.mesh.repository.network.MQTTRepository
|
|||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.telemetry
|
||||
import com.geeksville.mesh.user
|
||||
import com.geeksville.mesh.util.anonymize
|
||||
import com.geeksville.mesh.util.ignoreException
|
||||
import com.geeksville.mesh.util.toOneLineString
|
||||
import com.geeksville.mesh.util.toPIIString
|
||||
import com.geeksville.mesh.util.toRemoteExceptions
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
|
|
@ -100,7 +90,17 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.getFullTracerouteResponse
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.model.util.toOneLineString
|
||||
import org.meshtastic.core.model.util.toPIIString
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import org.meshtastic.core.strings.R
|
||||
|
|
@ -2155,8 +2155,9 @@ class MeshService :
|
|||
toRemoteExceptions {
|
||||
if (p.id == 0) p.id = generatePacketId()
|
||||
|
||||
val bytes = p.bytes!!
|
||||
info(
|
||||
"sendData dest=${p.to}, id=${p.id} <- ${p.bytes!!.size} bytes" +
|
||||
"sendData dest=${p.to}, id=${p.id} <- ${bytes.size} bytes" +
|
||||
" (connectionState=$connectionState)",
|
||||
)
|
||||
|
||||
|
|
@ -2164,7 +2165,7 @@ class MeshService :
|
|||
throw Exception("Port numbers must be non-zero!") // we are now more strict
|
||||
}
|
||||
|
||||
if (p.bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN.number) {
|
||||
if (bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN.number) {
|
||||
p.status = MessageStatus.ERROR
|
||||
throw RemoteException("Message too long")
|
||||
} else {
|
||||
|
|
@ -2188,7 +2189,7 @@ class MeshService :
|
|||
|
||||
GeeksvilleApplication.analytics.track(
|
||||
"data_send",
|
||||
DataPair("num_bytes", p.bytes.size),
|
||||
DataPair("num_bytes", bytes.size),
|
||||
DataPair("type", p.dataType),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ package com.geeksville.mesh.service
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
|
||||
class MeshServiceBroadcasts(
|
||||
private val context: Context,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import com.geeksville.mesh.R.raw
|
|||
import com.geeksville.mesh.TelemetryProtos.LocalStats
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.service.ReplyReceiver.Companion.KEY_TEXT_REPLY
|
||||
import com.geeksville.mesh.util.formatUptime
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,9 @@
|
|||
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.MeshProtos.ToRadio
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.errormsg
|
||||
import com.geeksville.mesh.android.BuildUtils.info
|
||||
|
|
@ -31,8 +29,6 @@ import com.geeksville.mesh.database.PacketRepository
|
|||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.fromRadio
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.util.toOneLineString
|
||||
import com.geeksville.mesh.util.toPIIString
|
||||
import dagger.Lazy
|
||||
import java8.util.concurrent.CompletableFuture
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -40,6 +36,10 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.toOneLineString
|
||||
import org.meshtastic.core.model.util.toPIIString
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
|
|||
|
|
@ -19,24 +19,22 @@ package com.geeksville.mesh.service
|
|||
|
||||
import android.content.BroadcastReceiver
|
||||
import androidx.core.app.RemoteInput
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import jakarta.inject.Inject
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
|
||||
/**
|
||||
* A [BroadcastReceiver] that handles inline replies from notifications.
|
||||
*
|
||||
* This receiver is triggered when a user replies to a message directly from a notification.
|
||||
* It extracts the reply text and the contact key from the intent, sends the message
|
||||
* using the [ServiceRepository], and then cancels the original notification.
|
||||
* This receiver is triggered when a user replies to a message directly from a notification. It extracts the reply text
|
||||
* and the contact key from the intent, sends the message using the [ServiceRepository], and then cancels the original
|
||||
* notification.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ReplyReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var serviceRepository: ServiceRepository
|
||||
@Inject lateinit var serviceRepository: ServiceRepository
|
||||
|
||||
@Inject
|
||||
lateinit var meshServiceNotifications: MeshServiceNotifications
|
||||
@Inject lateinit var meshServiceNotifications: MeshServiceNotifications
|
||||
|
||||
companion object {
|
||||
const val REPLY_ACTION = "com.geeksville.mesh.REPLY_ACTION"
|
||||
|
|
@ -57,9 +55,7 @@ class ReplyReceiver : BroadcastReceiver() {
|
|||
|
||||
if (remoteInput != null) {
|
||||
val contactKey = intent.getStringExtra(CONTACT_KEY) ?: ""
|
||||
val message = remoteInput.getCharSequence(
|
||||
KEY_TEXT_REPLY
|
||||
)?.toString() ?: ""
|
||||
val message = remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString() ?: ""
|
||||
sendMessage(message, contactKey)
|
||||
MeshServiceNotifications(context).cancelMessageNotification(contactKey)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ import com.geeksville.mesh.android.AddNavigationTracking
|
|||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.setAttributes
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.navigation.channelsGraph
|
||||
|
|
@ -112,6 +111,7 @@ import com.google.accompanist.permissions.isGranted
|
|||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.navigation.ConnectionsRoutes
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package com.geeksville.mesh.ui.common.preview
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.DeviceMetrics.Companion.currentTime
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.deviceMetrics
|
||||
import com.geeksville.mesh.environmentMetrics
|
||||
|
|
@ -28,118 +27,132 @@ import com.geeksville.mesh.paxcount
|
|||
import com.geeksville.mesh.position
|
||||
import com.geeksville.mesh.user
|
||||
import com.google.protobuf.ByteString
|
||||
import org.meshtastic.core.model.DeviceMetrics.Companion.currentTime
|
||||
import kotlin.random.Random
|
||||
|
||||
class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
|
||||
val mickeyMouse = Node(
|
||||
num = 1955,
|
||||
user = user {
|
||||
id = "mickeyMouseId"
|
||||
longName = "Mickey Mouse"
|
||||
shortName = "MM"
|
||||
hwModel = MeshProtos.HardwareModel.TBEAM
|
||||
role = ConfigProtos.Config.DeviceConfig.Role.ROUTER
|
||||
},
|
||||
position = position {
|
||||
latitudeI = 338125110
|
||||
longitudeI = -1179189760
|
||||
altitude = 138
|
||||
satsInView = 4
|
||||
},
|
||||
lastHeard = currentTime(),
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceMetrics = deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
isFavorite = true,
|
||||
hopsAway = 0
|
||||
)
|
||||
val mickeyMouse =
|
||||
Node(
|
||||
num = 1955,
|
||||
user =
|
||||
user {
|
||||
id = "mickeyMouseId"
|
||||
longName = "Mickey Mouse"
|
||||
shortName = "MM"
|
||||
hwModel = MeshProtos.HardwareModel.TBEAM
|
||||
role = ConfigProtos.Config.DeviceConfig.Role.ROUTER
|
||||
},
|
||||
position =
|
||||
position {
|
||||
latitudeI = 338125110
|
||||
longitudeI = -1179189760
|
||||
altitude = 138
|
||||
satsInView = 4
|
||||
},
|
||||
lastHeard = currentTime(),
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceMetrics =
|
||||
deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
isFavorite = true,
|
||||
hopsAway = 0,
|
||||
)
|
||||
|
||||
val minnieMouse = mickeyMouse.copy(
|
||||
num = Random.nextInt(),
|
||||
user = user {
|
||||
longName = "Minnie Mouse"
|
||||
shortName = "MiMo"
|
||||
id = "minnieMouseId"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
},
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
position = position {},
|
||||
hopsAway = 1
|
||||
)
|
||||
val minnieMouse =
|
||||
mickeyMouse.copy(
|
||||
num = Random.nextInt(),
|
||||
user =
|
||||
user {
|
||||
longName = "Minnie Mouse"
|
||||
shortName = "MiMo"
|
||||
id = "minnieMouseId"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
},
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
position = position {},
|
||||
hopsAway = 1,
|
||||
)
|
||||
|
||||
private val donaldDuck = Node(
|
||||
num = Random.nextInt(),
|
||||
position = position {
|
||||
latitudeI = 338052347
|
||||
longitudeI = -1179208460
|
||||
altitude = 121
|
||||
satsInView = 66
|
||||
},
|
||||
lastHeard = currentTime() - 300,
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceMetrics = deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
user = user {
|
||||
id = "donaldDuckId"
|
||||
longName = "Donald Duck, the Grand Duck of the Ducks"
|
||||
shortName = "DoDu"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
publicKey = ByteString.copyFrom(ByteArray(32) { 1 })
|
||||
},
|
||||
environmentMetrics = environmentMetrics {
|
||||
temperature = 28.0F
|
||||
relativeHumidity = 50.0F
|
||||
barometricPressure = 1013.25F
|
||||
gasResistance = 0.0F
|
||||
voltage = 3.7F
|
||||
current = 0.0F
|
||||
iaq = 100
|
||||
},
|
||||
paxcounter = paxcount {
|
||||
wifi = 30
|
||||
ble = 39
|
||||
uptime = 420
|
||||
},
|
||||
isFavorite = true,
|
||||
hopsAway = 2
|
||||
)
|
||||
private val donaldDuck =
|
||||
Node(
|
||||
num = Random.nextInt(),
|
||||
position =
|
||||
position {
|
||||
latitudeI = 338052347
|
||||
longitudeI = -1179208460
|
||||
altitude = 121
|
||||
satsInView = 66
|
||||
},
|
||||
lastHeard = currentTime() - 300,
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceMetrics =
|
||||
deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
user =
|
||||
user {
|
||||
id = "donaldDuckId"
|
||||
longName = "Donald Duck, the Grand Duck of the Ducks"
|
||||
shortName = "DoDu"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
publicKey = ByteString.copyFrom(ByteArray(32) { 1 })
|
||||
},
|
||||
environmentMetrics =
|
||||
environmentMetrics {
|
||||
temperature = 28.0F
|
||||
relativeHumidity = 50.0F
|
||||
barometricPressure = 1013.25F
|
||||
gasResistance = 0.0F
|
||||
voltage = 3.7F
|
||||
current = 0.0F
|
||||
iaq = 100
|
||||
},
|
||||
paxcounter =
|
||||
paxcount {
|
||||
wifi = 30
|
||||
ble = 39
|
||||
uptime = 420
|
||||
},
|
||||
isFavorite = true,
|
||||
hopsAway = 2,
|
||||
)
|
||||
|
||||
private val unknown = donaldDuck.copy(
|
||||
user = user {
|
||||
id = "myId"
|
||||
longName = "Meshtastic myId"
|
||||
shortName = "myId"
|
||||
hwModel = MeshProtos.HardwareModel.UNSET
|
||||
},
|
||||
environmentMetrics = environmentMetrics {},
|
||||
paxcounter = paxcount {},
|
||||
)
|
||||
private val unknown =
|
||||
donaldDuck.copy(
|
||||
user =
|
||||
user {
|
||||
id = "myId"
|
||||
longName = "Meshtastic myId"
|
||||
shortName = "myId"
|
||||
hwModel = MeshProtos.HardwareModel.UNSET
|
||||
},
|
||||
environmentMetrics = environmentMetrics {},
|
||||
paxcounter = paxcount {},
|
||||
)
|
||||
|
||||
private val almostNothing = Node(
|
||||
num = Random.nextInt(),
|
||||
)
|
||||
private val almostNothing = Node(num = Random.nextInt())
|
||||
|
||||
override val values: Sequence<Node>
|
||||
get() = sequenceOf(
|
||||
mickeyMouse, // "this" node
|
||||
unknown,
|
||||
almostNothing,
|
||||
minnieMouse,
|
||||
donaldDuck
|
||||
)
|
||||
get() =
|
||||
sequenceOf(
|
||||
mickeyMouse, // "this" node
|
||||
unknown,
|
||||
almostNothing,
|
||||
minnieMouse,
|
||||
donaldDuck,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,6 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.Node
|
||||
|
|
@ -108,6 +107,7 @@ import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
|||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.strings.R
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
|
|
@ -58,6 +57,7 @@ import kotlinx.coroutines.FlowPreview
|
|||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.ui.common.components.EmojiPickerDialog
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.Node
|
||||
|
|
@ -61,6 +60,7 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
|
|||
import com.geeksville.mesh.ui.common.theme.MessageItemColors
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.EnvironmentMetrics
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||
import com.geeksville.mesh.copy
|
||||
|
|
@ -60,7 +59,7 @@ import com.geeksville.mesh.ui.common.components.OptionLabel
|
|||
import com.geeksville.mesh.ui.common.components.SlidingSelector
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
|
||||
import com.geeksville.mesh.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ import com.geeksville.mesh.TelemetryProtos
|
|||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.util.formatUptime
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.R
|
||||
import java.text.DecimalFormat
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ import com.geeksville.mesh.model.MetricsViewModel
|
|||
import com.geeksville.mesh.model.TimeFrame
|
||||
import com.geeksville.mesh.ui.common.components.OptionLabel
|
||||
import com.geeksville.mesh.ui.common.components.SlidingSelector
|
||||
import com.geeksville.mesh.util.formatUptime
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.R
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
|
|
|||
|
|
@ -122,11 +122,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.database.entity.FirmwareRelease
|
||||
import com.geeksville.mesh.database.entity.asDeviceVersion
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.MetricsState
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.Node
|
||||
|
|
@ -148,16 +146,18 @@ import com.geeksville.mesh.ui.settings.components.SettingsItem
|
|||
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import com.geeksville.mesh.util.UnitConversions
|
||||
import com.geeksville.mesh.util.UnitConversions.toTempString
|
||||
import com.geeksville.mesh.util.formatAgo
|
||||
import com.geeksville.mesh.util.formatUptime
|
||||
import com.geeksville.mesh.util.thenIf
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import com.geeksville.mesh.util.toSmallDistanceString
|
||||
import com.geeksville.mesh.util.toSpeedString
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
import org.meshtastic.core.model.util.UnitConversions.toTempString
|
||||
import org.meshtastic.core.model.util.formatAgo
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.ConnectionState
|
||||
|
|
@ -59,6 +57,8 @@ import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
|||
import com.geeksville.mesh.ui.sharing.AddContactFAB
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.util.formatAgo
|
||||
import org.meshtastic.core.model.util.formatAgo
|
||||
|
||||
@Composable
|
||||
fun LastHeardInfo(modifier: Modifier = Modifier, lastHeard: Int, currentTimeMillis: Long) {
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ import androidx.core.net.toUri
|
|||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.ui.common.theme.HyperlinkBlue
|
||||
import com.geeksville.mesh.util.GPSFormat
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.util.GPSFormat
|
||||
import java.net.URLEncoder
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
|
|
|||
|
|
@ -25,14 +25,12 @@ import com.geeksville.mesh.IMeshService
|
|||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.util.positionToMeter
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -48,6 +46,8 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.positionToMeter
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import java.io.BufferedWriter
|
||||
import java.io.FileNotFoundException
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import com.geeksville.mesh.IMeshService
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.ModuleConfigProtos
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.isAnalyticsAvailable
|
||||
|
|
@ -74,6 +73,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
|
||||
import org.meshtastic.core.prefs.map.MapConsentPrefs
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ import androidx.navigation.NavController
|
|||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
|
||||
import com.geeksville.mesh.channelSettings
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.ui.common.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.common.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.common.components.SecurityIcon
|
||||
|
|
@ -82,6 +81,7 @@ import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed
|
|||
import com.geeksville.mesh.ui.common.components.rememberDragDropState
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import androidx.navigation.NavController
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.ConfigProtos.Config.PositionConfig
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.ui.common.components.BitwisePreference
|
||||
|
|
@ -53,6 +52,7 @@ import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
|||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
|
|
|
|||
|
|
@ -30,14 +30,13 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import com.geeksville.mesh.ui.common.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.common.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.common.components.RegularPreference
|
||||
import com.geeksville.mesh.ui.common.components.SwitchPreference
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import com.geeksville.mesh.user
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import com.geeksville.mesh.AdminProtos
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.errormsg
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.common.components.CopyIconButton
|
||||
|
|
@ -70,6 +69,7 @@ import com.google.zxing.WriterException
|
|||
import com.journeyapps.barcodescanner.BarcodeEncoder
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.R
|
||||
import timber.log.Timber
|
||||
import java.net.MalformedURLException
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.util
|
||||
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// return time if within 24 hours, otherwise date
|
||||
fun getShortDate(time: Long): String? {
|
||||
val date = if (time != 0L) Date(time) else return null
|
||||
val isWithin24Hours = System.currentTimeMillis() - date.time <= TimeUnit.DAYS.toMillis(1)
|
||||
|
||||
return if (isWithin24Hours) {
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
|
||||
} else {
|
||||
DateFormat.getDateInstance(DateFormat.SHORT).format(date)
|
||||
}
|
||||
}
|
||||
|
||||
// return time if within 24 hours, otherwise date/time
|
||||
fun getShortDateTime(time: Long): String {
|
||||
val date = Date(time)
|
||||
val isWithin24Hours = System.currentTimeMillis() - date.time <= TimeUnit.DAYS.toMillis(1)
|
||||
|
||||
return if (isWithin24Hours) {
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
|
||||
} else {
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatUptime(seconds: Int): String = formatUptime(seconds.toLong())
|
||||
|
||||
private fun formatUptime(seconds: Long): String {
|
||||
val days = TimeUnit.SECONDS.toDays(seconds)
|
||||
val hours = TimeUnit.SECONDS.toHours(seconds) % TimeUnit.DAYS.toHours(1)
|
||||
val minutes = TimeUnit.SECONDS.toMinutes(seconds) % TimeUnit.HOURS.toMinutes(1)
|
||||
val secs = seconds % TimeUnit.MINUTES.toSeconds(1)
|
||||
|
||||
return listOfNotNull(
|
||||
"${days}d".takeIf { days > 0 },
|
||||
"${hours}h".takeIf { hours > 0 },
|
||||
"${minutes}m".takeIf { minutes > 0 },
|
||||
"${secs}s".takeIf { secs > 0 },
|
||||
).joinToString(" ")
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun onlineTimeThreshold() = (System.currentTimeMillis() / 1000 - 2 * 60 * 60).toInt()
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.util
|
||||
|
||||
import android.widget.EditText
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
|
||||
/**
|
||||
* When printing strings to logs sometimes we want to print useful debugging information about users or positions. But
|
||||
* we don't want to leak things like usernames or locations. So this function if given a string, will return a string
|
||||
* which is a maximum of three characters long, taken from the tail of the string. Which should effectively hide real
|
||||
* usernames and locations, but still let us see if values were zero, empty or different.
|
||||
*/
|
||||
val Any?.anonymize: String
|
||||
get() = this.anonymize()
|
||||
|
||||
/** A version of anonymize that allows passing in a custom minimum length */
|
||||
fun Any?.anonymize(maxLen: Int = 3) = if (this != null) ("..." + this.toString().takeLast(maxLen)) else "null"
|
||||
|
||||
// A toString that makes sure all newlines are removed (for nice logging).
|
||||
fun Any.toOneLineString() = this.toString().replace('\n', ' ')
|
||||
|
||||
fun ConfigProtos.Config.toOneLineString(): String {
|
||||
val redactedFields = """(wifi_psk:|public_key:|private_key:|admin_key:)\s*".*"""
|
||||
return this.toString()
|
||||
.replace(redactedFields.toRegex()) { "${it.groupValues[1]} \"[REDACTED]\"" }
|
||||
.replace('\n', ' ')
|
||||
}
|
||||
|
||||
fun MeshProtos.toOneLineString(): String {
|
||||
val redactedFields = """(public_key:|private_key:|admin_key:)\s*".*""" // Redact keys
|
||||
return this.toString()
|
||||
.replace(redactedFields.toRegex()) { "${it.groupValues[1]} \"[REDACTED]\"" }
|
||||
.replace('\n', ' ')
|
||||
}
|
||||
|
||||
// Return a one line string version of an object (but if a release build, just say 'might be PII)
|
||||
fun Any.toPIIString() = if (!BuildConfig.DEBUG) {
|
||||
"<PII?>"
|
||||
} else {
|
||||
this.toOneLineString()
|
||||
}
|
||||
|
||||
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
|
||||
|
||||
fun formatAgo(lastSeenUnix: Int, currentTimeMillis: Long = System.currentTimeMillis()): String {
|
||||
val currentTime = (currentTimeMillis / 1000).toInt()
|
||||
val diffMin = (currentTime - lastSeenUnix) / 60
|
||||
return when {
|
||||
diffMin < 1 -> "now"
|
||||
diffMin < 60 -> diffMin.toString() + " min"
|
||||
diffMin < 2880 -> (diffMin / 60).toString() + " h"
|
||||
diffMin < 1440000 -> (diffMin / (60 * 24)).toString() + " d"
|
||||
else -> "?"
|
||||
}
|
||||
}
|
||||
|
||||
private const val MPS_TO_KMPH = 3.6f
|
||||
private const val KM_TO_MILES = 0.621371f
|
||||
|
||||
fun Int.mpsToKmph(): Float {
|
||||
// Convert meters per second to kilometers per hour
|
||||
val kmph = this * MPS_TO_KMPH
|
||||
return kmph
|
||||
}
|
||||
|
||||
fun Int.mpsToMph(): Float {
|
||||
// Convert meters per second to miles per hour
|
||||
val mph = this * MPS_TO_KMPH * KM_TO_MILES
|
||||
return mph
|
||||
}
|
||||
|
||||
// Allows usage like email.onEditorAction(EditorInfo.IME_ACTION_NEXT, { confirm() })
|
||||
fun EditText.onEditorAction(actionId: Int, func: () -> Unit) {
|
||||
setOnEditorActionListener { _, receivedActionId, _ ->
|
||||
if (actionId == receivedActionId) {
|
||||
func()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.geeksville.mesh.Position
|
||||
import java.util.Locale
|
||||
import kotlin.math.asin
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@SuppressLint("PropertyNaming")
|
||||
object GPSFormat {
|
||||
fun toDec(latitude: Double, longitude: Double): String =
|
||||
String.format(Locale.getDefault(), "%.5f, %.5f", latitude, longitude)
|
||||
}
|
||||
|
||||
private const val EARTH_RADIUS_METERS = 6371e3
|
||||
|
||||
/** @return distance in meters along the surface of the earth (ish) */
|
||||
fun latLongToMeter(latitudeA: Double, longitudeA: Double, latitudeB: Double, longitudeB: Double): Double {
|
||||
val lat1 = Math.toRadians(latitudeA)
|
||||
val lon1 = Math.toRadians(longitudeA)
|
||||
val lat2 = Math.toRadians(latitudeB)
|
||||
val lon2 = Math.toRadians(longitudeB)
|
||||
|
||||
val dLat = lat2 - lat1
|
||||
val dLon = lon2 - lon1
|
||||
|
||||
val a = sin(dLat / 2).pow(2) + cos(lat1) * cos(lat2) * sin(dLon / 2).pow(2)
|
||||
val c = 2 * asin(sqrt(a))
|
||||
|
||||
return EARTH_RADIUS_METERS * c
|
||||
}
|
||||
|
||||
// Same as above, but takes Mesh Position proto.
|
||||
fun positionToMeter(a: Position, b: Position): Double =
|
||||
latLongToMeter(a.latitude * 1e-7, a.longitude * 1e-7, b.latitude * 1e-7, b.longitude * 1e-7)
|
||||
|
||||
/**
|
||||
* Computes the bearing in degrees between two points on Earth.
|
||||
*
|
||||
* @param lat1 Latitude of the first point
|
||||
* @param lon1 Longitude of the first point
|
||||
* @param lat2 Latitude of the second point
|
||||
* @param lon2 Longitude of the second point
|
||||
* @return Bearing between the two points in degrees. A value of 0 means due north.
|
||||
*/
|
||||
fun bearing(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
|
||||
val lat1Rad = Math.toRadians(lat1)
|
||||
val lon1Rad = Math.toRadians(lon1)
|
||||
val lat2Rad = Math.toRadians(lat2)
|
||||
val lon2Rad = Math.toRadians(lon2)
|
||||
|
||||
val dLon = lon2Rad - lon1Rad
|
||||
|
||||
val y = sin(dLon) * cos(lat2Rad)
|
||||
val x = cos(lat1Rad) * sin(lat2Rad) - sin(lat1Rad) * cos(lat2Rad) * cos(dLon)
|
||||
val bearing = Math.toDegrees(atan2(y, x))
|
||||
|
||||
return (bearing + 360) % 360
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.util
|
||||
|
||||
import kotlin.math.ln
|
||||
|
||||
object UnitConversions {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun celsiusToFahrenheit(celsius: Float): Float {
|
||||
return (celsius * 1.8F) + 32
|
||||
}
|
||||
|
||||
fun Float.toTempString(isFahrenheit: Boolean) = if (isFahrenheit) {
|
||||
val fahrenheit = celsiusToFahrenheit(this)
|
||||
"%.0f°F".format(fahrenheit)
|
||||
} else {
|
||||
"%.0f°C".format(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculated the dew point based on the Magnus-Tetens approximation which is a widely used
|
||||
* formula for calculating dew point temperature.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun calculateDewPoint(tempCelsius: Float, humidity: Float): Float {
|
||||
val (a, b) = 17.27f to 237.7f
|
||||
val alpha = (a * tempCelsius) / (b + tempCelsius) + ln(humidity / 100f)
|
||||
return (b * alpha) / (a - alpha)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue