diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index bc2dfb5ae..8756b8ff2 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -5,6 +5,5 @@
-
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 5cac80212..e3cb2ebd5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -30,15 +30,15 @@ android {
keyPassword "$meshtasticKeyPassword"
}
} */
- compileSdkVersion 29
+ compileSdkVersion 30
// leave undefined to use version plugin wants
// buildToolsVersion "30.0.2" // Note: 30.0.2 doesn't yet work on Github actions CI
defaultConfig {
applicationId "com.geeksville.mesh"
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
- targetSdkVersion 29
- versionCode 20211 // format is Mmmss (where M is 1+the numeric major number
- versionName "1.2.11"
+ targetSdkVersion 29 // 30 can't work until an explicit location permissions dialog is added
+ versionCode 20222 // format is Mmmss (where M is 1+the numeric major number
+ versionName "1.2.22"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// per https://developer.android.com/studio/write/vector-asset-studio
@@ -119,14 +119,14 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
- implementation 'androidx.fragment:fragment-ktx:1.3.1'
+ implementation 'androidx.fragment:fragment-ktx:1.3.2'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
@@ -151,7 +151,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// For now I'm not using javalite, because I want JSON printing
- implementation ('com.google.protobuf:protobuf-java:3.15.5')
+ implementation ('com.google.protobuf:protobuf-java:3.15.6')
// For UART access
// implementation 'com.google.android.things:androidthings:1.0'
@@ -170,7 +170,7 @@ dependencies {
implementation 'com.google.android.gms:play-services-auth:19.0.0'
// Add the Firebase SDK for Crashlytics.
- implementation 'com.google.firebase:firebase-crashlytics:17.3.1'
+ implementation 'com.google.firebase:firebase-crashlytics:17.4.1'
// alas implementation bug deep in the bowels when I tried it for my SyncBluetoothDevice class
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
index a9ee757d0..17382d016 100644
--- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
+++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
@@ -113,4 +113,7 @@ interface IMeshService {
Return a number 0-100 for progress. -1 for completed and success, -2 for failure
*/
int getUpdateStatus();
+
+ int getRegion();
+ void setRegion(int regionCode);
}
diff --git a/app/src/main/java/com/geeksville/mesh/DataPacket.kt b/app/src/main/java/com/geeksville/mesh/DataPacket.kt
index ee5df4940..7f5f20b76 100644
--- a/app/src/main/java/com/geeksville/mesh/DataPacket.kt
+++ b/app/src/main/java/com/geeksville/mesh/DataPacket.kt
@@ -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
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index c7879cf1d..f312bb667 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -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()
+ )
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
index f2fa418e6..e0a9bd87b 100644
--- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
+++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
@@ -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)
diff --git a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt
index da76ccfaa..a650dfe06 100644
--- a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt
+++ b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt
@@ -32,8 +32,7 @@ data class MyNodeInfo(
parcel.readInt(),
parcel.readInt(),
parcel.readInt()
- ) {
- }
+ )
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(myNodeNum)
diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
index f4864dd91..ef72d478e 100644
--- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
+++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
@@ -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
diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
index 665a8de25..fe66997cd 100644
--- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
@@ -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) })
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt
index 62409f4ff..cae6558bb 100644
--- a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt
@@ -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
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt
index 3fa0b4697..31a26aec5 100644
--- a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt
@@ -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)
}
diff --git a/app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt b/app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt
index 961524b66..e2096f2bf 100644
--- a/app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt
@@ -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, Logging {
+data class DeviceVersion(val asString: String) : Comparable, 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
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index fe4175e9b..2125b5978 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -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)
diff --git a/app/src/main/java/com/geeksville/mesh/service/BLEException.kt b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt
index 4ee38cd1b..68f7c6c4d 100644
--- a/app/src/main/java/com/geeksville/mesh/service/BLEException.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt
@@ -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")
\ No newline at end of file
+open class BLECharacteristicNotFoundException(uuid: UUID) :
+ BLEException("Can't get characteristic $uuid")
+
+/// Our interface is being shut down
+open class BLEConnectionClosing : BLEException("Connection closing ")
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt
index a424f8640..85e4fc091 100644
--- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt
@@ -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")
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/BluetoothStateReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/BluetoothStateReceiver.kt
index 770341e7c..a4edefb51 100644
--- a/app/src/main/java/com/geeksville/mesh/service/BluetoothStateReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothStateReceiver.kt
@@ -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
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/service/InterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/service/InterfaceFactory.kt
new file mode 100644
index 000000000..ec178df5e
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/service/InterfaceFactory.kt
@@ -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()
+
+ 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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index 68e9e1652..63685c2f6 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -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()
+ 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())
}
+ /// scan the channel list and make sure it has one PRIMARY channel and is maxChannels long
+ private fun fixupChannelList(lIn: List): Array {
+ // 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
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
index 6d782269e..dd10134b5 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
@@ -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
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceLocationCallback.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceLocationCallback.kt
index ac04a0f24..bb7e4bc24 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceLocationCallback.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceLocationCallback.kt
@@ -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
- }
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
index 5e02067a4..d56ad88be 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
@@ -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)
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt
index c1477b718..b7776acd8 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt
@@ -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
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
index d765c21aa..f63b28ece 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
@@ -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) {
diff --git a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt
index 80d4495be..f7bdef9df 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt
@@ -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
diff --git a/app/src/main/java/com/geeksville/mesh/service/NopInterface.kt b/app/src/main/java/com/geeksville/mesh/service/NopInterface.kt
index f4e462349..3b24ad26d 100644
--- a/app/src/main/java/com/geeksville/mesh/service/NopInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/NopInterface.kt
@@ -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) {
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
index 112f7010f..9467187b3 100644
--- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
@@ -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(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
diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt
index ee460c9ca..65ae1ddfa 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt
@@ -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())
}
/**
diff --git a/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt b/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt
index 00b802527..5710f596c 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SerialInterface.kt
@@ -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.
*/
diff --git a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
index b9050105e..8326a5168 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
@@ -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())
diff --git a/app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt b/app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt
new file mode 100644
index 000000000..717805ef6
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/service/StreamInterface.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/service/TCPInterface.kt b/app/src/main/java/com/geeksville/mesh/service/TCPInterface.kt
new file mode 100644
index 000000000..33760eeea
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/service/TCPInterface.kt
@@ -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")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
index 9ca623088..e006ef038 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -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()
diff --git a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt
index 75822cdb2..44f79eda0 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt
@@ -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) }
})
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/LocationUtils.kt b/app/src/main/java/com/geeksville/mesh/ui/LocationUtils.kt
index eed32c702..3a23b75b0 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/LocationUtils.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/LocationUtils.kt
@@ -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))
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
index 43553a20d..d552e4b67 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
@@ -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()
+ }) */
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/PacketListAdapter.kt b/app/src/main/java/com/geeksville/mesh/ui/PacketListAdapter.kt
index 52ec35892..726f56802 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/PacketListAdapter.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/PacketListAdapter.kt
@@ -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() {
-
- private val inflater: LayoutInflater = LayoutInflater.from(context)
- private var packets = emptyList()
-
- 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) {
- 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() {
+
+ private val inflater: LayoutInflater = LayoutInflater.from(context)
+ private var packets = emptyList()
+
+ 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) {
+ this.packets = packets
+ notifyDataSetChanged()
+ }
+
+ override fun getItemCount() = packets.size
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
index d6fd03cfd..0a61cd403 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -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)
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
index 0649aa429..3873fd27b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
@@ -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
diff --git a/app/src/main/proto b/app/src/main/proto
index 820fa497d..f9c4f8758 160000
--- a/app/src/main/proto
+++ b/app/src/main/proto
@@ -1 +1 @@
-Subproject commit 820fa497dfde07e129cad6955bf2f4b2b9cecebc
+Subproject commit f9c4f875818c9aa6995e6e25803d52557a1779c7
diff --git a/app/src/main/res/drawable/ic_twotone_public_24.xml b/app/src/main/res/drawable/ic_twotone_public_24.xml
new file mode 100644
index 000000000..403051e61
--- /dev/null
+++ b/app/src/main/res/drawable/ic_twotone_public_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/layout/channel_fragment.xml b/app/src/main/res/layout/channel_fragment.xml
index 461eafab0..b834136f3 100644
--- a/app/src/main/res/layout/channel_fragment.xml
+++ b/app/src/main/res/layout/channel_fragment.xml
@@ -1,6 +1,7 @@
@@ -23,7 +24,7 @@
android:digits="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890- "
android:hint="@string/channel_name"
android:imeOptions="actionDone"
- android:maxLength="11"
+ android:maxLength="15"
android:singleLine="true"
android:text="@string/unset" />
@@ -91,30 +92,46 @@
+ android:layout_height="wrap_content"
+ android:hint="@string/set_channel_options" />
+
+
+ app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
+ app:layout_constraintStart_toEndOf="@+id/resetButton">
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 6b1d4c37f..c57c57e63 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -1,5 +1,5 @@
-
-
+
+
ConfiguraciónNombre del canalOpciones del canal
@@ -10,9 +10,9 @@
icono de la aplicaciónNombre de usuario desconocidoAvatar de usuario
- hey encontré el caché, está aquí al lado del tigre grande. Estoy un poco asustado.
+ hey encontré el caché está aquí al lado del tigre grande. Estoy un poco asustado.Enviar texto
- Aún no ha emparejado un radio compatible con Meshtastic con este teléfono. Empareje un dispositivo y configure su nombre de usuario. \n\nEsta aplicación de código abierto es una prueba alfa; si encuentra un problema, publique en el foro: meshtastic.discourse.group. \n\nPara obtener más información, visite nuestra página web - www.meshtastic.org.
+ Aún no ha emparejado un radio compatible con Meshtastic con este teléfono. Empareje un dispositivo y configure su nombre de usuario. \n\nEsta aplicación de código abierto es una prueba alfa; si encuentra un problema publique en el foro: meshtastic.discourse.group. \n\nPara obtener más información visite nuestra página web - www.meshtastic.org.Nombre de usuario sin configurarTu nombreEstadísticas de uso anónimo e informes de fallos.
@@ -28,4 +28,69 @@
¿Estás seguro de que quieres cambiar el canal? Toda comunicación con otros nodos se detendrá hasta que comparta la nueva configuración del canal.Nueva URL de canal recibida¿Quieres cambiar cambiar al canal de \'%s\'?
-
+ Falta un permiso necesario Meshtastic no podrá funcionar correctamente. Por favor habilite en la configuración de la aplicación Android.
+ La radio estaba en reposo no podía cambiar de canal
+ informe de fallos
+ Informar de un error
+ ¿Estás seguro de que quieres informar de un error? Después de informar por favor publique en meshtastic.discourse.group para que podamos comparar el informe con lo que encontró.
+ Reportar
+ Seleccione la radio
+ Actualmente estás emparejado con la radio %s
+ Todavía no has emparejado una radio.
+ Cambiar la radio
+ Por favor empareja el dispositivo en los Ajustes de Android.
+ Emparejamiento completado iniciando el servicio
+ El emparejamiento ha fallado por favor seleccione de nuevo
+ El acceso a la localización está desactivado no puede proporcionar la posición a la malla.
+ Compartir
+ Desconectado
+ Dispositivo en reposo
+ Conectado: %s de %s en línea
+ Una lista de nodos en la red
+ Actualizar el firmware
+ Conectado a la radio
+ Conectado a la radio (%s)
+ No está conectado seleccione la radio de abajo
+ Conectado a la radio pero está en reposo
+ Actualizar a %s
+ Es necesario actualizar la aplicación
+ Ninguno (desactivado)
+ Corto alcance (pero rápido)
+ Alcance medio (pero rápido)
+ Largo alcance (pero más lento)
+ Muy largo alcance (pero lento)
+ SIN RECONOCIMIENTO
+ Notificaciones de servicio de Meshtastic
+ Debes activar los servicios de localización (de alta precisión) en los Ajustes de Android
+ Acerca de
+ Una lista de nodos en la malla
+ Mensajes de texto
+ La URL de este canal no es válida y no puede utilizarse
+ Panel de depuración
+ 500 últimos mensajes
+ Limpiar
+ Actualizando el firmware espera hasta ocho minutos...
+ Actualización exitosa
+ Actualización fallida
+ tiempo de recepción del mensaje
+ estado de recepción de mensajes
+ Estado de entrega del mensaje
+ Periodo de emisión de la posición (en segundos) 0 - deshabilitar
+ Período de reposo del dispositivo (en segundos)
+ Notificaciones de mensajes
+ El periodo mínimo de emisión de este canal es %d
+ Protocolo de prueba de esfuerzo
+ Configuración avanzada
+ Es necesario actualizar el firmware
+ Vale
+ ¡Debe establecer una región!
+ Región
+ No se puede cambiar de canal porque la radio aún no está conectada. Por favor inténtelo de nuevo.
+ 55.332244 34.442211
+ Guardar mensajes como csv...
+ Establecer las opciones de los canales
+ Reiniciar
+ ¿Estás seguro de que quieres cambiar al canal por defecto?
+ Restablecer los valores predeterminados
+ Aplique
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 225eadc86..e7bf4bb1b 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -76,4 +76,36 @@
Musíte zapnúť služby o polohe zariadenia (GPS) v nastaveniach systému Android.UNRECOGNIZEDTáto aplikácia je vyvíjaná hobbystami, našli by ste si chvíľku pre jej hodnotenie? Ak nájdete chyby, proím oznámte ich na našom fóre - meshtastic.discourse.group . Ďakujeme za Vašu podporu!
+ Zoznam zariadení v mesh sieti
+ Textové správy
+ URL tohoto kanála nie je platá a nemôže byť použitá
+ Debug okno
+ 500 posledných správ
+ Zmazať
+ Aktualizácia firmvéru, môže to trvať do 8 minút...
+ Aktualizácia úspešná
+ Aktualizácia zlyhala
+ čas prijatia správy
+ stav prijatia správy
+ stav doručenia správy
+ Interval rozosielania pozície (v sekundách)
+ Interval uspávania zariadenia (v sekundách)
+ Upozornenia na správy
+ Najkratší interval rozosielania pre tento kanál je %d
+ Stres test protokolu
+ Rozšírené nastavenia
+ Nutná aktualizácia formvéru
+ Ok
+ Musíte nastaviť región!
+ Región
+ 48.14816 17.10674
+ Ulož spávy ako CSV súbor
+ Nastaviť kanál
+ Resetovať
+ Resetovať na základné (default) nastavenia
+ Použiť
+ Ste si istý, že chcete preladiť na základný (default) kanál?
+ Nie je možné zmeniť kanál, pretože vysielač ešte nie je pripojený. Skúste to neskôr.
+ Firmvér vysielača je príliš zastaralý, aby dokázal komunikovať s aplikáciou. Prosím choďte na panel nastavení a zvoľte možnosť \"Aktualizácia firmvéru\". Viac informácií nájdete na našom sprievodcovi inštaláciou firmvéru na Github-e.
+ 0.1.01
diff --git a/app/src/main/res/values/firmwareversion.xml b/app/src/main/res/values/firmwareversion.xml
index b4bc9818d..5e6633ae8 100644
--- a/app/src/main/res/values/firmwareversion.xml
+++ b/app/src/main/res/values/firmwareversion.xml
@@ -1,3 +1,3 @@
- 0.1.01
+ 0.1.01
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 11f071ffd..1e930b4ac 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -90,11 +90,17 @@
Protocol stress testAdvanced settingsFirmware update required
- The radio firmware is too old to talk to this application, please go to the settings pane and choose "Update Firmware". For more information on this see our wiki.
+ The radio firmware is too old to talk to this application, please go to the settings pane and choose "Update Firmware". For more information on this see our Firmware Installation guide on Github.OkayYou must set a region!RegionCouldn\'t change channel, because radio is not yet connected. Please try again.55.332244 34.442211Save messages as csv...
+ Set channel options
+ Reset
+ Are you sure you want to change to the default channel?
+ Reset to defaults
+ Apply
+ No application found to send URLs
diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshServiceTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshServiceTest.kt
index aabea7598..40bd2c1c0 100644
--- a/app/src/test/java/com/geeksville/mesh/service/MeshServiceTest.kt
+++ b/app/src/test/java/com/geeksville/mesh/service/MeshServiceTest.kt
@@ -17,15 +17,7 @@ class MeshServiceTest {
val newerTime = 20
updateNodeInfoTime(nodeInfo, newerTime)
- Assert.assertEquals(newerTime, nodeInfo.position?.time)
- }
-
- @Test
- fun givenNodeInfo_whenUpdatingWithOldTime_thenPositionTimeIsNotUpdated() {
- val olderTime = 5
- val timeBeforeTryingToUpdate = nodeInfo.position?.time
- updateNodeInfoTime(nodeInfo, olderTime)
- Assert.assertEquals(timeBeforeTryingToUpdate, nodeInfo.position?.time)
+ Assert.assertEquals(newerTime, nodeInfo.lastHeard)
}
}
diff --git a/build.gradle b/build.gradle
index ba99badad..7702b8bea 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,8 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.4.31'
- ext.coroutines_version = "1.3.9"
+ ext.kotlin_version = '1.4.32'
+ ext.coroutines_version = "1.4.1"
repositories {
google()
@@ -21,7 +21,7 @@ buildscript {
// Check that you have the Google Services Gradle plugin v4.3.2 or later
// (if not, add it).
classpath 'com.google.gms:google-services:4.3.5'
- classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
+ classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2'
// protobuf plugin - docs here https://github.com/google/protobuf-gradle-plugin
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.15'
diff --git a/geeksville-androidlib b/geeksville-androidlib
index 158f6f2dd..f9bb14be9 160000
--- a/geeksville-androidlib
+++ b/geeksville-androidlib
@@ -1 +1 @@
-Subproject commit 158f6f2dd5dfe81833ed035d54045d7b34394e51
+Subproject commit f9bb14be97e5d04f73758e806ee6cc7a32a6f43d