Merge branch 'master' into master

This commit is contained in:
Kevin Hester
2021-04-15 12:04:37 +08:00
committed by GitHub
48 changed files with 1099 additions and 607 deletions

View File

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

View File

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

View File

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

View File

@@ -32,8 +32,7 @@ data class MyNodeInfo(
parcel.readInt(),
parcel.readInt(),
parcel.readInt()
) {
}
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(myNodeNum)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
*/

View File

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

View 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
}
}

View 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")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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