mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-05 06:33:52 -04:00
Merge branch 'master' into master
This commit is contained in:
@@ -65,8 +65,7 @@ data class DataPacket(
|
||||
parcel.readInt(),
|
||||
parcel.readParcelable(MessageStatus::class.java.classLoader),
|
||||
parcel.readInt()
|
||||
) {
|
||||
}
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
@@ -363,7 +363,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
rater.monitor() // Monitors the app launch times
|
||||
|
||||
// Only ask to rate if the user has a suitable store
|
||||
AppRate.showRateDialogIfMeetsConditions(this); // Shows the Rate Dialog when conditions are met
|
||||
AppRate.showRateDialogIfMeetsConditions(this) // Shows the Rate Dialog when conditions are met
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,9 +489,11 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
}
|
||||
|
||||
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||
val device: UsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
|
||||
debug("Handle USB device attached! $device")
|
||||
usbDevice = device
|
||||
val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
|
||||
if (device != null) {
|
||||
debug("Handle USB device attached! $device")
|
||||
usbDevice = device
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_MAIN -> {
|
||||
@@ -596,7 +598,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
filter.addAction(MeshService.actionReceived(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE))
|
||||
filter.addAction((MeshService.ACTION_MESSAGE_STATUS))
|
||||
registerReceiver(meshServiceReceiver, filter)
|
||||
receiverRegistered = true;
|
||||
receiverRegistered = true
|
||||
}
|
||||
|
||||
private fun unregisterMeshReceiver() {
|
||||
@@ -663,30 +665,32 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
|
||||
debug("Getting latest radioconfig from service")
|
||||
try {
|
||||
val info = service.myNodeInfo
|
||||
val info: MyNodeInfo? = service.myNodeInfo // this can be null
|
||||
model.myNodeInfo.value = info
|
||||
|
||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
||||
if (isOld)
|
||||
showAlert(R.string.app_too_old, R.string.must_update)
|
||||
else {
|
||||
|
||||
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
|
||||
if (curVer < MeshService.minFirmwareVersion)
|
||||
showAlert(R.string.firmware_too_old, R.string.firmware_old)
|
||||
if (info != null) {
|
||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
||||
if (isOld)
|
||||
showAlert(R.string.app_too_old, R.string.must_update)
|
||||
else {
|
||||
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
|
||||
|
||||
model.radioConfig.value =
|
||||
RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
|
||||
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
|
||||
if (curVer < MeshService.minFirmwareVersion)
|
||||
showAlert(R.string.firmware_too_old, R.string.firmware_old)
|
||||
else {
|
||||
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
|
||||
|
||||
model.channels.value =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
|
||||
model.radioConfig.value =
|
||||
RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
|
||||
|
||||
updateNodesFromDevice()
|
||||
model.channels.value =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
|
||||
|
||||
// we have a connection to our device now, do the channel change
|
||||
perhapsChangeChannel()
|
||||
updateNodesFromDevice()
|
||||
|
||||
// we have a connection to our device now, do the channel change
|
||||
perhapsChangeChannel()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: RemoteException) {
|
||||
@@ -799,13 +803,10 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
}
|
||||
|
||||
MeshService.ACTION_MESH_CONNECTED -> {
|
||||
val connected =
|
||||
MeshService.ConnectionState.valueOf(
|
||||
intent.getStringExtra(
|
||||
EXTRA_CONNECTED
|
||||
)!!
|
||||
)
|
||||
onMeshConnectionChanged(connected)
|
||||
val extra = intent.getStringExtra(EXTRA_CONNECTED)
|
||||
if (extra != null) {
|
||||
onMeshConnectionChanged(MeshService.ConnectionState.valueOf(extra))
|
||||
}
|
||||
}
|
||||
else -> TODO()
|
||||
}
|
||||
@@ -972,12 +973,11 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
|
||||
try {
|
||||
bindMeshService()
|
||||
}
|
||||
catch(ex: BindFailedException) {
|
||||
} catch (ex: BindFailedException) {
|
||||
// App is probably shutting down, ignore
|
||||
errormsg("Bind of MeshService failed")
|
||||
}
|
||||
|
||||
|
||||
val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
|
||||
if (!bonded && usbDevice == null) // we will handle USB later
|
||||
showSettingsPage()
|
||||
@@ -1090,7 +1090,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
applicationContext.contentResolver.openFileDescriptor(file_uri, "w")?.use {
|
||||
FileOutputStream(it.fileDescriptor).use { fs ->
|
||||
// Write header
|
||||
fs.write(("from,rssi,snr,time,dist\n").toByteArray());
|
||||
fs.write(("from,rssi,snr,time,dist\n").toByteArray())
|
||||
// Packets are ordered by time, we keep most recent position of
|
||||
// our device in my_position.
|
||||
var my_position: MeshProtos.Position? = null
|
||||
@@ -1101,8 +1101,12 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||
my_position = position
|
||||
} else if (my_position != null) {
|
||||
val dist = positionToMeter(my_position!!, position).roundToInt()
|
||||
fs.write("%x,%d,%f,%d,%d\n".format(packet_proto.from,packet_proto.rxRssi,
|
||||
packet_proto.rxSnr, packet_proto.rxTime, dist).toByteArray())
|
||||
fs.write(
|
||||
"%x,%d,%f,%d,%d\n".format(
|
||||
packet_proto.from, packet_proto.rxRssi,
|
||||
packet_proto.rxSnr, packet_proto.rxTime, dist
|
||||
).toByteArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,13 @@ class MeshUtilApplication : GeeksvilleApplication() {
|
||||
}
|
||||
|
||||
fun sendCrashReports() {
|
||||
if(isAnalyticsAllowed)
|
||||
if (isAnalyticsAllowed)
|
||||
crashlytics.sendUnsentReports()
|
||||
}
|
||||
|
||||
// Send any old reports if user approves
|
||||
sendCrashReports()
|
||||
|
||||
|
||||
// Attach to our exception wrapper
|
||||
Exceptions.reporter = { exception, _, _ ->
|
||||
crashlytics.recordException(exception)
|
||||
|
||||
@@ -32,8 +32,7 @@ data class MyNodeInfo(
|
||||
parcel.readInt(),
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
) {
|
||||
}
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeInt(myNodeNum)
|
||||
|
||||
@@ -43,7 +43,7 @@ data class Position(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Int,
|
||||
val time: Int = currentTime(), // default to current time in secs
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val batteryPctLevel: Int = 0
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
@@ -84,11 +84,13 @@ data class NodeInfo(
|
||||
var user: MeshUser? = null,
|
||||
var position: Position? = null,
|
||||
var snr: Float = Float.MAX_VALUE,
|
||||
var rssi: Int = Int.MAX_VALUE
|
||||
var rssi: Int = Int.MAX_VALUE,
|
||||
var lastHeard: Int = 0 // the last time we've seen this node in secs since 1970
|
||||
) : Parcelable {
|
||||
|
||||
/// Return the last time we've seen this node in secs since 1970
|
||||
val lastSeen get() = position?.time ?: 0
|
||||
/**
|
||||
* Return the last time we've seen this node in secs since 1970
|
||||
*/
|
||||
|
||||
val batteryPctLevel get() = position?.batteryPctLevel
|
||||
|
||||
@@ -104,7 +106,7 @@ data class NodeInfo(
|
||||
// FIXME - use correct timeout from the device settings
|
||||
val timeout =
|
||||
15 * 60 // Don't set this timeout too tight, or otherwise we will stop sending GPS helper positions to our device
|
||||
return (now - lastSeen <= timeout) || lastSeen == 0
|
||||
return (now - lastHeard <= timeout) || lastHeard == 0
|
||||
}
|
||||
|
||||
/// return the position if it is valid, else null
|
||||
|
||||
@@ -7,9 +7,7 @@ import com.google.protobuf.ByteString
|
||||
fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
|
||||
|
||||
|
||||
data class Channel(
|
||||
val settings: ChannelProtos.ChannelSettings = ChannelProtos.ChannelSettings.getDefaultInstance()
|
||||
) {
|
||||
data class Channel(val settings: ChannelProtos.ChannelSettings) {
|
||||
companion object {
|
||||
// These bytes must match the well known and not secret bytes used the default channel AES128 key device code
|
||||
val channelDefaultKey = byteArrayOfInts(
|
||||
@@ -17,10 +15,16 @@ data class Channel(
|
||||
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf
|
||||
)
|
||||
|
||||
private val cleartextPSK = ByteString.EMPTY
|
||||
private val defaultPSK =
|
||||
byteArrayOfInts(1) // a shortstring code to indicate we need our default PSK
|
||||
|
||||
// TH=he unsecured channel that devices ship with
|
||||
val defaultChannel = Channel(
|
||||
val default = Channel(
|
||||
ChannelProtos.ChannelSettings.newBuilder()
|
||||
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
|
||||
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096)
|
||||
.setPsk(ByteString.copyFrom(defaultPSK))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +54,7 @@ data class Channel(
|
||||
val pskIndex = settings.psk.byteAt(0).toInt()
|
||||
|
||||
if (pskIndex == 0)
|
||||
ByteString.EMPTY // Treat as an empty PSK (no encryption)
|
||||
cleartextPSK
|
||||
else {
|
||||
// Treat an index of 1 as the old channelDefaultKey and work up from there
|
||||
val bytes = channelDefaultKey.clone()
|
||||
@@ -73,6 +77,10 @@ data class Channel(
|
||||
|
||||
return "#${name}-${suffix}"
|
||||
}
|
||||
|
||||
override fun equals(o: Any?): Boolean = (o is Channel)
|
||||
&& psk.toByteArray() contentEquals o.psk.toByteArray()
|
||||
&& name == o.name
|
||||
}
|
||||
|
||||
fun xorHash(b: ByteArray) = b.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) })
|
||||
@@ -1,14 +1,29 @@
|
||||
package com.geeksville.mesh.model
|
||||
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
|
||||
enum class ChannelOption(val modemConfig: ChannelProtos.ChannelSettings.ModemConfig, val configRes: Int, val minBroadcastPeriodSecs: Int) {
|
||||
enum class ChannelOption(
|
||||
val modemConfig: ChannelProtos.ChannelSettings.ModemConfig,
|
||||
val configRes: Int,
|
||||
val minBroadcastPeriodSecs: Int
|
||||
) {
|
||||
SHORT(ChannelProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128, R.string.modem_config_short, 3),
|
||||
MEDIUM(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128, R.string.modem_config_medium, 12),
|
||||
LONG(ChannelProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512, R.string.modem_config_long, 240),
|
||||
VERY_LONG(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096, R.string.modem_config_very_long, 375);
|
||||
MEDIUM(
|
||||
ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128,
|
||||
R.string.modem_config_medium,
|
||||
12
|
||||
),
|
||||
LONG(
|
||||
ChannelProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512,
|
||||
R.string.modem_config_long,
|
||||
240
|
||||
),
|
||||
VERY_LONG(
|
||||
ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096,
|
||||
R.string.modem_config_very_long,
|
||||
375
|
||||
);
|
||||
|
||||
companion object {
|
||||
fun fromConfig(modemConfig: ChannelProtos.ChannelSettings.ModemConfig?): ChannelOption? {
|
||||
@@ -18,6 +33,7 @@ enum class ChannelOption(val modemConfig: ChannelProtos.ChannelSettings.ModemCon
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val defaultMinBroadcastPeriod = VERY_LONG.minBroadcastPeriodSecs
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.journeyapps.barcodescanner.BarcodeEncoder
|
||||
@@ -42,11 +40,12 @@ data class ChannelSet(
|
||||
/**
|
||||
* Return the primary channel info
|
||||
*/
|
||||
val primaryChannel: Channel? get() =
|
||||
if(protobuf.settingsCount > 0)
|
||||
Channel(protobuf.getSettings(0))
|
||||
else
|
||||
null
|
||||
val primaryChannel: Channel?
|
||||
get() =
|
||||
if (protobuf.settingsCount > 0)
|
||||
Channel(protobuf.getSettings(0))
|
||||
else
|
||||
null
|
||||
|
||||
/// Return an URL that represents the current channel values
|
||||
/// @param upperCasePrefix - portions of the URL can be upper case to make for more efficient QR codes
|
||||
@@ -56,7 +55,7 @@ data class ChannelSet(
|
||||
val channelBytes = protobuf.toByteArray() ?: ByteArray(0) // if unset just use empty
|
||||
val enc = Base64.encodeToString(channelBytes, base64Flags)
|
||||
|
||||
val p = if(upperCasePrefix)
|
||||
val p = if (upperCasePrefix)
|
||||
prefix.toUpperCase()
|
||||
else
|
||||
prefix
|
||||
@@ -68,7 +67,12 @@ data class ChannelSet(
|
||||
|
||||
// We encode as UPPER case for the QR code URL because QR codes are more efficient for that special case
|
||||
val bitMatrix =
|
||||
multiFormatWriter.encode(getChannelUrl(true).toString(), BarcodeFormat.QR_CODE, 192, 192);
|
||||
multiFormatWriter.encode(
|
||||
getChannelUrl(true).toString(),
|
||||
BarcodeFormat.QR_CODE,
|
||||
192,
|
||||
192
|
||||
)
|
||||
val barcodeEncoder = BarcodeEncoder()
|
||||
return barcodeEncoder.createBitmap(bitMatrix)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@ import com.geeksville.android.Logging
|
||||
/**
|
||||
* Provide structured access to parse and compare device version strings
|
||||
*/
|
||||
data class DeviceVersion(val asString: String): Comparable<DeviceVersion>, Logging {
|
||||
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
|
||||
}
|
||||
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
|
||||
|
||||
@@ -12,7 +12,9 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.RadioConfigProtos
|
||||
import com.geeksville.mesh.database.MeshtasticDatabase
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
@@ -134,15 +136,11 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
|
||||
}
|
||||
}
|
||||
|
||||
var region: RadioConfigProtos.RegionCode?
|
||||
get() = radioConfig.value?.preferences?.region
|
||||
var region: RadioConfigProtos.RegionCode
|
||||
get() = meshService?.region?.let { RadioConfigProtos.RegionCode.forNumber(it) }
|
||||
?: RadioConfigProtos.RegionCode.Unset
|
||||
set(value) {
|
||||
val config = radioConfig.value
|
||||
if (value != null && config != null) {
|
||||
val builder = config.toBuilder()
|
||||
builder.preferencesBuilder.region = value
|
||||
setRadioConfig(builder.build())
|
||||
}
|
||||
meshService?.region = value.number
|
||||
}
|
||||
|
||||
/// hardware info about our local device (can be null)
|
||||
|
||||
@@ -5,4 +5,8 @@ import java.util.*
|
||||
|
||||
open class BLEException(msg: String) : IOException(msg)
|
||||
|
||||
open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid")
|
||||
open class BLECharacteristicNotFoundException(uuid: UUID) :
|
||||
BLEException("Can't get characteristic $uuid")
|
||||
|
||||
/// Our interface is being shut down
|
||||
open class BLEConnectionClosing : BLEException("Connection closing ")
|
||||
@@ -78,7 +78,15 @@ A variable keepAllPackets, if set to true will suppress this behavior and instea
|
||||
class BluetoothInterface(val service: RadioInterfaceService, val address: String) : IRadioInterface,
|
||||
Logging {
|
||||
|
||||
companion object : Logging {
|
||||
companion object : Logging, InterfaceFactory('x') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = BluetoothInterface(service, rest)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
|
||||
/// this service UUID is publically visible for scanning
|
||||
val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")
|
||||
@@ -97,15 +105,13 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
||||
return bluetoothManager.adapter
|
||||
}
|
||||
|
||||
fun toInterfaceName(deviceName: String) = "x$deviceName"
|
||||
|
||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||
fun addressValid(context: Context, address: String): Boolean {
|
||||
override fun addressValid(context: Context, rest: String): Boolean {
|
||||
val allPaired =
|
||||
getBluetoothAdapter(context)?.bondedDevices.orEmpty().map { it.address }.toSet()
|
||||
|
||||
return if (!allPaired.contains(address)) {
|
||||
warn("Ignoring stale bond to ${address.anonymize}")
|
||||
return if (!allPaired.contains(rest)) {
|
||||
warn("Ignoring stale bond to ${rest.anonymize}")
|
||||
false
|
||||
} else
|
||||
true
|
||||
@@ -313,7 +319,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
||||
var fromNumChanged = false
|
||||
|
||||
private fun startWatchingFromNum() {
|
||||
safe!!.setNotify(fromNum, true) {
|
||||
safe?.setNotify(fromNum, true) {
|
||||
// We might get multiple notifies before we get around to reading from the radio - so just set one flag
|
||||
fromNumChanged = true
|
||||
debug("fromNum changed")
|
||||
@@ -469,7 +475,11 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
||||
safe =
|
||||
null // We do this first, because if we throw we still want to mark that we no longer have a valid connection
|
||||
|
||||
s?.close()
|
||||
try {
|
||||
s?.close()
|
||||
} catch (_: BLEConnectionClosing) {
|
||||
warn("Ignoring BLE errors while closing")
|
||||
}
|
||||
} else {
|
||||
debug("Radio was not connected, skipping disable")
|
||||
}
|
||||
|
||||
@@ -26,5 +26,9 @@ class BluetoothStateReceiver(
|
||||
}
|
||||
}
|
||||
|
||||
private val Intent.bluetoothAdapterState: Int get() = getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)
|
||||
private val Intent.bluetoothAdapterState: Int
|
||||
get() = getIntExtra(
|
||||
BluetoothAdapter.EXTRA_STATE,
|
||||
-1
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* A base class for the singleton factories that make interfaces. One instance per interface type
|
||||
*/
|
||||
abstract class InterfaceFactory(val prefix: Char) {
|
||||
companion object {
|
||||
private val factories = mutableMapOf<Char, InterfaceFactory>()
|
||||
|
||||
fun getFactory(l: Char) = factories.get(l)
|
||||
}
|
||||
|
||||
protected fun registerFactory() {
|
||||
factories[prefix] = this
|
||||
}
|
||||
|
||||
abstract fun createInterface(service: RadioInterfaceService, rest: String): IRadioInterface
|
||||
|
||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||
open fun addressValid(context: Context, rest: String): Boolean = true
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.*
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Handles all the communication with android apps. Also keeps an internal model
|
||||
@@ -55,7 +56,7 @@ class MeshService : Service(), Logging {
|
||||
/* @Deprecated(message = "Does not filter by port number. For legacy reasons only broadcast for UNKNOWN_APP, switch to ACTION_RECEIVED")
|
||||
const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA" */
|
||||
|
||||
fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
||||
private fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
||||
|
||||
/// generate a RECEIVED action filter string that includes either the portnumber as an int, or preferably a symbolic name from portnums.proto
|
||||
fun actionReceived(portNum: Int): String {
|
||||
@@ -70,7 +71,7 @@ class MeshService : Service(), Logging {
|
||||
const val ACTION_MESSAGE_STATUS = "$prefix.MESSAGE_STATUS"
|
||||
|
||||
open class NodeNotFoundException(reason: String) : Exception(reason)
|
||||
class InvalidNodeIdException() : NodeNotFoundException("Invalid NodeId")
|
||||
class InvalidNodeIdException : NodeNotFoundException("Invalid NodeId")
|
||||
class NodeNumNotFoundException(id: Int) : NodeNotFoundException("NodeNum not found $id")
|
||||
class IdNotFoundException(id: String) : NodeNotFoundException("ID not found $id")
|
||||
|
||||
@@ -78,7 +79,7 @@ class MeshService : Service(), Logging {
|
||||
RadioNotConnectedException(message)
|
||||
|
||||
/** We treat software update as similar to loss of comms to the regular bluetooth service (so things like sendPosition for background GPS ignores the problem */
|
||||
class IsUpdatingException() :
|
||||
class IsUpdatingException :
|
||||
RadioNotConnectedException("Operation prohibited during firmware update")
|
||||
|
||||
/**
|
||||
@@ -132,7 +133,7 @@ class MeshService : Service(), Logging {
|
||||
}
|
||||
|
||||
private val locationCallback = MeshServiceLocationCallback(
|
||||
::sendPositionScoped,
|
||||
::perhapsSendPosition,
|
||||
onSendPositionFailed = { onConnectionChanged(ConnectionState.DEVICE_SLEEP) },
|
||||
getNodeNum = { myNodeNum }
|
||||
)
|
||||
@@ -160,6 +161,36 @@ class MeshService : Service(), Logging {
|
||||
).show()
|
||||
}
|
||||
|
||||
private var locationIntervalMsec = 0L
|
||||
|
||||
/**
|
||||
* a periodic callback that perhaps send our position to other nodes.
|
||||
* We first check to see if our local device has already sent a position and if so, we punt until the next check.
|
||||
* This allows us to only 'fill in' with GPS positions when the local device happens to have no good GPS sats.
|
||||
*/
|
||||
private fun perhapsSendPosition(
|
||||
lat: Double = 0.0,
|
||||
lon: Double = 0.0,
|
||||
alt: Int = 0,
|
||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) {
|
||||
// This operation can take a while, so instead of staying in the callback (location services) context
|
||||
// do most of the work in my service thread
|
||||
serviceScope.handledLaunch {
|
||||
// if android called us too soon, just ignore
|
||||
|
||||
val myInfo = localNodeInfo
|
||||
val lastSendMsec = (myInfo?.position?.time ?: 0) * 1000L
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastSendMsec < locationIntervalMsec)
|
||||
debug("Not sending position - the local node has sent one recently...")
|
||||
else {
|
||||
sendPosition(lat, lon, alt, destNum, wantResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* start our location requests (if they weren't already running)
|
||||
*
|
||||
@@ -172,6 +203,7 @@ class MeshService : Service(), Logging {
|
||||
if (fusedLocationClient == null && isGooglePlayAvailable(this)) {
|
||||
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
|
||||
|
||||
locationIntervalMsec = requestInterval
|
||||
val request = LocationRequest.create().apply {
|
||||
interval = requestInterval
|
||||
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
|
||||
@@ -242,24 +274,31 @@ class MeshService : Service(), Logging {
|
||||
get() = (if (connectionState == ConnectionState.CONNECTED) radio.serviceP else null)
|
||||
?: throw RadioNotConnectedException()
|
||||
|
||||
/// Send a command/packet to our radio. But cope with the possiblity that we might start up
|
||||
/// before we are fully bound to the RadioInterfaceService
|
||||
private fun sendToRadio(p: ToRadio.Builder) {
|
||||
/** Send a command/packet to our radio. But cope with the possiblity that we might start up
|
||||
before we are fully bound to the RadioInterfaceService
|
||||
@param requireConnected set to false if you are okay with using a partially connected device (i.e. during startup)
|
||||
*/
|
||||
private fun sendToRadio(p: ToRadio.Builder, requireConnected: Boolean = true) {
|
||||
val b = p.build().toByteArray()
|
||||
|
||||
if (SoftwareUpdateService.isUpdating)
|
||||
throw IsUpdatingException()
|
||||
|
||||
connectedRadio.sendToRadio(b)
|
||||
if (requireConnected)
|
||||
connectedRadio.sendToRadio(b)
|
||||
else {
|
||||
val s = radio.serviceP ?: throw RadioNotConnectedException()
|
||||
s.sendToRadio(b)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a mesh packet to the radio, if the radio is not currently connected this function will throw NotConnectedException
|
||||
*/
|
||||
private fun sendToRadio(packet: MeshPacket) {
|
||||
private fun sendToRadio(packet: MeshPacket, requireConnected: Boolean = true) {
|
||||
sendToRadio(ToRadio.newBuilder().apply {
|
||||
this.packet = packet
|
||||
})
|
||||
}, requireConnected)
|
||||
}
|
||||
|
||||
private fun updateMessageNotification(message: DataPacket) =
|
||||
@@ -428,7 +467,7 @@ class MeshService : Service(), Logging {
|
||||
|
||||
private var radioConfig: RadioConfigProtos.RadioConfig? = null
|
||||
|
||||
private var channels = arrayOf<ChannelProtos.Channel>()
|
||||
private var channels = fixupChannelList(listOf())
|
||||
|
||||
/// True after we've done our initial node db init
|
||||
@Volatile
|
||||
@@ -454,6 +493,17 @@ class MeshService : Service(), Logging {
|
||||
n
|
||||
)
|
||||
|
||||
/**
|
||||
* Return the nodeinfo for the local node, or null if not found
|
||||
*/
|
||||
private val localNodeInfo
|
||||
get(): NodeInfo? =
|
||||
try {
|
||||
toNodeInfo(myNodeNum)
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
/** Map a nodenum to the nodeid string, or return null if not present
|
||||
If we have a NodeInfo for this ID we prefer to return the string ID inside the user record.
|
||||
but some nodes might not have a user record at all (because not yet received), in that case, we return
|
||||
@@ -500,9 +550,13 @@ class MeshService : Service(), Logging {
|
||||
}
|
||||
|
||||
/// A helper function that makes it easy to update node info objects
|
||||
private fun updateNodeInfo(nodeNum: Int, updatefn: (NodeInfo) -> Unit) {
|
||||
private fun updateNodeInfo(
|
||||
nodeNum: Int,
|
||||
withBroadcast: Boolean = true,
|
||||
updateFn: (NodeInfo) -> Unit
|
||||
) {
|
||||
val info = getOrCreateNodeInfo(nodeNum)
|
||||
updatefn(info)
|
||||
updateFn(info)
|
||||
|
||||
// This might have been the first time we know an ID for this node, so also update the by ID map
|
||||
val userId = info.user?.id.orEmpty()
|
||||
@@ -510,7 +564,8 @@ class MeshService : Service(), Logging {
|
||||
nodeDBbyID[userId] = info
|
||||
|
||||
// parcelable is busted
|
||||
serviceBroadcasts.broadcastNodeChange(info)
|
||||
if (withBroadcast)
|
||||
serviceBroadcasts.broadcastNodeChange(info)
|
||||
}
|
||||
|
||||
/// My node num
|
||||
@@ -549,7 +604,7 @@ class MeshService : Service(), Logging {
|
||||
setChannel(it)
|
||||
}
|
||||
|
||||
channels = asChannels.toTypedArray()
|
||||
channels = fixupChannelList(asChannels)
|
||||
}
|
||||
|
||||
/// Generate a new mesh packet builder with our node as the sender, and the specified node num
|
||||
@@ -716,7 +771,10 @@ class MeshService : Service(), Logging {
|
||||
|
||||
// Handle new style position info
|
||||
Portnums.PortNum.POSITION_APP_VALUE -> {
|
||||
val u = MeshProtos.Position.parseFrom(data.payload)
|
||||
var u = MeshProtos.Position.parseFrom(data.payload)
|
||||
// position updates from mesh usually don't include times. So promote rx time
|
||||
if (u.time == 0 && packet.rxTime != 0)
|
||||
u = u.toBuilder().setTime(packet.rxTime).build()
|
||||
debug("position_app ${packet.from} ${u.toOneLineString()}")
|
||||
handleReceivedPosition(packet.from, u, dataPacket.time)
|
||||
}
|
||||
@@ -781,6 +839,7 @@ class MeshService : Service(), Logging {
|
||||
val mi = myNodeInfo
|
||||
if (mi != null) {
|
||||
val ch = a.getChannelResponse
|
||||
// add new entries if needed
|
||||
channels[ch.index] = ch
|
||||
debug("Admin: Received channel ${ch.index}")
|
||||
if (ch.index + 1 < mi.maxChannels) {
|
||||
@@ -827,11 +886,16 @@ class MeshService : Service(), Logging {
|
||||
p: MeshProtos.Position,
|
||||
defaultTime: Long = System.currentTimeMillis()
|
||||
) {
|
||||
updateNodeInfo(fromNum) {
|
||||
debug("update ${it.user?.longName} with ${p.toOneLineString()}")
|
||||
it.position = Position(p)
|
||||
updateNodeInfoTime(it, (defaultTime / 1000).toInt())
|
||||
}
|
||||
// Nodes periodically send out position updates, but those updates might not contain a lat & lon (because no GPS lock)
|
||||
// We like to look at the local node to see if it has been sending out valid lat/lon, so for the LOCAL node (only)
|
||||
// we don't record these nop position updates
|
||||
if (myNodeNum == fromNum && p.latitudeI == 0 && p.longitudeI == 0)
|
||||
debug("Ignoring nop position update for the local node")
|
||||
else
|
||||
updateNodeInfo(fromNum) {
|
||||
debug("update position: ${it.user?.longName} with ${p.toOneLineString()}")
|
||||
it.position = Position(p, (defaultTime / 1000L).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
/// If packets arrive before we have our node DB, we delay parsing them until the DB is ready
|
||||
@@ -914,15 +978,17 @@ class MeshService : Service(), Logging {
|
||||
// Update last seen for the node that sent the packet, but also for _our node_ because anytime a packet passes
|
||||
// through our node on the way to the phone that means that local node is also alive in the mesh
|
||||
|
||||
updateNodeInfo(myNodeNum) {
|
||||
it.position = it.position?.copy(time = currentSecond())
|
||||
val isOtherNode = myNodeNum != fromNum
|
||||
updateNodeInfo(myNodeNum, withBroadcast = isOtherNode) {
|
||||
it.lastHeard = currentSecond()
|
||||
}
|
||||
|
||||
// if (p.hasPosition()) handleReceivedPosition(fromNum, p.position, rxTime)
|
||||
// Do not generate redundant broadcasts of node change for this bookkeeping updateNodeInfo call
|
||||
// because apps really only care about important updates of node state - which handledReceivedData will give them
|
||||
updateNodeInfo(fromNum, withBroadcast = false) {
|
||||
// If the rxTime was not set by the device (because device software was old), guess at a time
|
||||
val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond()
|
||||
|
||||
// If the rxTime was not set by the device (because device software was old), guess at a time
|
||||
val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond()
|
||||
updateNodeInfo(fromNum) {
|
||||
// Update our last seen based on any valid timestamps. If the device didn't provide a timestamp make one
|
||||
updateNodeInfoTime(it, rxTime)
|
||||
it.snr = packet.rxSnr
|
||||
@@ -946,28 +1012,32 @@ class MeshService : Service(), Logging {
|
||||
/// If we just changed our nodedb, we might want to do somethings
|
||||
private fun onNodeDBChanged() {
|
||||
maybeUpdateServiceStatusNotification()
|
||||
|
||||
serviceScope.handledLaunch(Dispatchers.Main) {
|
||||
setupLocationRequest()
|
||||
}
|
||||
}
|
||||
|
||||
private var locationRequestInterval: Long = 0;
|
||||
private fun setupLocationRequest() {
|
||||
val desiredInterval: Long = if (myNodeInfo?.hasGPS == true) {
|
||||
0L // no requests when device has GPS
|
||||
} else if (numOnlineNodes < 2) {
|
||||
5 * 60 * 1000L // send infrequently, device needs these requests to set its clock
|
||||
} else {
|
||||
radioConfig?.preferences?.positionBroadcastSecs?.times(1000L) ?: 5 * 60 * 1000L
|
||||
}
|
||||
stopLocationRequests()
|
||||
val mi = myNodeInfo
|
||||
val prefs = radioConfig?.preferences
|
||||
if (mi != null && prefs != null) {
|
||||
var broadcastSecs = prefs.positionBroadcastSecs
|
||||
|
||||
debug("desired location request $desiredInterval, current $locationRequestInterval")
|
||||
var desiredInterval = if (broadcastSecs == 0) // unset by device, use default
|
||||
15 * 60 * 1000L
|
||||
else
|
||||
broadcastSecs * 1000L
|
||||
|
||||
if (desiredInterval != locationRequestInterval) {
|
||||
if (locationRequestInterval > 0) stopLocationRequests()
|
||||
if (desiredInterval > 0) startLocationRequests(desiredInterval)
|
||||
locationRequestInterval = desiredInterval
|
||||
if (prefs.locationShare == RadioConfigProtos.LocationSharing.LocDisabled) {
|
||||
info("GPS location sharing is disabled")
|
||||
desiredInterval = 0
|
||||
}
|
||||
|
||||
if (desiredInterval != 0L) {
|
||||
info("desired GPS assistance interval $desiredInterval")
|
||||
startLocationRequests(desiredInterval)
|
||||
} else {
|
||||
info("No GPS assistance desired, but sending UTC time to mesh")
|
||||
sendPosition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,7 +1149,7 @@ class MeshService : Service(), Logging {
|
||||
// claim we have a valid connection still
|
||||
connectionState = ConnectionState.DEVICE_SLEEP
|
||||
startDeviceSleep()
|
||||
throw ex; // Important to rethrow so that we don't tell the app all is well
|
||||
throw ex // Important to rethrow so that we don't tell the app all is well
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1315,13 +1385,45 @@ class MeshService : Service(), Logging {
|
||||
radioConfig = null
|
||||
|
||||
// prefill the channel array with null channels
|
||||
channels = Array(myInfo.maxChannels) {
|
||||
val b = ChannelProtos.Channel.newBuilder()
|
||||
b.index = it
|
||||
b.build()
|
||||
}
|
||||
channels = fixupChannelList(listOf<ChannelProtos.Channel>())
|
||||
}
|
||||
|
||||
/// scan the channel list and make sure it has one PRIMARY channel and is maxChannels long
|
||||
private fun fixupChannelList(lIn: List<ChannelProtos.Channel>): Array<ChannelProtos.Channel> {
|
||||
// When updating old firmware, we will briefly be told that there is zero channels
|
||||
val maxChannels =
|
||||
max(myNodeInfo?.maxChannels ?: 8, 8) // If we don't have my node info, assume 8 channels
|
||||
var l = lIn
|
||||
while (l.size < maxChannels) {
|
||||
val b = ChannelProtos.Channel.newBuilder()
|
||||
b.index = l.size
|
||||
l += b.build()
|
||||
}
|
||||
return l.toTypedArray()
|
||||
}
|
||||
|
||||
|
||||
private fun setRegionOnDevice() {
|
||||
val curConfigRegion =
|
||||
radioConfig?.preferences?.region ?: RadioConfigProtos.RegionCode.Unset
|
||||
|
||||
if (curConfigRegion.number != curRegionValue && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE)
|
||||
if (deviceVersion >= minFirmwareVersion) {
|
||||
info("Telling device to upgrade region")
|
||||
|
||||
// Tell the device to set the new region field (old devices will simply ignore this)
|
||||
radioConfig?.let { currentConfig ->
|
||||
val newConfig = currentConfig.toBuilder()
|
||||
|
||||
val newPrefs = currentConfig.preferences.toBuilder()
|
||||
newPrefs.regionValue = curRegionValue
|
||||
newConfig.preferences = newPrefs.build()
|
||||
|
||||
sendRadioConfig(newConfig.build())
|
||||
}
|
||||
} else
|
||||
warn("Device is too old to understand region changes")
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are updating nodes we might need to use old (fixed by firmware build)
|
||||
@@ -1356,31 +1458,14 @@ class MeshService : Service(), Logging {
|
||||
}
|
||||
|
||||
// If nothing was set in our (new style radio preferences, but we now have a valid setting - slam it in)
|
||||
if (curConfigRegion == RadioConfigProtos.RegionCode.Unset && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE) {
|
||||
if (deviceVersion >= minFirmwareVersion) {
|
||||
info("Telling device to upgrade region")
|
||||
|
||||
// Tell the device to set the new region field (old devices will simply ignore this)
|
||||
radioConfig?.let { currentConfig ->
|
||||
val newConfig = currentConfig.toBuilder()
|
||||
|
||||
val newPrefs = currentConfig.preferences.toBuilder()
|
||||
newPrefs.regionValue = curRegionValue
|
||||
newConfig.preferences = newPrefs.build()
|
||||
|
||||
sendRadioConfig(newConfig.build())
|
||||
}
|
||||
}
|
||||
else
|
||||
warn("Device is too old to understand region changes")
|
||||
}
|
||||
setRegionOnDevice()
|
||||
}
|
||||
}
|
||||
|
||||
/// If we've received our initial config, our radio settings and all of our channels, send any queueed packets and broadcast connected to clients
|
||||
private fun onHasSettings() {
|
||||
|
||||
processEarlyPackets() // send receive any packets that were queued up
|
||||
processEarlyPackets() // send any packets that were queued up
|
||||
|
||||
// broadcast an intent with our new connection state
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
@@ -1388,6 +1473,8 @@ class MeshService : Service(), Logging {
|
||||
reportConnection()
|
||||
|
||||
updateRegion()
|
||||
|
||||
setupLocationRequest() // start sending location packets if needed
|
||||
}
|
||||
|
||||
private fun handleConfigComplete(configCompleteId: Int) {
|
||||
@@ -1432,13 +1519,13 @@ class MeshService : Service(), Logging {
|
||||
private fun requestRadioConfig() {
|
||||
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) {
|
||||
getRadioRequest = true
|
||||
})
|
||||
}, requireConnected = false)
|
||||
}
|
||||
|
||||
private fun requestChannel(channelIndex: Int) {
|
||||
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) {
|
||||
getChannelRequest = channelIndex + 1
|
||||
})
|
||||
}, requireConnected = false)
|
||||
}
|
||||
|
||||
private fun setChannel(channel: ChannelProtos.Channel) {
|
||||
@@ -1466,48 +1553,41 @@ class MeshService : Service(), Logging {
|
||||
* Must be called from serviceScope. Use sendPositionScoped() for direct calls.
|
||||
*/
|
||||
private fun sendPosition(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
alt: Int,
|
||||
lat: Double = 0.0,
|
||||
lon: Double = 0.0,
|
||||
alt: Int = 0,
|
||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) {
|
||||
debug("Sending our position to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
||||
|
||||
val position = MeshProtos.Position.newBuilder().also {
|
||||
it.longitudeI = Position.degI(lon)
|
||||
it.latitudeI = Position.degI(lat)
|
||||
|
||||
it.altitude = alt
|
||||
it.time = currentSecond() // Include our current timestamp
|
||||
}.build()
|
||||
|
||||
// Also update our own map for our nodenum, by handling the packet just like packets from other users
|
||||
handleReceivedPosition(myNodeInfo!!.myNodeNum, position)
|
||||
|
||||
val fullPacket =
|
||||
newMeshPacketTo(destNum).buildMeshPacket(priority = MeshProtos.MeshPacket.Priority.BACKGROUND) {
|
||||
// Use the new position as data format
|
||||
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
||||
payload = position.toByteString()
|
||||
|
||||
this.wantResponse = wantResponse
|
||||
}
|
||||
|
||||
// send the packet into the mesh
|
||||
sendToRadio(fullPacket)
|
||||
}
|
||||
|
||||
private fun sendPositionScoped(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
alt: Int,
|
||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) = serviceScope.handledLaunch {
|
||||
try {
|
||||
sendPosition(lat, lon, alt, destNum, wantResponse)
|
||||
} catch (ex: RadioNotConnectedException) {
|
||||
val mi = myNodeInfo
|
||||
if (mi != null) {
|
||||
debug("Sending our position/time to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
||||
|
||||
val position = MeshProtos.Position.newBuilder().also {
|
||||
it.longitudeI = Position.degI(lon)
|
||||
it.latitudeI = Position.degI(lat)
|
||||
|
||||
it.altitude = alt
|
||||
it.time = currentSecond() // Include our current timestamp
|
||||
}.build()
|
||||
|
||||
// Also update our own map for our nodenum, by handling the packet just like packets from other users
|
||||
handleReceivedPosition(mi.myNodeNum, position)
|
||||
|
||||
val fullPacket =
|
||||
newMeshPacketTo(destNum).buildMeshPacket(priority = MeshProtos.MeshPacket.Priority.BACKGROUND) {
|
||||
// Use the new position as data format
|
||||
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
||||
payload = position.toByteString()
|
||||
|
||||
this.wantResponse = wantResponse
|
||||
}
|
||||
|
||||
// send the packet into the mesh
|
||||
sendToRadio(fullPacket)
|
||||
}
|
||||
} catch (ex: BLEException) {
|
||||
warn("Ignoring disconnected radio during gps location update")
|
||||
}
|
||||
}
|
||||
@@ -1539,9 +1619,7 @@ class MeshService : Service(), Logging {
|
||||
val myNode = myNodeInfo
|
||||
if (myNode != null) {
|
||||
|
||||
|
||||
val myInfo = toNodeInfo(myNode.myNodeNum)
|
||||
if (longName == myInfo.user?.longName && shortName == myInfo.user?.shortName)
|
||||
if (longName == localNodeInfo?.user?.longName && shortName == localNodeInfo?.user?.shortName)
|
||||
debug("Ignoring nop owner change")
|
||||
else {
|
||||
debug("SetOwner $myId : ${longName.anonymize} : $shortName")
|
||||
@@ -1627,8 +1705,10 @@ class MeshService : Service(), Logging {
|
||||
} else {
|
||||
debug("Creating firmware update coroutine")
|
||||
updateJob = serviceScope.handledLaunch {
|
||||
debug("Starting firmware update coroutine")
|
||||
SoftwareUpdateService.doUpdate(this@MeshService, safe, filename)
|
||||
exceptionReporter {
|
||||
debug("Starting firmware update coroutine")
|
||||
SoftwareUpdateService.doUpdate(this@MeshService, safe, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1660,7 +1740,7 @@ class MeshService : Service(), Logging {
|
||||
offlineSentPackets.add(p)
|
||||
}
|
||||
|
||||
val binder = object : IMeshService.Stub() {
|
||||
private val binder = object : IMeshService.Stub() {
|
||||
|
||||
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
|
||||
debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
|
||||
@@ -1684,6 +1764,12 @@ class MeshService : Service(), Logging {
|
||||
}
|
||||
|
||||
override fun getUpdateStatus(): Int = SoftwareUpdateService.progress
|
||||
override fun getRegion(): Int = curRegionValue
|
||||
|
||||
override fun setRegion(regionCode: Int) = toRemoteExceptions {
|
||||
curRegionValue = regionCode
|
||||
setRegionOnDevice()
|
||||
}
|
||||
|
||||
override fun startFirmwareUpdate() = toRemoteExceptions {
|
||||
doFirmwareUpdate()
|
||||
@@ -1788,6 +1874,5 @@ class MeshService : Service(), Logging {
|
||||
}
|
||||
|
||||
fun updateNodeInfoTime(it: NodeInfo, rxTime: Int) {
|
||||
if (it.position?.time == null || it.position?.time!! < rxTime)
|
||||
it.position = it.position?.copy(time = rxTime)
|
||||
it.lastHeard = rxTime
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.Portnums
|
||||
|
||||
class MeshServiceBroadcasts(
|
||||
private val context: Context,
|
||||
@@ -18,7 +17,12 @@ class MeshServiceBroadcasts(
|
||||
*/
|
||||
fun broadcastReceivedData(payload: DataPacket) {
|
||||
|
||||
explicitBroadcast(Intent(MeshService.actionReceived(payload.dataType)).putExtra(EXTRA_PAYLOAD, payload))
|
||||
explicitBroadcast(
|
||||
Intent(MeshService.actionReceived(payload.dataType)).putExtra(
|
||||
EXTRA_PAYLOAD,
|
||||
payload
|
||||
)
|
||||
)
|
||||
|
||||
/*
|
||||
// For the time being we ALSO broadcast using old ACTION_RECEIVED_DATA field for any oldschool opaque packets
|
||||
|
||||
@@ -21,8 +21,7 @@ typealias GetNodeNum = () -> Int
|
||||
class MeshServiceLocationCallback(
|
||||
private val onSendPosition: SendPosition,
|
||||
private val onSendPositionFailed: OnSendFailure,
|
||||
private val getNodeNum: GetNodeNum,
|
||||
private val sendRateLimitInSeconds: Int = DEFAULT_SEND_RATE_LIMIT
|
||||
private val getNodeNum: GetNodeNum
|
||||
) : LocationCallback() {
|
||||
|
||||
companion object {
|
||||
@@ -40,7 +39,8 @@ class MeshServiceLocationCallback(
|
||||
|
||||
try {
|
||||
// Do we want to broadcast this position globally, or are we just telling the local node what its current position is (
|
||||
val shouldBroadcast = isAllowedToSend()
|
||||
val shouldBroadcast =
|
||||
true // no need to rate limit, because we are just sending at the interval requested by the preferences
|
||||
val destinationNumber =
|
||||
if (shouldBroadcast) DataPacket.NODENUM_BROADCAST else getNodeNum()
|
||||
|
||||
@@ -69,17 +69,4 @@ class MeshServiceLocationCallback(
|
||||
wantResponse // wantResponse?
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting function.
|
||||
*/
|
||||
private fun isAllowedToSend(): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
// we limit our sends onto the lora net to a max one once every FIXME
|
||||
val sendLora = (now - lastSendTimeMs >= sendRateLimitInSeconds * 1000)
|
||||
if (sendLora) {
|
||||
lastSendTimeMs = now
|
||||
}
|
||||
return sendLora
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,9 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MainActivity
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.notificationManager
|
||||
import com.geeksville.mesh.ui.SLogging
|
||||
import com.geeksville.mesh.utf8
|
||||
import java.io.Closeable
|
||||
|
||||
|
||||
@@ -29,6 +26,7 @@ class MeshServiceNotifications(
|
||||
private val context: Context
|
||||
) : Closeable {
|
||||
private val notificationManager: NotificationManager get() = context.notificationManager
|
||||
|
||||
// We have two notification channels: one for general service status and another one for messages
|
||||
val notifyId = 101
|
||||
private val messageNotifyId = 102
|
||||
@@ -96,12 +94,16 @@ class MeshServiceNotifications(
|
||||
}
|
||||
|
||||
fun updateServiceStateNotification(summaryString: String) =
|
||||
notificationManager.notify(notifyId,
|
||||
createServiceStateNotification(summaryString))
|
||||
notificationManager.notify(
|
||||
notifyId,
|
||||
createServiceStateNotification(summaryString)
|
||||
)
|
||||
|
||||
fun updateMessageNotification(name: String, message: String) =
|
||||
notificationManager.notify(messageNotifyId,
|
||||
createMessageNotifcation(name, message))
|
||||
notificationManager.notify(
|
||||
messageNotifyId,
|
||||
createMessageNotifcation(name, message)
|
||||
)
|
||||
|
||||
private val openAppIntent: PendingIntent by lazy {
|
||||
PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0)
|
||||
@@ -126,7 +128,7 @@ class MeshServiceNotifications(
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun commonBuilder(channel: String) : NotificationCompat.Builder {
|
||||
fun commonBuilder(channel: String): NotificationCompat.Builder {
|
||||
val builder = NotificationCompat.Builder(context, channel)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(openAppIntent)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.RadioConfigProtos
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/// Our saved preferences as stored on disk
|
||||
|
||||
@@ -3,18 +3,14 @@ package com.geeksville.mesh.service
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.*
|
||||
import com.geeksville.andlib.BuildConfig
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Helper that calls MeshService.startService()
|
||||
*/
|
||||
public class ServiceStarter(
|
||||
class ServiceStarter(
|
||||
appContext: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : Worker(appContext, workerParams) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.Context
|
||||
import com.geeksville.android.BuildUtils
|
||||
import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.model.getInitials
|
||||
@@ -8,9 +11,18 @@ import okhttp3.internal.toHexString
|
||||
|
||||
/** A simulated interface that is used for testing in the simulator */
|
||||
class MockInterface(private val service: RadioInterfaceService) : Logging, IRadioInterface {
|
||||
companion object : Logging {
|
||||
companion object : Logging, InterfaceFactory('m') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = MockInterface(service)
|
||||
|
||||
const val interfaceName = "m"
|
||||
override fun addressValid(context: Context, rest: String): Boolean =
|
||||
BuildUtils.isEmulator || ((context.applicationContext as GeeksvilleApplication).isInTestLab)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
}
|
||||
|
||||
private var messageCount = 50
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.android.Logging
|
||||
|
||||
class NopInterface : IRadioInterface {
|
||||
companion object : Logging, InterfaceFactory('n') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = NopInterface()
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSendToRadio(p: ByteArray) {
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ open class RadioNotConnectedException(message: String = "Not connected to radio"
|
||||
BLEException(message)
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handles the bluetooth link with a mesh radio device. Does not cache any device state,
|
||||
* just does bluetooth comms etc...
|
||||
@@ -50,9 +49,11 @@ class RadioInterfaceService : Service(), Logging {
|
||||
*/
|
||||
const val RADIO_CONNECTED_ACTION = "$prefix.CONNECT_CHANGED"
|
||||
|
||||
const val DEVADDR_KEY_OLD = "devAddr"
|
||||
const val DEVADDR_KEY = "devAddr2" // the new name for devaddr
|
||||
|
||||
/// We keep this var alive so that the following factory objects get created and not stripped during the android build
|
||||
private val factories = arrayOf<InterfaceFactory>(BluetoothInterface, SerialInterface, TCPInterface, MockInterface, NopInterface)
|
||||
|
||||
/// This is public only so that SimRadio can bootstrap our message flow
|
||||
fun broadcastReceivedFromRadio(context: Context, payload: ByteArray) {
|
||||
val intent = Intent(RECEIVE_FROMRADIO_ACTION)
|
||||
@@ -77,27 +78,13 @@ class RadioInterfaceService : Service(), Logging {
|
||||
val prefs = getPrefs(context)
|
||||
var address = prefs.getString(DEVADDR_KEY, null)
|
||||
|
||||
if (address == null) { /// Check for the old preferences name we used to use
|
||||
var rest = prefs.getString(DEVADDR_KEY_OLD, null)
|
||||
if(rest == "null")
|
||||
rest = null
|
||||
|
||||
if (rest != null)
|
||||
address = BluetoothInterface.toInterfaceName(rest) // Add the bluetooth prefix
|
||||
}
|
||||
|
||||
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the user
|
||||
if(address == null && isMockInterfaceAvailable(context))
|
||||
address = MockInterface.interfaceName
|
||||
if (address == null && MockInterface.addressValid(context, ""))
|
||||
address = MockInterface.prefix.toString()
|
||||
|
||||
return address
|
||||
}
|
||||
|
||||
/** return true if we should show the mock interface on this device
|
||||
* (ie are we in an emulator or in testlab
|
||||
*/
|
||||
fun isMockInterfaceAvailable(context: Context) = isEmulator || ((context.applicationContext as GeeksvilleApplication).isInTestLab)
|
||||
|
||||
/** Like getDeviceAddress, but filtered to return only devices we are currently bonded with
|
||||
*
|
||||
* at
|
||||
@@ -114,13 +101,7 @@ class RadioInterfaceService : Service(), Logging {
|
||||
if (address != null) {
|
||||
val c = address[0]
|
||||
val rest = address.substring(1)
|
||||
val isValid = when (c) {
|
||||
'x' -> BluetoothInterface.addressValid(context, rest)
|
||||
's' -> SerialInterface.addressValid(context, rest)
|
||||
'n' -> true
|
||||
'm' -> true
|
||||
else -> TODO("Unexpected interface type $c")
|
||||
}
|
||||
val isValid = InterfaceFactory.getFactory(c)?.addressValid(context, rest) ?: false
|
||||
if (!isValid)
|
||||
return null
|
||||
}
|
||||
@@ -141,8 +122,7 @@ class RadioInterfaceService : Service(), Logging {
|
||||
*/
|
||||
var serviceScope = CoroutineScope(Dispatchers.IO + Job())
|
||||
|
||||
private val nopIf = NopInterface()
|
||||
private var radioIf: IRadioInterface = nopIf
|
||||
private var radioIf: IRadioInterface = NopInterface()
|
||||
|
||||
/** true if we have started our interface
|
||||
*
|
||||
@@ -221,13 +201,13 @@ class RadioInterfaceService : Service(), Logging {
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return binder;
|
||||
return binder
|
||||
}
|
||||
|
||||
|
||||
/** Start our configured interface (if it isn't already running) */
|
||||
private fun startInterface() {
|
||||
if (radioIf != nopIf)
|
||||
if (radioIf !is NopInterface)
|
||||
warn("Can't start interface - $radioIf is already running")
|
||||
else {
|
||||
val address = getBondedDeviceAddress(this)
|
||||
@@ -244,26 +224,17 @@ class RadioInterfaceService : Service(), Logging {
|
||||
|
||||
val c = address[0]
|
||||
val rest = address.substring(1)
|
||||
radioIf = when (c) {
|
||||
'x' -> BluetoothInterface(this, rest)
|
||||
's' -> SerialInterface(this, rest)
|
||||
'm' -> MockInterface(this)
|
||||
'n' -> nopIf
|
||||
else -> {
|
||||
errormsg("Unexpected radio interface type")
|
||||
nopIf
|
||||
}
|
||||
}
|
||||
radioIf =
|
||||
InterfaceFactory.getFactory(c)?.createInterface(this, rest) ?: NopInterface()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun stopInterface() {
|
||||
val r = radioIf
|
||||
info("stopping interface $r")
|
||||
isStarted = false
|
||||
radioIf = nopIf
|
||||
radioIf = NopInterface()
|
||||
r.close()
|
||||
|
||||
// cancel any old jobs and get ready for the new ones
|
||||
@@ -276,7 +247,7 @@ class RadioInterfaceService : Service(), Logging {
|
||||
receivedPacketsLog.close()
|
||||
|
||||
// Don't broadcast disconnects if we were just using the nop device
|
||||
if (r != nopIf)
|
||||
if (r !is NopInterface)
|
||||
onDisconnect(isPermanent = true) // Tell any clients we are now offline
|
||||
}
|
||||
|
||||
@@ -307,8 +278,6 @@ class RadioInterfaceService : Service(), Logging {
|
||||
debug("Setting bonded device to ${address.anonymize}")
|
||||
|
||||
getPrefs(this).edit(commit = true) {
|
||||
this.remove(DEVADDR_KEY_OLD) // remove any old version of the key
|
||||
|
||||
if (address == null)
|
||||
this.remove(DEVADDR_KEY)
|
||||
else
|
||||
|
||||
@@ -229,7 +229,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
||||
if (reliable != null)
|
||||
if (!characteristic.value.contentEquals(reliable)) {
|
||||
errormsg("A reliable write failed!")
|
||||
gatt.abortReliableWrite();
|
||||
gatt.abortReliableWrite()
|
||||
completeWork(
|
||||
STATUS_RELIABLE_WRITE_FAILED,
|
||||
characteristic
|
||||
@@ -325,7 +325,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
||||
if (newWork.timeoutMillis != 0L) {
|
||||
|
||||
activeTimeout = serviceScope.launch {
|
||||
debug("Starting failsafe timer ${newWork.timeoutMillis}")
|
||||
// debug("Starting failsafe timer ${newWork.timeoutMillis}")
|
||||
delay(newWork.timeoutMillis)
|
||||
errormsg("Failsafe BLE timer expired!")
|
||||
completeWork(
|
||||
@@ -415,7 +415,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
||||
if (work == null)
|
||||
warn("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
|
||||
else {
|
||||
debug("work ${work.tag} is completed, resuming status=$status, res=$res")
|
||||
// debug("work ${work.tag} is completed, resuming status=$status, res=$res")
|
||||
if (status != 0)
|
||||
work.completion.resumeWithException(
|
||||
BLEStatusException(
|
||||
@@ -773,7 +773,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
||||
|
||||
closeGatt()
|
||||
|
||||
failAllWork(BLEException("Connection closing"))
|
||||
failAllWork(BLEConnectionClosing())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,19 +16,27 @@ import com.hoho.android.usbserial.driver.UsbSerialProber
|
||||
import com.hoho.android.usbserial.util.SerialInputOutputManager
|
||||
|
||||
|
||||
class SerialInterface(private val service: RadioInterfaceService, val address: String) : Logging,
|
||||
IRadioInterface, SerialInputOutputManager.Listener {
|
||||
companion object : Logging {
|
||||
private const val START1 = 0x94.toByte()
|
||||
private const val START2 = 0xc3.toByte()
|
||||
private const val MAX_TO_FROM_RADIO_SIZE = 512
|
||||
/**
|
||||
* An interface that assumes we are talking to a meshtastic device via USB serial
|
||||
*/
|
||||
class SerialInterface(service: RadioInterfaceService, private val address: String) :
|
||||
StreamInterface(service), Logging, SerialInputOutputManager.Listener {
|
||||
companion object : Logging, InterfaceFactory('s') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = SerialInterface(service, rest)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
|
||||
/**
|
||||
* according to https://stackoverflow.com/questions/12388914/usb-device-access-pop-up-suppression/15151075#15151075
|
||||
* we should never ask for USB permissions ourselves, instead we should rely on the external dialog printed by the system. If
|
||||
* we do that the system will remember we have accesss
|
||||
*/
|
||||
val assumePermission = true
|
||||
const val assumePermission = true
|
||||
|
||||
fun toInterfaceName(deviceName: String) = "s$deviceName"
|
||||
|
||||
@@ -41,14 +49,14 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
||||
return drivers
|
||||
}
|
||||
|
||||
fun addressValid(context: Context, rest: String): Boolean {
|
||||
override fun addressValid(context: Context, rest: String): Boolean {
|
||||
findSerial(context, rest)?.let { d ->
|
||||
return assumePermission || context.usbManager.hasPermission(d.device)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findSerial(context: Context, rest: String): UsbSerialDriver? {
|
||||
private fun findSerial(context: Context, rest: String): UsbSerialDriver? {
|
||||
val drivers = findDrivers(context)
|
||||
|
||||
return if (drivers.isEmpty())
|
||||
@@ -61,7 +69,7 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
||||
private var uart: UsbSerialDriver? = null
|
||||
private var ioManager: SerialInputOutputManager? = null
|
||||
|
||||
var usbReceiver = object : BroadcastReceiver() {
|
||||
private var usbReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
|
||||
if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) {
|
||||
@@ -85,16 +93,6 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
||||
}
|
||||
}
|
||||
|
||||
private val debugLineBuf = kotlin.text.StringBuilder()
|
||||
|
||||
/** The index of the next byte we are hoping to receive */
|
||||
private var ptr = 0
|
||||
|
||||
/** The two halves of our length */
|
||||
private var msb = 0
|
||||
private var lsb = 0
|
||||
private var packetLen = 0
|
||||
|
||||
init {
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||
@@ -105,16 +103,15 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
debug("Closing serial port for good")
|
||||
service.unregisterReceiver(usbReceiver)
|
||||
onDeviceDisconnect(true)
|
||||
super.close()
|
||||
}
|
||||
|
||||
/** Tell MeshService our device has gone away, but wait for it to come back
|
||||
*
|
||||
* @param waitForStopped if true we should wait for the manager to finish - must be false if called from inside the manager callbacks
|
||||
* */
|
||||
fun onDeviceDisconnect(waitForStopped: Boolean) {
|
||||
override fun onDeviceDisconnect(waitForStopped: Boolean) {
|
||||
ignoreException {
|
||||
ioManager?.let {
|
||||
debug("USB device disconnected, but it might come back")
|
||||
@@ -143,10 +140,10 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
||||
}
|
||||
}
|
||||
|
||||
service.onDisconnect(isPermanent = true) // if USB device disconnects it is definitely permantently gone, not sleeping)
|
||||
super.onDeviceDisconnect(waitForStopped)
|
||||
}
|
||||
|
||||
private fun connect() {
|
||||
override fun connect() {
|
||||
val manager = service.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
val device = findSerial(service, address)
|
||||
|
||||
@@ -175,101 +172,20 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
||||
thread.name = "serial reader"
|
||||
thread.start() // No need to keep reference to thread around, we quit by asking the ioManager to quit
|
||||
|
||||
// Before telling mesh service, send a few START1s to wake a sleeping device
|
||||
val wakeBytes = byteArrayOf(START1, START1, START1, START1)
|
||||
io.writeAsync(wakeBytes)
|
||||
|
||||
// Now tell clients they can (finally use the api)
|
||||
service.onConnect()
|
||||
super.connect()
|
||||
}
|
||||
} else {
|
||||
errormsg("Can't find device")
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSendToRadio(p: ByteArray) {
|
||||
// This method is called from a continuation and it might show up late, so check for uart being null
|
||||
|
||||
val header = ByteArray(4)
|
||||
header[0] = START1
|
||||
header[1] = START2
|
||||
header[2] = (p.size shr 8).toByte()
|
||||
header[3] = (p.size and 0xff).toByte()
|
||||
override fun sendBytes(p: ByteArray) {
|
||||
ioManager?.apply {
|
||||
writeAsync(header)
|
||||
writeAsync(p)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Print device serial debug output somewhere */
|
||||
private fun debugOut(b: Byte) {
|
||||
when (val c = b.toChar()) {
|
||||
'\r' -> {
|
||||
} // ignore
|
||||
'\n' -> {
|
||||
debug("DeviceLog: $debugLineBuf")
|
||||
debugLineBuf.clear()
|
||||
}
|
||||
else ->
|
||||
debugLineBuf.append(c)
|
||||
}
|
||||
}
|
||||
|
||||
private val rxPacket = ByteArray(MAX_TO_FROM_RADIO_SIZE)
|
||||
|
||||
private fun readChar(c: Byte) {
|
||||
// Assume we will be advancing our pointer
|
||||
var nextPtr = ptr + 1
|
||||
|
||||
fun lostSync() {
|
||||
errormsg("Lost protocol sync")
|
||||
nextPtr = 0
|
||||
}
|
||||
|
||||
/// Deliver our current packet and restart our reader
|
||||
fun deliverPacket() {
|
||||
val buf = rxPacket.copyOf(packetLen)
|
||||
service.handleFromRadio(buf)
|
||||
|
||||
nextPtr = 0 // Start parsing the next packet
|
||||
}
|
||||
|
||||
when (ptr) {
|
||||
0 -> // looking for START1
|
||||
if (c != START1) {
|
||||
debugOut(c)
|
||||
nextPtr = 0 // Restart from scratch
|
||||
}
|
||||
1 -> // Looking for START2
|
||||
if (c != START2)
|
||||
lostSync() // Restart from scratch
|
||||
2 -> // Looking for MSB of our 16 bit length
|
||||
msb = c.toInt() and 0xff
|
||||
3 -> { // Looking for LSB of our 16 bit length
|
||||
lsb = c.toInt() and 0xff
|
||||
|
||||
// We've read our header, do one big read for the packet itself
|
||||
packetLen = (msb shl 8) or lsb
|
||||
if (packetLen > MAX_TO_FROM_RADIO_SIZE)
|
||||
lostSync() // If packet len is too long, the bytes must have been corrupted, start looking for START1 again
|
||||
else if (packetLen == 0)
|
||||
deliverPacket() // zero length packets are valid and should be delivered immediately (because there won't be a next byte of payload)
|
||||
}
|
||||
else -> {
|
||||
// We are looking at the packet bytes now
|
||||
rxPacket[ptr - 4] = c
|
||||
|
||||
// Note: we have to check if ptr +1 is equal to packet length (for example, for a 1 byte packetlen, this code will be run with ptr of4
|
||||
if (ptr - 4 + 1 == packetLen) {
|
||||
deliverPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
ptr = nextPtr
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when [SerialInputOutputManager.run] aborts due to an error.
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.zip.CRC32
|
||||
/**
|
||||
* Some misformatted ESP32s have problems
|
||||
*/
|
||||
class DeviceRejectedException() : BLEException("Device rejected filesize")
|
||||
class DeviceRejectedException : BLEException("Device rejected filesize")
|
||||
|
||||
/**
|
||||
* Move this somewhere as a generic network byte order function
|
||||
@@ -211,18 +211,18 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||
* @param isAppload if false, we don't report failure indications (because we consider spiffs non critical for now). But do report to analytics
|
||||
*/
|
||||
fun sendProgress(context: Context, p: Int, isAppload: Boolean) {
|
||||
if(!isAppload && p < 0)
|
||||
reportError("Error while writing spiffs $progress") // See if this is happening in the wild
|
||||
if (!isAppload && p < 0)
|
||||
errormsg("Error while writing spiffs $p") // treat errors writing spiffs as non fatal for now (user partition probably missized and most people don't need it)
|
||||
else
|
||||
if (progress != p) {
|
||||
progress = p
|
||||
|
||||
if(progress != p && (p >= 0 || isAppload)) {
|
||||
progress = p
|
||||
|
||||
val intent = Intent(ACTION_UPDATE_PROGRESS).putExtra(
|
||||
EXTRA_PROGRESS,
|
||||
p
|
||||
)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
val intent = Intent(ACTION_UPDATE_PROGRESS).putExtra(
|
||||
EXTRA_PROGRESS,
|
||||
p
|
||||
)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/** Return true if we thing the firmwarte shoulde be updated
|
||||
@@ -293,14 +293,12 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||
// we must attempt spiffs first, because if we update the appload the device will reboot afterwards
|
||||
try {
|
||||
assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS) }
|
||||
}
|
||||
catch(_: BLECharacteristicNotFoundException) {
|
||||
} catch (_: BLECharacteristicNotFoundException) {
|
||||
// If we can't update spiffs (because not supported by target), do not fail
|
||||
errormsg("Ignoring failure to update spiffs on old appload")
|
||||
}
|
||||
catch(_: DeviceRejectedException) {
|
||||
// the spi filesystem of this device is malformatted
|
||||
reportError("Device rejected invalid spiffs partition")
|
||||
} catch (_: DeviceRejectedException) {
|
||||
// the spi filesystem of this device is malformatted, fail silently because most users don't need the web server
|
||||
errormsg("Device rejected invalid spiffs partition")
|
||||
}
|
||||
|
||||
assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) }
|
||||
@@ -315,9 +313,14 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||
* A public function so that if you have your own SafeBluetooth connection already open
|
||||
* you can use it for the software update.
|
||||
*/
|
||||
private fun doUpdate(context: Context, sync: SafeBluetooth, assetName: String, flashRegion: Int = FLASH_REGION_APPLOAD) {
|
||||
private fun doUpdate(
|
||||
context: Context,
|
||||
sync: SafeBluetooth,
|
||||
assetName: String,
|
||||
flashRegion: Int = FLASH_REGION_APPLOAD
|
||||
) {
|
||||
val isAppload = flashRegion == FLASH_REGION_APPLOAD
|
||||
|
||||
|
||||
try {
|
||||
val g = sync.gatt!!
|
||||
val service = g.services.find { it.uuid == SW_UPDATE_UUID }
|
||||
@@ -332,7 +335,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||
|
||||
info("Starting firmware update for $assetName, flash region $flashRegion")
|
||||
|
||||
sendProgress(context,0, isAppload)
|
||||
sendProgress(context, 0, isAppload)
|
||||
val totalSizeDesc = getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)
|
||||
val dataDesc = getCharacteristic(SW_UPDATE_DATA_CHARACTER)
|
||||
val crc32Desc = getCharacteristic(SW_UPDATE_CRC32_CHARACTER)
|
||||
@@ -347,10 +350,9 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||
updateRegionDesc,
|
||||
toNetworkByteArray(flashRegion, BluetoothGattCharacteristic.FORMAT_UINT8)
|
||||
)
|
||||
}
|
||||
catch(ex: BLECharacteristicNotFoundException) {
|
||||
} catch (ex: BLECharacteristicNotFoundException) {
|
||||
errormsg("Can't set flash programming region (old appload?")
|
||||
if(flashRegion != FLASH_REGION_APPLOAD) {
|
||||
if (flashRegion != FLASH_REGION_APPLOAD) {
|
||||
throw ex
|
||||
}
|
||||
warn("Ignoring setting appload flashRegion")
|
||||
@@ -374,13 +376,21 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||
throw DeviceRejectedException()
|
||||
|
||||
// Send all the blocks
|
||||
var oldProgress = -1 // used to limit # of log spam
|
||||
while (firmwareNumSent < firmwareSize) {
|
||||
// If we are doing the spiffs partition, we limit progress to a max of 50%, so that the user doesn't think we are done
|
||||
// yet
|
||||
val maxProgress = if(flashRegion != FLASH_REGION_APPLOAD)
|
||||
val maxProgress = if (flashRegion != FLASH_REGION_APPLOAD)
|
||||
50 else 100
|
||||
sendProgress(context, firmwareNumSent * maxProgress / firmwareSize, isAppload)
|
||||
debug("sending block ${progress}%")
|
||||
sendProgress(
|
||||
context,
|
||||
firmwareNumSent * maxProgress / firmwareSize,
|
||||
isAppload
|
||||
)
|
||||
if (progress != oldProgress) {
|
||||
debug("sending block ${progress}%")
|
||||
oldProgress = progress
|
||||
}
|
||||
var blockSize = 512 - 3 // Max size MTU excluding framing
|
||||
|
||||
if (blockSize > firmwareStream.available())
|
||||
|
||||
136
app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt
Normal file
136
app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt
Normal file
@@ -0,0 +1,136 @@
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.android.Logging
|
||||
|
||||
|
||||
/**
|
||||
* An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP probably)
|
||||
*/
|
||||
abstract class StreamInterface(protected val service: RadioInterfaceService) :
|
||||
Logging,
|
||||
IRadioInterface {
|
||||
companion object : Logging {
|
||||
private const val START1 = 0x94.toByte()
|
||||
private const val START2 = 0xc3.toByte()
|
||||
private const val MAX_TO_FROM_RADIO_SIZE = 512
|
||||
}
|
||||
|
||||
private val debugLineBuf = kotlin.text.StringBuilder()
|
||||
|
||||
/** The index of the next byte we are hoping to receive */
|
||||
private var ptr = 0
|
||||
|
||||
/** The two halves of our length */
|
||||
private var msb = 0
|
||||
private var lsb = 0
|
||||
private var packetLen = 0
|
||||
|
||||
override fun close() {
|
||||
debug("Closing stream for good")
|
||||
onDeviceDisconnect(true)
|
||||
}
|
||||
|
||||
/** Tell MeshService our device has gone away, but wait for it to come back
|
||||
*
|
||||
* @param waitForStopped if true we should wait for the manager to finish - must be false if called from inside the manager callbacks
|
||||
* */
|
||||
protected open fun onDeviceDisconnect(waitForStopped: Boolean) {
|
||||
service.onDisconnect(isPermanent = true) // if USB device disconnects it is definitely permantently gone, not sleeping)
|
||||
}
|
||||
|
||||
protected open fun connect() {
|
||||
// Before telling mesh service, send a few START1s to wake a sleeping device
|
||||
val wakeBytes = byteArrayOf(START1, START1, START1, START1)
|
||||
sendBytes(wakeBytes)
|
||||
|
||||
// Now tell clients they can (finally use the api)
|
||||
service.onConnect()
|
||||
}
|
||||
|
||||
abstract fun sendBytes(p: ByteArray)
|
||||
|
||||
/// If subclasses need to flash at the end of a packet they can implement
|
||||
open fun flushBytes() {}
|
||||
|
||||
override fun handleSendToRadio(p: ByteArray) {
|
||||
// This method is called from a continuation and it might show up late, so check for uart being null
|
||||
|
||||
val header = ByteArray(4)
|
||||
header[0] = START1
|
||||
header[1] = START2
|
||||
header[2] = (p.size shr 8).toByte()
|
||||
header[3] = (p.size and 0xff).toByte()
|
||||
|
||||
sendBytes(header)
|
||||
sendBytes(p)
|
||||
flushBytes()
|
||||
}
|
||||
|
||||
|
||||
/** Print device serial debug output somewhere */
|
||||
private fun debugOut(b: Byte) {
|
||||
when (val c = b.toChar()) {
|
||||
'\r' -> {
|
||||
} // ignore
|
||||
'\n' -> {
|
||||
debug("DeviceLog: $debugLineBuf")
|
||||
debugLineBuf.clear()
|
||||
}
|
||||
else ->
|
||||
debugLineBuf.append(c)
|
||||
}
|
||||
}
|
||||
|
||||
private val rxPacket = ByteArray(MAX_TO_FROM_RADIO_SIZE)
|
||||
|
||||
protected fun readChar(c: Byte) {
|
||||
// Assume we will be advancing our pointer
|
||||
var nextPtr = ptr + 1
|
||||
|
||||
fun lostSync() {
|
||||
errormsg("Lost protocol sync")
|
||||
nextPtr = 0
|
||||
}
|
||||
|
||||
/// Deliver our current packet and restart our reader
|
||||
fun deliverPacket() {
|
||||
val buf = rxPacket.copyOf(packetLen)
|
||||
service.handleFromRadio(buf)
|
||||
|
||||
nextPtr = 0 // Start parsing the next packet
|
||||
}
|
||||
|
||||
when (ptr) {
|
||||
0 -> // looking for START1
|
||||
if (c != START1) {
|
||||
debugOut(c)
|
||||
nextPtr = 0 // Restart from scratch
|
||||
}
|
||||
1 -> // Looking for START2
|
||||
if (c != START2)
|
||||
lostSync() // Restart from scratch
|
||||
2 -> // Looking for MSB of our 16 bit length
|
||||
msb = c.toInt() and 0xff
|
||||
3 -> { // Looking for LSB of our 16 bit length
|
||||
lsb = c.toInt() and 0xff
|
||||
|
||||
// We've read our header, do one big read for the packet itself
|
||||
packetLen = (msb shl 8) or lsb
|
||||
if (packetLen > MAX_TO_FROM_RADIO_SIZE)
|
||||
lostSync() // If packet len is too long, the bytes must have been corrupted, start looking for START1 again
|
||||
else if (packetLen == 0)
|
||||
deliverPacket() // zero length packets are valid and should be delivered immediately (because there won't be a next byte of payload)
|
||||
}
|
||||
else -> {
|
||||
// We are looking at the packet bytes now
|
||||
rxPacket[ptr - 4] = c
|
||||
|
||||
// Note: we have to check if ptr +1 is equal to packet length (for example, for a 1 byte packetlen, this code will be run with ptr of4
|
||||
if (ptr - 4 + 1 == packetLen) {
|
||||
deliverPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
ptr = nextPtr
|
||||
}
|
||||
}
|
||||
102
app/src/main/java/com/geeksville/mesh/service/TCPInterface.kt
Normal file
102
app/src/main/java/com/geeksville/mesh/service/TCPInterface.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.util.Exceptions
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.InetAddress
|
||||
import java.net.Socket
|
||||
import java.net.SocketTimeoutException
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
class TCPInterface(service: RadioInterfaceService, private val address: String) :
|
||||
StreamInterface(service) {
|
||||
|
||||
companion object : Logging, InterfaceFactory('t') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = TCPInterface(service, rest)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
}
|
||||
|
||||
var socket: Socket? = null
|
||||
lateinit var outStream: OutputStream
|
||||
lateinit var inStream: InputStream
|
||||
|
||||
init {
|
||||
connect()
|
||||
}
|
||||
|
||||
override fun sendBytes(p: ByteArray) {
|
||||
outStream.write(p)
|
||||
}
|
||||
|
||||
override fun flushBytes() {
|
||||
outStream.flush()
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnect(waitForStopped: Boolean) {
|
||||
val s = socket
|
||||
if (s != null) {
|
||||
debug("Closing TCP socket")
|
||||
socket = null
|
||||
outStream.close()
|
||||
inStream.close()
|
||||
s.close()
|
||||
}
|
||||
super.onDeviceDisconnect(waitForStopped)
|
||||
}
|
||||
|
||||
override fun connect() {
|
||||
//here you must put your computer's IP address.
|
||||
//here you must put your computer's IP address.
|
||||
|
||||
// No need to keep a reference to this thread - it will exit when we close inStream
|
||||
thread(start = true, isDaemon = true, name = "TCP reader") {
|
||||
try {
|
||||
val a = InetAddress.getByName(address)
|
||||
debug("TCP connecting to $address")
|
||||
|
||||
//create a socket to make the connection with the server
|
||||
val port = 4403
|
||||
val s = Socket(a, port)
|
||||
s.tcpNoDelay = true
|
||||
s.soTimeout = 500
|
||||
socket = s
|
||||
outStream = BufferedOutputStream(s.getOutputStream())
|
||||
inStream = s.getInputStream()
|
||||
|
||||
// Note: we call the super method FROM OUR NEW THREAD
|
||||
super.connect()
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
val c = inStream.read()
|
||||
if (c == -1) {
|
||||
warn("Got EOF on TCP stream")
|
||||
onDeviceDisconnect(false)
|
||||
break
|
||||
} else
|
||||
readChar(c.toByte())
|
||||
} catch (ex: SocketTimeoutException) {
|
||||
// Ignore and start another read
|
||||
}
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
errormsg("IOException in TCP reader: $ex") // FIXME, show message to user
|
||||
onDeviceDisconnect(false)
|
||||
} catch (ex: Throwable) {
|
||||
Exceptions.report(ex, "Exception in TCP reader")
|
||||
onDeviceDisconnect(false)
|
||||
}
|
||||
debug("Exiting TCP reader")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
@@ -18,7 +19,6 @@ import com.geeksville.android.Logging
|
||||
import com.geeksville.android.hideKeyboard
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.databinding.ChannelFragmentBinding
|
||||
import com.geeksville.mesh.model.Channel
|
||||
@@ -50,6 +50,7 @@ fun ImageView.setOpaque() {
|
||||
class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||
|
||||
private var _binding: ChannelFragmentBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
@@ -81,6 +82,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||
val channels = model.channels.value
|
||||
val channel = channels?.primaryChannel
|
||||
|
||||
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
||||
|
||||
// Only let buttons work if we are connected to the radio
|
||||
binding.shareButton.isEnabled = connected
|
||||
binding.resetButton.isEnabled = connected && Channel.default != channel
|
||||
|
||||
binding.editableCheckbox.isChecked = false // start locked
|
||||
if (channel != null) {
|
||||
binding.qrView.visibility = View.VISIBLE
|
||||
@@ -89,7 +96,6 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||
|
||||
// For now, we only let the user edit/save channels while the radio is awake - because the service
|
||||
// doesn't cache radioconfig writes.
|
||||
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
||||
binding.editableCheckbox.isEnabled = connected
|
||||
|
||||
binding.qrView.setImageBitmap(channels.getChannelQR())
|
||||
@@ -138,8 +144,38 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||
type = "text/plain"
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
requireActivity().startActivity(shareIntent)
|
||||
try {
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
requireActivity().startActivity(shareIntent)
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Snackbar.make(
|
||||
binding.shareButton,
|
||||
R.string.no_app_found,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send new channel settings to the device
|
||||
private fun installSettings(newChannel: ChannelProtos.ChannelSettings) {
|
||||
val newSet =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
|
||||
// Try to change the radio, if it fails, tell the user why and throw away their redits
|
||||
try {
|
||||
model.setChannels(newSet)
|
||||
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("ignoring channel problem", ex)
|
||||
|
||||
setGUIfromModel() // Throw away user edits
|
||||
|
||||
// Tell the user to try again
|
||||
Snackbar.make(
|
||||
binding.editableCheckbox,
|
||||
R.string.radio_sleeping,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,13 +186,34 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||
requireActivity().hideKeyboard()
|
||||
}
|
||||
|
||||
binding.resetButton.setOnClickListener { _ ->
|
||||
// User just locked it, we should warn and then apply changes to radio
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.reset_to_defaults)
|
||||
.setMessage(R.string.are_you_shure_change_default)
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
setGUIfromModel() // throw away any edits
|
||||
}
|
||||
.setPositiveButton(R.string.apply) { _, _ ->
|
||||
debug("Switching back to default channel")
|
||||
installSettings(Channel.default.settings)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
|
||||
binding.editableCheckbox.setOnClickListener { _ ->
|
||||
|
||||
/// We use this to determine if the user tried to install a custom name
|
||||
var originalName = ""
|
||||
|
||||
val checked = binding.editableCheckbox.isChecked
|
||||
if (checked) {
|
||||
// User just unlocked for editing - remove the # goo around the channel name
|
||||
model.channels.value?.primaryChannel?.let { ch ->
|
||||
binding.channelNameEdit.setText(ch.name)
|
||||
// Note: We are careful to show the emptystring here if the user was on a default channel, so the user knows they should it for any changes
|
||||
originalName = ch.settings.name
|
||||
binding.channelNameEdit.setText(originalName)
|
||||
}
|
||||
} else {
|
||||
// User just locked it, we should warn and then apply changes to radio
|
||||
@@ -170,49 +227,33 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||
// Generate a new channel with only the changes the user can change in the GUI
|
||||
model.channels.value?.primaryChannel?.let { oldPrimary ->
|
||||
var newSettings = oldPrimary.settings.toBuilder()
|
||||
newSettings.name = binding.channelNameEdit.text.toString().trim()
|
||||
val newName = binding.channelNameEdit.text.toString().trim()
|
||||
|
||||
// Generate a new AES256 key unleess the user is trying to go back to stock
|
||||
if (!newSettings.name.equals(
|
||||
Channel.defaultChannel.name,
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
// Find the new modem config
|
||||
val selectedChannelOptionString =
|
||||
binding.filledExposedDropdown.editableText.toString()
|
||||
var modemConfig = getModemConfig(selectedChannelOptionString)
|
||||
if (modemConfig == ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED) // Huh? didn't find it - keep same
|
||||
modemConfig = oldPrimary.settings.modemConfig
|
||||
|
||||
// Generate a new AES256 key if the user changes channel name or the name is non-default and the settings changed
|
||||
if (newName != originalName || (newName.isNotEmpty() && modemConfig != oldPrimary.settings.modemConfig)) {
|
||||
// Install a new customized channel
|
||||
debug("ASSIGNING NEW AES256 KEY")
|
||||
val random = SecureRandom()
|
||||
val bytes = ByteArray(32)
|
||||
random.nextBytes(bytes)
|
||||
newSettings.name = newName
|
||||
newSettings.psk = ByteString.copyFrom(bytes)
|
||||
} else {
|
||||
debug("Switching back to default channel")
|
||||
newSettings = Channel.defaultChannel.settings.toBuilder()
|
||||
newSettings = Channel.default.settings.toBuilder()
|
||||
}
|
||||
|
||||
val selectedChannelOptionString =
|
||||
binding.filledExposedDropdown.editableText.toString()
|
||||
val modemConfig = getModemConfig(selectedChannelOptionString)
|
||||
// No matter what apply the speed selection from the user
|
||||
newSettings.modemConfig = modemConfig
|
||||
|
||||
if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
|
||||
newSettings.modemConfig = modemConfig
|
||||
|
||||
val newChannel = newSettings.build()
|
||||
val newSet = ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
|
||||
// Try to change the radio, if it fails, tell the user why and throw away their redits
|
||||
try {
|
||||
model.setChannels(newSet)
|
||||
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("ignoring channel problem", ex)
|
||||
|
||||
setGUIfromModel() // Throw away user edits
|
||||
|
||||
// Tell the user to try again
|
||||
Snackbar.make(
|
||||
binding.editableCheckbox,
|
||||
R.string.radio_sleeping,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
installSettings(newSettings.build())
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.geeksville.mesh.model.UIViewModel
|
||||
class DebugFragment : Fragment() {
|
||||
|
||||
private var _binding: DebugFragmentBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
@@ -44,11 +45,11 @@ class DebugFragment : Fragment() {
|
||||
model.deleteAllPacket()
|
||||
}
|
||||
|
||||
binding.closeButton.setOnClickListener{
|
||||
parentFragmentManager.popBackStack();
|
||||
binding.closeButton.setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
model.allPackets.observe(viewLifecycleOwner, Observer {
|
||||
packets -> packets?.let { adapter.setPackets(it) }
|
||||
model.allPackets.observe(viewLifecycleOwner, Observer { packets ->
|
||||
packets?.let { adapter.setPackets(it) }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -131,8 +131,10 @@ fun positionToMeter(a: MeshProtos.Position, b: MeshProtos.Position): Double {
|
||||
a.latitudeI * 1e-7,
|
||||
a.longitudeI * 1e-7,
|
||||
b.latitudeI * 1e-7,
|
||||
b.longitudeI * 1e-7)
|
||||
b.longitudeI * 1e-7
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees/mins/secs to a single double
|
||||
*
|
||||
@@ -186,7 +188,7 @@ fun bearing(
|
||||
val y = sin(deltaLonRad) * cos(lat2Rad)
|
||||
val x =
|
||||
cos(lat1Rad) * sin(lat2Rad) - (sin(lat1Rad) * cos(lat2Rad)
|
||||
* Math.cos(deltaLonRad))
|
||||
* Math.cos(deltaLonRad))
|
||||
return radToBearing(Math.atan2(y, x))
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
||||
import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
||||
@@ -25,7 +24,6 @@ import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.google.android.material.chip.Chip
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.util.*
|
||||
|
||||
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
|
||||
@@ -54,9 +52,9 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
private val timeFormat: DateFormat =
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
|
||||
private fun getShortDateTime(time : Date): String {
|
||||
private fun getShortDateTime(time: Date): String {
|
||||
// return time if within 24 hours, otherwise date/time
|
||||
val one_day = 60*60*24*100L
|
||||
val one_day = 60 * 60 * 24 * 100L
|
||||
if (System.currentTimeMillis() - time.time > one_day) {
|
||||
return dateTimeFormat.format(time)
|
||||
} else return timeFormat.format(time)
|
||||
@@ -152,11 +150,25 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
if (isMe) {
|
||||
marginParams.leftMargin = messageOffset
|
||||
marginParams.rightMargin = 0
|
||||
context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg)) }
|
||||
context?.let {
|
||||
holder.card.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
it,
|
||||
R.color.colorMyMsg
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
marginParams.rightMargin = messageOffset
|
||||
marginParams.leftMargin = 0
|
||||
context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg)) }
|
||||
context?.let {
|
||||
holder.card.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
it,
|
||||
R.color.colorMsg
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Hide the username chip for my messages
|
||||
if (isMe) {
|
||||
@@ -240,20 +252,26 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
||||
fun updateTextEnabled() {
|
||||
binding.textInputLayout.isEnabled =
|
||||
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null && model.radioConfig.value != null
|
||||
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED
|
||||
|
||||
// Just being connected is enough to allow sending texts I think
|
||||
// && model.nodeDB.myId.value != null && model.radioConfig.value != null
|
||||
}
|
||||
|
||||
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled() })
|
||||
updateTextEnabled()
|
||||
})
|
||||
|
||||
model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
|
||||
/* model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled() })
|
||||
updateTextEnabled()
|
||||
})
|
||||
|
||||
model.radioConfig.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled() })
|
||||
updateTextEnabled()
|
||||
}) */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import java.text.DateFormat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import java.util.*
|
||||
|
||||
class PacketListAdapter internal constructor(
|
||||
context: Context
|
||||
) : RecyclerView.Adapter<PacketListAdapter.PacketViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var packets = emptyList<Packet>()
|
||||
|
||||
private val timeFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
|
||||
inner class PacketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val packetTypeView: TextView = itemView.findViewById(R.id.type)
|
||||
val packetDateReceivedView: TextView = itemView.findViewById(R.id.dateReceived)
|
||||
val packetRawMessage : TextView = itemView.findViewById(R.id.rawMessage)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PacketViewHolder {
|
||||
val itemView = inflater.inflate(R.layout.adapter_packet_layout, parent, false)
|
||||
return PacketViewHolder(itemView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PacketViewHolder, position: Int) {
|
||||
val current = packets[position]
|
||||
holder.packetTypeView.text = current.message_type
|
||||
holder.packetRawMessage.text = current.raw_message
|
||||
val date = Date(current.received_date)
|
||||
holder.packetDateReceivedView.text = timeFormat.format(date)
|
||||
}
|
||||
|
||||
internal fun setPackets(packets: List<Packet>) {
|
||||
this.packets = packets
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemCount() = packets.size
|
||||
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class PacketListAdapter internal constructor(
|
||||
context: Context
|
||||
) : RecyclerView.Adapter<PacketListAdapter.PacketViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var packets = emptyList<Packet>()
|
||||
|
||||
private val timeFormat: DateFormat =
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
|
||||
inner class PacketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val packetTypeView: TextView = itemView.findViewById(R.id.type)
|
||||
val packetDateReceivedView: TextView = itemView.findViewById(R.id.dateReceived)
|
||||
val packetRawMessage: TextView = itemView.findViewById(R.id.rawMessage)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PacketViewHolder {
|
||||
val itemView = inflater.inflate(R.layout.adapter_packet_layout, parent, false)
|
||||
return PacketViewHolder(itemView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PacketViewHolder, position: Int) {
|
||||
val current = packets[position]
|
||||
holder.packetTypeView.text = current.message_type
|
||||
holder.packetRawMessage.text = current.raw_message
|
||||
val date = Date(current.received_date)
|
||||
holder.packetDateReceivedView.text = timeFormat.format(date)
|
||||
}
|
||||
|
||||
internal fun setPackets(packets: List<Packet>) {
|
||||
this.packets = packets
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemCount() = packets.size
|
||||
|
||||
}
|
||||
@@ -38,10 +38,7 @@ import com.geeksville.mesh.android.bluetoothManager
|
||||
import com.geeksville.mesh.android.usbManager
|
||||
import com.geeksville.mesh.databinding.SettingsFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.BluetoothInterface
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.service.RadioInterfaceService
|
||||
import com.geeksville.mesh.service.SerialInterface
|
||||
import com.geeksville.mesh.service.*
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressSuccess
|
||||
@@ -59,7 +56,7 @@ import kotlinx.coroutines.Job
|
||||
import java.util.regex.Pattern
|
||||
|
||||
|
||||
object SLogging : Logging {}
|
||||
object SLogging : Logging
|
||||
|
||||
/// Change to a new macaddr selection, updating GUI and radio
|
||||
fun changeDeviceSelection(context: MainActivity, newAddr: String?) {
|
||||
@@ -186,7 +183,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||
// if that device later disconnects remove it as a candidate
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
|
||||
if ((result.device.name?.startsWith("Mesh") ?: false)) {
|
||||
if ((result.device.name?.startsWith("Mesh") == true)) {
|
||||
val addr = result.device.address
|
||||
val fullAddr = "x$addr" // full address with the bluetooh prefix
|
||||
// prevent logspam because weill get get lots of redundant scan results
|
||||
@@ -245,14 +242,12 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||
debug("BTScan component active")
|
||||
selectedAddress = RadioInterfaceService.getDeviceAddress(context)
|
||||
|
||||
return if (bluetoothAdapter == null || RadioInterfaceService.isMockInterfaceAvailable(
|
||||
context
|
||||
)
|
||||
) {
|
||||
return if (bluetoothAdapter == null || MockInterface.addressValid(context, "")) {
|
||||
warn("No bluetooth adapter. Running under emulation?")
|
||||
|
||||
val testnodes = listOf(
|
||||
DeviceListEntry("Simulated interface", "m", true),
|
||||
DeviceListEntry("Included simulator", "m", true),
|
||||
DeviceListEntry("Complete simulator", "t10.0.2.2", true),
|
||||
DeviceListEntry(context.getString(R.string.none), "n", true)
|
||||
/* Don't populate fake bluetooth devices, because we don't want testlab inside of google
|
||||
to try and use them.
|
||||
@@ -494,7 +489,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
}
|
||||
|
||||
/// Set the correct update button configuration based on current progress
|
||||
private fun refreshUpdateButton() {
|
||||
private fun refreshUpdateButton(enable: Boolean) {
|
||||
debug("Reiniting the udpate button")
|
||||
val info = model.myNodeInfo.value
|
||||
val service = model.meshService
|
||||
@@ -505,7 +500,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
|
||||
val progress = service.updateStatus
|
||||
|
||||
binding.updateFirmwareButton.isEnabled =
|
||||
binding.updateFirmwareButton.isEnabled = enable &&
|
||||
(progress < 0) // if currently doing an upgrade disable button
|
||||
|
||||
if (progress >= 0) {
|
||||
@@ -542,7 +537,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
val connected = model.isConnected.value
|
||||
|
||||
val isConnected = connected == MeshService.ConnectionState.CONNECTED
|
||||
binding.nodeSettings.visibility = if(isConnected) View.VISIBLE else View.GONE
|
||||
binding.nodeSettings.visibility = if (isConnected) View.VISIBLE else View.GONE
|
||||
|
||||
if (connected == MeshService.ConnectionState.DISCONNECTED)
|
||||
model.ownerName.value = ""
|
||||
@@ -552,25 +547,19 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
val spinner = binding.regionSpinner
|
||||
val unsetIndex = regions.indexOf(RadioConfigProtos.RegionCode.Unset.name)
|
||||
spinner.onItemSelectedListener = null
|
||||
if(region != null) {
|
||||
debug("current region is $region")
|
||||
var regionIndex = regions.indexOf(region.name)
|
||||
if(regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
|
||||
regionIndex = unsetIndex
|
||||
|
||||
// We don't want to be notified of our own changes, so turn off listener while making them
|
||||
spinner.setSelection(regionIndex, false)
|
||||
spinner.onItemSelectedListener = regionSpinnerListener
|
||||
spinner.isEnabled = true
|
||||
}
|
||||
else {
|
||||
warn("region is unset!")
|
||||
spinner.setSelection(unsetIndex, false)
|
||||
spinner.isEnabled = false // leave disabled, because we can't get our region
|
||||
}
|
||||
debug("current region is $region")
|
||||
var regionIndex = regions.indexOf(region.name)
|
||||
if (regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
|
||||
regionIndex = unsetIndex
|
||||
|
||||
// We don't want to be notified of our own changes, so turn off listener while making them
|
||||
spinner.setSelection(regionIndex, false)
|
||||
spinner.onItemSelectedListener = regionSpinnerListener
|
||||
spinner.isEnabled = true
|
||||
|
||||
// If actively connected possibly let the user update firmware
|
||||
refreshUpdateButton()
|
||||
refreshUpdateButton(region != RadioConfigProtos.RegionCode.Unset)
|
||||
|
||||
// Update the status string (highest priority messages first)
|
||||
val info = model.myNodeInfo.value
|
||||
@@ -590,14 +579,14 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
}
|
||||
}
|
||||
|
||||
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener{
|
||||
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>,
|
||||
view: View,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val item = parent.getItemAtPosition(position) as String
|
||||
val item = parent.getItemAtPosition(position) as String?
|
||||
val asProto = item!!.let { RadioConfigProtos.RegionCode.valueOf(it) }
|
||||
exceptionToSnackbar(requireView()) {
|
||||
model.region = asProto
|
||||
@@ -622,7 +611,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
|
||||
// init our region spinner
|
||||
val spinner = binding.regionSpinner
|
||||
val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
||||
val regionAdapter =
|
||||
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
||||
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = regionAdapter
|
||||
|
||||
@@ -632,7 +622,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
|
||||
|
||||
// Only let user edit their name or set software update while connected to a radio
|
||||
model.isConnected.observe(viewLifecycleOwner, Observer { connectionState ->
|
||||
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
|
||||
updateNodeInfo()
|
||||
})
|
||||
|
||||
@@ -702,7 +692,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
scanModel.onSelected(requireActivity() as MainActivity, device)
|
||||
|
||||
if (!b.isSelected)
|
||||
binding.scanStatusText.setText(getString(R.string.please_pair))
|
||||
binding.scanStatusText.text = getString(R.string.please_pair)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,7 +755,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
// get rid of the warning text once at least one device is paired.
|
||||
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
|
||||
binding.warningNotPaired.visibility =
|
||||
if (hasBonded && !RadioInterfaceService.isMockInterfaceAvailable(requireContext())) View.GONE else View.VISIBLE
|
||||
if (hasBonded && !MockInterface.addressValid(requireContext(), ""))
|
||||
View.GONE
|
||||
else
|
||||
View.VISIBLE
|
||||
}
|
||||
|
||||
/// Setup the GUI to do a classic (pre SDK 26 BLE scan)
|
||||
@@ -935,7 +928,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
|
||||
private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
refreshUpdateButton()
|
||||
refreshUpdateButton(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||
val name = n.user?.longName ?: n.user?.id ?: "Unknown node"
|
||||
holder.nodeNameView.text = name
|
||||
|
||||
val pos = n.validPosition;
|
||||
val pos = n.validPosition
|
||||
if (pos != null) {
|
||||
val coords =
|
||||
String.format("%.5f %.5f", pos.latitude, pos.longitude).replace(",", ".")
|
||||
@@ -141,7 +141,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||
}
|
||||
renderBattery(n.batteryPctLevel, holder)
|
||||
|
||||
holder.lastTime.text = formatAgo(n.lastSeen);
|
||||
holder.lastTime.text = formatAgo(n.lastHeard)
|
||||
|
||||
if ((n.num == ourNodeInfo?.num) || (n.snr > 100f)) {
|
||||
holder.signalView.visibility = View.INVISIBLE
|
||||
|
||||
Reference in New Issue
Block a user