kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Merge branch 'master' into master
commit
82ebfd0094
|
@ -5,6 +5,5 @@
|
|||
<mapping directory="$PROJECT_DIR$/app/src/main/proto" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/design" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/geeksville-androidlib" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/mesh_shared/src/main/proto" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -65,8 +65,7 @@ data class DataPacket(
|
|||
parcel.readInt(),
|
||||
parcel.readParcelable(MessageStatus::class.java.classLoader),
|
||||
parcel.readInt()
|
||||
) {
|
||||
}
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
|
|
@ -363,7 +363,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
rater.monitor() // Monitors the app launch times
|
||||
|
||||
// Only ask to rate if the user has a suitable store
|
||||
AppRate.showRateDialogIfMeetsConditions(this); // Shows the Rate Dialog when conditions are met
|
||||
AppRate.showRateDialogIfMeetsConditions(this) // Shows the Rate Dialog when conditions are met
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -489,9 +489,11 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
|
||||
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||
val device: UsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
|
||||
debug("Handle USB device attached! $device")
|
||||
usbDevice = device
|
||||
val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
|
||||
if (device != null) {
|
||||
debug("Handle USB device attached! $device")
|
||||
usbDevice = device
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_MAIN -> {
|
||||
|
@ -596,7 +598,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
filter.addAction(MeshService.actionReceived(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE))
|
||||
filter.addAction((MeshService.ACTION_MESSAGE_STATUS))
|
||||
registerReceiver(meshServiceReceiver, filter)
|
||||
receiverRegistered = true;
|
||||
receiverRegistered = true
|
||||
}
|
||||
|
||||
private fun unregisterMeshReceiver() {
|
||||
|
@ -663,30 +665,32 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
|
||||
debug("Getting latest radioconfig from service")
|
||||
try {
|
||||
val info = service.myNodeInfo
|
||||
val info: MyNodeInfo? = service.myNodeInfo // this can be null
|
||||
model.myNodeInfo.value = info
|
||||
|
||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
||||
if (isOld)
|
||||
showAlert(R.string.app_too_old, R.string.must_update)
|
||||
else {
|
||||
|
||||
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
|
||||
if (curVer < MeshService.minFirmwareVersion)
|
||||
showAlert(R.string.firmware_too_old, R.string.firmware_old)
|
||||
if (info != null) {
|
||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
||||
if (isOld)
|
||||
showAlert(R.string.app_too_old, R.string.must_update)
|
||||
else {
|
||||
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
|
||||
|
||||
model.radioConfig.value =
|
||||
RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
|
||||
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
|
||||
if (curVer < MeshService.minFirmwareVersion)
|
||||
showAlert(R.string.firmware_too_old, R.string.firmware_old)
|
||||
else {
|
||||
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
|
||||
|
||||
model.channels.value =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
|
||||
model.radioConfig.value =
|
||||
RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
|
||||
|
||||
updateNodesFromDevice()
|
||||
model.channels.value =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
|
||||
|
||||
// we have a connection to our device now, do the channel change
|
||||
perhapsChangeChannel()
|
||||
updateNodesFromDevice()
|
||||
|
||||
// we have a connection to our device now, do the channel change
|
||||
perhapsChangeChannel()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: RemoteException) {
|
||||
|
@ -799,13 +803,10 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
|
||||
MeshService.ACTION_MESH_CONNECTED -> {
|
||||
val connected =
|
||||
MeshService.ConnectionState.valueOf(
|
||||
intent.getStringExtra(
|
||||
EXTRA_CONNECTED
|
||||
)!!
|
||||
)
|
||||
onMeshConnectionChanged(connected)
|
||||
val extra = intent.getStringExtra(EXTRA_CONNECTED)
|
||||
if (extra != null) {
|
||||
onMeshConnectionChanged(MeshService.ConnectionState.valueOf(extra))
|
||||
}
|
||||
}
|
||||
else -> TODO()
|
||||
}
|
||||
|
@ -972,12 +973,11 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
|
||||
try {
|
||||
bindMeshService()
|
||||
}
|
||||
catch(ex: BindFailedException) {
|
||||
} catch (ex: BindFailedException) {
|
||||
// App is probably shutting down, ignore
|
||||
errormsg("Bind of MeshService failed")
|
||||
}
|
||||
|
||||
|
||||
val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
|
||||
if (!bonded && usbDevice == null) // we will handle USB later
|
||||
showSettingsPage()
|
||||
|
@ -1090,7 +1090,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
applicationContext.contentResolver.openFileDescriptor(file_uri, "w")?.use {
|
||||
FileOutputStream(it.fileDescriptor).use { fs ->
|
||||
// Write header
|
||||
fs.write(("from,rssi,snr,time,dist\n").toByteArray());
|
||||
fs.write(("from,rssi,snr,time,dist\n").toByteArray())
|
||||
// Packets are ordered by time, we keep most recent position of
|
||||
// our device in my_position.
|
||||
var my_position: MeshProtos.Position? = null
|
||||
|
@ -1101,8 +1101,12 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
my_position = position
|
||||
} else if (my_position != null) {
|
||||
val dist = positionToMeter(my_position!!, position).roundToInt()
|
||||
fs.write("%x,%d,%f,%d,%d\n".format(packet_proto.from,packet_proto.rxRssi,
|
||||
packet_proto.rxSnr, packet_proto.rxTime, dist).toByteArray())
|
||||
fs.write(
|
||||
"%x,%d,%f,%d,%d\n".format(
|
||||
packet_proto.from, packet_proto.rxRssi,
|
||||
packet_proto.rxSnr, packet_proto.rxTime, dist
|
||||
).toByteArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,13 +37,13 @@ class MeshUtilApplication : GeeksvilleApplication() {
|
|||
}
|
||||
|
||||
fun sendCrashReports() {
|
||||
if(isAnalyticsAllowed)
|
||||
if (isAnalyticsAllowed)
|
||||
crashlytics.sendUnsentReports()
|
||||
}
|
||||
|
||||
// Send any old reports if user approves
|
||||
sendCrashReports()
|
||||
|
||||
|
||||
// Attach to our exception wrapper
|
||||
Exceptions.reporter = { exception, _, _ ->
|
||||
crashlytics.recordException(exception)
|
||||
|
|
|
@ -32,8 +32,7 @@ data class MyNodeInfo(
|
|||
parcel.readInt(),
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
) {
|
||||
}
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeInt(myNodeNum)
|
||||
|
|
|
@ -43,7 +43,7 @@ data class Position(
|
|||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Int,
|
||||
val time: Int = currentTime(), // default to current time in secs
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val batteryPctLevel: Int = 0
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
|
@ -84,11 +84,13 @@ data class NodeInfo(
|
|||
var user: MeshUser? = null,
|
||||
var position: Position? = null,
|
||||
var snr: Float = Float.MAX_VALUE,
|
||||
var rssi: Int = Int.MAX_VALUE
|
||||
var rssi: Int = Int.MAX_VALUE,
|
||||
var lastHeard: Int = 0 // the last time we've seen this node in secs since 1970
|
||||
) : Parcelable {
|
||||
|
||||
/// Return the last time we've seen this node in secs since 1970
|
||||
val lastSeen get() = position?.time ?: 0
|
||||
/**
|
||||
* Return the last time we've seen this node in secs since 1970
|
||||
*/
|
||||
|
||||
val batteryPctLevel get() = position?.batteryPctLevel
|
||||
|
||||
|
@ -104,7 +106,7 @@ data class NodeInfo(
|
|||
// FIXME - use correct timeout from the device settings
|
||||
val timeout =
|
||||
15 * 60 // Don't set this timeout too tight, or otherwise we will stop sending GPS helper positions to our device
|
||||
return (now - lastSeen <= timeout) || lastSeen == 0
|
||||
return (now - lastHeard <= timeout) || lastHeard == 0
|
||||
}
|
||||
|
||||
/// return the position if it is valid, else null
|
||||
|
|
|
@ -7,9 +7,7 @@ import com.google.protobuf.ByteString
|
|||
fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
|
||||
|
||||
|
||||
data class Channel(
|
||||
val settings: ChannelProtos.ChannelSettings = ChannelProtos.ChannelSettings.getDefaultInstance()
|
||||
) {
|
||||
data class Channel(val settings: ChannelProtos.ChannelSettings) {
|
||||
companion object {
|
||||
// These bytes must match the well known and not secret bytes used the default channel AES128 key device code
|
||||
val channelDefaultKey = byteArrayOfInts(
|
||||
|
@ -17,10 +15,16 @@ data class Channel(
|
|||
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf
|
||||
)
|
||||
|
||||
private val cleartextPSK = ByteString.EMPTY
|
||||
private val defaultPSK =
|
||||
byteArrayOfInts(1) // a shortstring code to indicate we need our default PSK
|
||||
|
||||
// TH=he unsecured channel that devices ship with
|
||||
val defaultChannel = Channel(
|
||||
val default = Channel(
|
||||
ChannelProtos.ChannelSettings.newBuilder()
|
||||
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
|
||||
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096)
|
||||
.setPsk(ByteString.copyFrom(defaultPSK))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -50,7 +54,7 @@ data class Channel(
|
|||
val pskIndex = settings.psk.byteAt(0).toInt()
|
||||
|
||||
if (pskIndex == 0)
|
||||
ByteString.EMPTY // Treat as an empty PSK (no encryption)
|
||||
cleartextPSK
|
||||
else {
|
||||
// Treat an index of 1 as the old channelDefaultKey and work up from there
|
||||
val bytes = channelDefaultKey.clone()
|
||||
|
@ -73,6 +77,10 @@ data class Channel(
|
|||
|
||||
return "#${name}-${suffix}"
|
||||
}
|
||||
|
||||
override fun equals(o: Any?): Boolean = (o is Channel)
|
||||
&& psk.toByteArray() contentEquals o.psk.toByteArray()
|
||||
&& name == o.name
|
||||
}
|
||||
|
||||
fun xorHash(b: ByteArray) = b.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) })
|
|
@ -1,14 +1,29 @@
|
|||
package com.geeksville.mesh.model
|
||||
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
|
||||
enum class ChannelOption(val modemConfig: ChannelProtos.ChannelSettings.ModemConfig, val configRes: Int, val minBroadcastPeriodSecs: Int) {
|
||||
enum class ChannelOption(
|
||||
val modemConfig: ChannelProtos.ChannelSettings.ModemConfig,
|
||||
val configRes: Int,
|
||||
val minBroadcastPeriodSecs: Int
|
||||
) {
|
||||
SHORT(ChannelProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128, R.string.modem_config_short, 3),
|
||||
MEDIUM(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128, R.string.modem_config_medium, 12),
|
||||
LONG(ChannelProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512, R.string.modem_config_long, 240),
|
||||
VERY_LONG(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096, R.string.modem_config_very_long, 375);
|
||||
MEDIUM(
|
||||
ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128,
|
||||
R.string.modem_config_medium,
|
||||
12
|
||||
),
|
||||
LONG(
|
||||
ChannelProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512,
|
||||
R.string.modem_config_long,
|
||||
240
|
||||
),
|
||||
VERY_LONG(
|
||||
ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096,
|
||||
R.string.modem_config_very_long,
|
||||
375
|
||||
);
|
||||
|
||||
companion object {
|
||||
fun fromConfig(modemConfig: ChannelProtos.ChannelSettings.ModemConfig?): ChannelOption? {
|
||||
|
@ -18,6 +33,7 @@ enum class ChannelOption(val modemConfig: ChannelProtos.ChannelSettings.ModemCon
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val defaultMinBroadcastPeriod = VERY_LONG.minBroadcastPeriodSecs
|
||||
}
|
||||
}
|
|
@ -4,8 +4,6 @@ import android.graphics.Bitmap
|
|||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.journeyapps.barcodescanner.BarcodeEncoder
|
||||
|
@ -42,11 +40,12 @@ data class ChannelSet(
|
|||
/**
|
||||
* Return the primary channel info
|
||||
*/
|
||||
val primaryChannel: Channel? get() =
|
||||
if(protobuf.settingsCount > 0)
|
||||
Channel(protobuf.getSettings(0))
|
||||
else
|
||||
null
|
||||
val primaryChannel: Channel?
|
||||
get() =
|
||||
if (protobuf.settingsCount > 0)
|
||||
Channel(protobuf.getSettings(0))
|
||||
else
|
||||
null
|
||||
|
||||
/// Return an URL that represents the current channel values
|
||||
/// @param upperCasePrefix - portions of the URL can be upper case to make for more efficient QR codes
|
||||
|
@ -56,7 +55,7 @@ data class ChannelSet(
|
|||
val channelBytes = protobuf.toByteArray() ?: ByteArray(0) // if unset just use empty
|
||||
val enc = Base64.encodeToString(channelBytes, base64Flags)
|
||||
|
||||
val p = if(upperCasePrefix)
|
||||
val p = if (upperCasePrefix)
|
||||
prefix.toUpperCase()
|
||||
else
|
||||
prefix
|
||||
|
@ -68,7 +67,12 @@ data class ChannelSet(
|
|||
|
||||
// We encode as UPPER case for the QR code URL because QR codes are more efficient for that special case
|
||||
val bitMatrix =
|
||||
multiFormatWriter.encode(getChannelUrl(true).toString(), BarcodeFormat.QR_CODE, 192, 192);
|
||||
multiFormatWriter.encode(
|
||||
getChannelUrl(true).toString(),
|
||||
BarcodeFormat.QR_CODE,
|
||||
192,
|
||||
192
|
||||
)
|
||||
val barcodeEncoder = BarcodeEncoder()
|
||||
return barcodeEncoder.createBitmap(bitMatrix)
|
||||
}
|
||||
|
|
|
@ -5,14 +5,15 @@ import com.geeksville.android.Logging
|
|||
/**
|
||||
* Provide structured access to parse and compare device version strings
|
||||
*/
|
||||
data class DeviceVersion(val asString: String): Comparable<DeviceVersion>, Logging {
|
||||
data class DeviceVersion(val asString: String) : Comparable<DeviceVersion>, Logging {
|
||||
|
||||
val asInt get() = try {
|
||||
verStringToInt(asString)
|
||||
} catch(e: Exception) {
|
||||
warn("Exception while parsing version '$asString', assuming version 0")
|
||||
0
|
||||
}
|
||||
val asInt
|
||||
get() = try {
|
||||
verStringToInt(asString)
|
||||
} catch (e: Exception) {
|
||||
warn("Exception while parsing version '$asString', assuming version 0")
|
||||
0
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a version string of the form 1.23.57 to a comparable integer of
|
||||
|
|
|
@ -12,7 +12,9 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.RadioConfigProtos
|
||||
import com.geeksville.mesh.database.MeshtasticDatabase
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
|
@ -134,15 +136,11 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
|
|||
}
|
||||
}
|
||||
|
||||
var region: RadioConfigProtos.RegionCode?
|
||||
get() = radioConfig.value?.preferences?.region
|
||||
var region: RadioConfigProtos.RegionCode
|
||||
get() = meshService?.region?.let { RadioConfigProtos.RegionCode.forNumber(it) }
|
||||
?: RadioConfigProtos.RegionCode.Unset
|
||||
set(value) {
|
||||
val config = radioConfig.value
|
||||
if (value != null && config != null) {
|
||||
val builder = config.toBuilder()
|
||||
builder.preferencesBuilder.region = value
|
||||
setRadioConfig(builder.build())
|
||||
}
|
||||
meshService?.region = value.number
|
||||
}
|
||||
|
||||
/// hardware info about our local device (can be null)
|
||||
|
|
|
@ -5,4 +5,8 @@ import java.util.*
|
|||
|
||||
open class BLEException(msg: String) : IOException(msg)
|
||||
|
||||
open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid")
|
||||
open class BLECharacteristicNotFoundException(uuid: UUID) :
|
||||
BLEException("Can't get characteristic $uuid")
|
||||
|
||||
/// Our interface is being shut down
|
||||
open class BLEConnectionClosing : BLEException("Connection closing ")
|
|
@ -78,7 +78,15 @@ A variable keepAllPackets, if set to true will suppress this behavior and instea
|
|||
class BluetoothInterface(val service: RadioInterfaceService, val address: String) : IRadioInterface,
|
||||
Logging {
|
||||
|
||||
companion object : Logging {
|
||||
companion object : Logging, InterfaceFactory('x') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = BluetoothInterface(service, rest)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
|
||||
/// this service UUID is publically visible for scanning
|
||||
val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")
|
||||
|
@ -97,15 +105,13 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
return bluetoothManager.adapter
|
||||
}
|
||||
|
||||
fun toInterfaceName(deviceName: String) = "x$deviceName"
|
||||
|
||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||
fun addressValid(context: Context, address: String): Boolean {
|
||||
override fun addressValid(context: Context, rest: String): Boolean {
|
||||
val allPaired =
|
||||
getBluetoothAdapter(context)?.bondedDevices.orEmpty().map { it.address }.toSet()
|
||||
|
||||
return if (!allPaired.contains(address)) {
|
||||
warn("Ignoring stale bond to ${address.anonymize}")
|
||||
return if (!allPaired.contains(rest)) {
|
||||
warn("Ignoring stale bond to ${rest.anonymize}")
|
||||
false
|
||||
} else
|
||||
true
|
||||
|
@ -313,7 +319,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
var fromNumChanged = false
|
||||
|
||||
private fun startWatchingFromNum() {
|
||||
safe!!.setNotify(fromNum, true) {
|
||||
safe?.setNotify(fromNum, true) {
|
||||
// We might get multiple notifies before we get around to reading from the radio - so just set one flag
|
||||
fromNumChanged = true
|
||||
debug("fromNum changed")
|
||||
|
@ -469,7 +475,11 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
safe =
|
||||
null // We do this first, because if we throw we still want to mark that we no longer have a valid connection
|
||||
|
||||
s?.close()
|
||||
try {
|
||||
s?.close()
|
||||
} catch (_: BLEConnectionClosing) {
|
||||
warn("Ignoring BLE errors while closing")
|
||||
}
|
||||
} else {
|
||||
debug("Radio was not connected, skipping disable")
|
||||
}
|
||||
|
|
|
@ -26,5 +26,9 @@ class BluetoothStateReceiver(
|
|||
}
|
||||
}
|
||||
|
||||
private val Intent.bluetoothAdapterState: Int get() = getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)
|
||||
private val Intent.bluetoothAdapterState: Int
|
||||
get() = getIntExtra(
|
||||
BluetoothAdapter.EXTRA_STATE,
|
||||
-1
|
||||
)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* A base class for the singleton factories that make interfaces. One instance per interface type
|
||||
*/
|
||||
abstract class InterfaceFactory(val prefix: Char) {
|
||||
companion object {
|
||||
private val factories = mutableMapOf<Char, InterfaceFactory>()
|
||||
|
||||
fun getFactory(l: Char) = factories.get(l)
|
||||
}
|
||||
|
||||
protected fun registerFactory() {
|
||||
factories[prefix] = this
|
||||
}
|
||||
|
||||
abstract fun createInterface(service: RadioInterfaceService, rest: String): IRadioInterface
|
||||
|
||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||
open fun addressValid(context: Context, rest: String): Boolean = true
|
||||
}
|
|
@ -38,6 +38,7 @@ import kotlinx.coroutines.*
|
|||
import kotlinx.serialization.json.Json
|
||||
import java.util.*
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Handles all the communication with android apps. Also keeps an internal model
|
||||
|
@ -55,7 +56,7 @@ class MeshService : Service(), Logging {
|
|||
/* @Deprecated(message = "Does not filter by port number. For legacy reasons only broadcast for UNKNOWN_APP, switch to ACTION_RECEIVED")
|
||||
const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA" */
|
||||
|
||||
fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
||||
private fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
||||
|
||||
/// generate a RECEIVED action filter string that includes either the portnumber as an int, or preferably a symbolic name from portnums.proto
|
||||
fun actionReceived(portNum: Int): String {
|
||||
|
@ -70,7 +71,7 @@ class MeshService : Service(), Logging {
|
|||
const val ACTION_MESSAGE_STATUS = "$prefix.MESSAGE_STATUS"
|
||||
|
||||
open class NodeNotFoundException(reason: String) : Exception(reason)
|
||||
class InvalidNodeIdException() : NodeNotFoundException("Invalid NodeId")
|
||||
class InvalidNodeIdException : NodeNotFoundException("Invalid NodeId")
|
||||
class NodeNumNotFoundException(id: Int) : NodeNotFoundException("NodeNum not found $id")
|
||||
class IdNotFoundException(id: String) : NodeNotFoundException("ID not found $id")
|
||||
|
||||
|
@ -78,7 +79,7 @@ class MeshService : Service(), Logging {
|
|||
RadioNotConnectedException(message)
|
||||
|
||||
/** We treat software update as similar to loss of comms to the regular bluetooth service (so things like sendPosition for background GPS ignores the problem */
|
||||
class IsUpdatingException() :
|
||||
class IsUpdatingException :
|
||||
RadioNotConnectedException("Operation prohibited during firmware update")
|
||||
|
||||
/**
|
||||
|
@ -132,7 +133,7 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
private val locationCallback = MeshServiceLocationCallback(
|
||||
::sendPositionScoped,
|
||||
::perhapsSendPosition,
|
||||
onSendPositionFailed = { onConnectionChanged(ConnectionState.DEVICE_SLEEP) },
|
||||
getNodeNum = { myNodeNum }
|
||||
)
|
||||
|
@ -160,6 +161,36 @@ class MeshService : Service(), Logging {
|
|||
).show()
|
||||
}
|
||||
|
||||
private var locationIntervalMsec = 0L
|
||||
|
||||
/**
|
||||
* a periodic callback that perhaps send our position to other nodes.
|
||||
* We first check to see if our local device has already sent a position and if so, we punt until the next check.
|
||||
* This allows us to only 'fill in' with GPS positions when the local device happens to have no good GPS sats.
|
||||
*/
|
||||
private fun perhapsSendPosition(
|
||||
lat: Double = 0.0,
|
||||
lon: Double = 0.0,
|
||||
alt: Int = 0,
|
||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) {
|
||||
// This operation can take a while, so instead of staying in the callback (location services) context
|
||||
// do most of the work in my service thread
|
||||
serviceScope.handledLaunch {
|
||||
// if android called us too soon, just ignore
|
||||
|
||||
val myInfo = localNodeInfo
|
||||
val lastSendMsec = (myInfo?.position?.time ?: 0) * 1000L
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastSendMsec < locationIntervalMsec)
|
||||
debug("Not sending position - the local node has sent one recently...")
|
||||
else {
|
||||
sendPosition(lat, lon, alt, destNum, wantResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* start our location requests (if they weren't already running)
|
||||
*
|
||||
|
@ -172,6 +203,7 @@ class MeshService : Service(), Logging {
|
|||
if (fusedLocationClient == null && isGooglePlayAvailable(this)) {
|
||||
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
|
||||
|
||||
locationIntervalMsec = requestInterval
|
||||
val request = LocationRequest.create().apply {
|
||||
interval = requestInterval
|
||||
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
|
||||
|
@ -242,24 +274,31 @@ class MeshService : Service(), Logging {
|
|||
get() = (if (connectionState == ConnectionState.CONNECTED) radio.serviceP else null)
|
||||
?: throw RadioNotConnectedException()
|
||||
|
||||
/// Send a command/packet to our radio. But cope with the possiblity that we might start up
|
||||
/// before we are fully bound to the RadioInterfaceService
|
||||
private fun sendToRadio(p: ToRadio.Builder) {
|
||||
/** Send a command/packet to our radio. But cope with the possiblity that we might start up
|
||||
before we are fully bound to the RadioInterfaceService
|
||||
@param requireConnected set to false if you are okay with using a partially connected device (i.e. during startup)
|
||||
*/
|
||||
private fun sendToRadio(p: ToRadio.Builder, requireConnected: Boolean = true) {
|
||||
val b = p.build().toByteArray()
|
||||
|
||||
if (SoftwareUpdateService.isUpdating)
|
||||
throw IsUpdatingException()
|
||||
|
||||
connectedRadio.sendToRadio(b)
|
||||
if (requireConnected)
|
||||
connectedRadio.sendToRadio(b)
|
||||
else {
|
||||
val s = radio.serviceP ?: throw RadioNotConnectedException()
|
||||
s.sendToRadio(b)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a mesh packet to the radio, if the radio is not currently connected this function will throw NotConnectedException
|
||||
*/
|
||||
private fun sendToRadio(packet: MeshPacket) {
|
||||
private fun sendToRadio(packet: MeshPacket, requireConnected: Boolean = true) {
|
||||
sendToRadio(ToRadio.newBuilder().apply {
|
||||
this.packet = packet
|
||||
})
|
||||
}, requireConnected)
|
||||
}
|
||||
|
||||
private fun updateMessageNotification(message: DataPacket) =
|
||||
|
@ -428,7 +467,7 @@ class MeshService : Service(), Logging {
|
|||
|
||||
private var radioConfig: RadioConfigProtos.RadioConfig? = null
|
||||
|
||||
private var channels = arrayOf<ChannelProtos.Channel>()
|
||||
private var channels = fixupChannelList(listOf())
|
||||
|
||||
/// True after we've done our initial node db init
|
||||
@Volatile
|
||||
|
@ -454,6 +493,17 @@ class MeshService : Service(), Logging {
|
|||
n
|
||||
)
|
||||
|
||||
/**
|
||||
* Return the nodeinfo for the local node, or null if not found
|
||||
*/
|
||||
private val localNodeInfo
|
||||
get(): NodeInfo? =
|
||||
try {
|
||||
toNodeInfo(myNodeNum)
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
/** Map a nodenum to the nodeid string, or return null if not present
|
||||
If we have a NodeInfo for this ID we prefer to return the string ID inside the user record.
|
||||
but some nodes might not have a user record at all (because not yet received), in that case, we return
|
||||
|
@ -500,9 +550,13 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
/// A helper function that makes it easy to update node info objects
|
||||
private fun updateNodeInfo(nodeNum: Int, updatefn: (NodeInfo) -> Unit) {
|
||||
private fun updateNodeInfo(
|
||||
nodeNum: Int,
|
||||
withBroadcast: Boolean = true,
|
||||
updateFn: (NodeInfo) -> Unit
|
||||
) {
|
||||
val info = getOrCreateNodeInfo(nodeNum)
|
||||
updatefn(info)
|
||||
updateFn(info)
|
||||
|
||||
// This might have been the first time we know an ID for this node, so also update the by ID map
|
||||
val userId = info.user?.id.orEmpty()
|
||||
|
@ -510,7 +564,8 @@ class MeshService : Service(), Logging {
|
|||
nodeDBbyID[userId] = info
|
||||
|
||||
// parcelable is busted
|
||||
serviceBroadcasts.broadcastNodeChange(info)
|
||||
if (withBroadcast)
|
||||
serviceBroadcasts.broadcastNodeChange(info)
|
||||
}
|
||||
|
||||
/// My node num
|
||||
|
@ -549,7 +604,7 @@ class MeshService : Service(), Logging {
|
|||
setChannel(it)
|
||||
}
|
||||
|
||||
channels = asChannels.toTypedArray()
|
||||
channels = fixupChannelList(asChannels)
|
||||
}
|
||||
|
||||
/// Generate a new mesh packet builder with our node as the sender, and the specified node num
|
||||
|
@ -716,7 +771,10 @@ class MeshService : Service(), Logging {
|
|||
|
||||
// Handle new style position info
|
||||
Portnums.PortNum.POSITION_APP_VALUE -> {
|
||||
val u = MeshProtos.Position.parseFrom(data.payload)
|
||||
var u = MeshProtos.Position.parseFrom(data.payload)
|
||||
// position updates from mesh usually don't include times. So promote rx time
|
||||
if (u.time == 0 && packet.rxTime != 0)
|
||||
u = u.toBuilder().setTime(packet.rxTime).build()
|
||||
debug("position_app ${packet.from} ${u.toOneLineString()}")
|
||||
handleReceivedPosition(packet.from, u, dataPacket.time)
|
||||
}
|
||||
|
@ -781,6 +839,7 @@ class MeshService : Service(), Logging {
|
|||
val mi = myNodeInfo
|
||||
if (mi != null) {
|
||||
val ch = a.getChannelResponse
|
||||
// add new entries if needed
|
||||
channels[ch.index] = ch
|
||||
debug("Admin: Received channel ${ch.index}")
|
||||
if (ch.index + 1 < mi.maxChannels) {
|
||||
|
@ -827,11 +886,16 @@ class MeshService : Service(), Logging {
|
|||
p: MeshProtos.Position,
|
||||
defaultTime: Long = System.currentTimeMillis()
|
||||
) {
|
||||
updateNodeInfo(fromNum) {
|
||||
debug("update ${it.user?.longName} with ${p.toOneLineString()}")
|
||||
it.position = Position(p)
|
||||
updateNodeInfoTime(it, (defaultTime / 1000).toInt())
|
||||
}
|
||||
// Nodes periodically send out position updates, but those updates might not contain a lat & lon (because no GPS lock)
|
||||
// We like to look at the local node to see if it has been sending out valid lat/lon, so for the LOCAL node (only)
|
||||
// we don't record these nop position updates
|
||||
if (myNodeNum == fromNum && p.latitudeI == 0 && p.longitudeI == 0)
|
||||
debug("Ignoring nop position update for the local node")
|
||||
else
|
||||
updateNodeInfo(fromNum) {
|
||||
debug("update position: ${it.user?.longName} with ${p.toOneLineString()}")
|
||||
it.position = Position(p, (defaultTime / 1000L).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
/// If packets arrive before we have our node DB, we delay parsing them until the DB is ready
|
||||
|
@ -914,15 +978,17 @@ class MeshService : Service(), Logging {
|
|||
// Update last seen for the node that sent the packet, but also for _our node_ because anytime a packet passes
|
||||
// through our node on the way to the phone that means that local node is also alive in the mesh
|
||||
|
||||
updateNodeInfo(myNodeNum) {
|
||||
it.position = it.position?.copy(time = currentSecond())
|
||||
val isOtherNode = myNodeNum != fromNum
|
||||
updateNodeInfo(myNodeNum, withBroadcast = isOtherNode) {
|
||||
it.lastHeard = currentSecond()
|
||||
}
|
||||
|
||||
// if (p.hasPosition()) handleReceivedPosition(fromNum, p.position, rxTime)
|
||||
// Do not generate redundant broadcasts of node change for this bookkeeping updateNodeInfo call
|
||||
// because apps really only care about important updates of node state - which handledReceivedData will give them
|
||||
updateNodeInfo(fromNum, withBroadcast = false) {
|
||||
// If the rxTime was not set by the device (because device software was old), guess at a time
|
||||
val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond()
|
||||
|
||||
// If the rxTime was not set by the device (because device software was old), guess at a time
|
||||
val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond()
|
||||
updateNodeInfo(fromNum) {
|
||||
// Update our last seen based on any valid timestamps. If the device didn't provide a timestamp make one
|
||||
updateNodeInfoTime(it, rxTime)
|
||||
it.snr = packet.rxSnr
|
||||
|
@ -946,28 +1012,32 @@ class MeshService : Service(), Logging {
|
|||
/// If we just changed our nodedb, we might want to do somethings
|
||||
private fun onNodeDBChanged() {
|
||||
maybeUpdateServiceStatusNotification()
|
||||
|
||||
serviceScope.handledLaunch(Dispatchers.Main) {
|
||||
setupLocationRequest()
|
||||
}
|
||||
}
|
||||
|
||||
private var locationRequestInterval: Long = 0;
|
||||
private fun setupLocationRequest() {
|
||||
val desiredInterval: Long = if (myNodeInfo?.hasGPS == true) {
|
||||
0L // no requests when device has GPS
|
||||
} else if (numOnlineNodes < 2) {
|
||||
5 * 60 * 1000L // send infrequently, device needs these requests to set its clock
|
||||
} else {
|
||||
radioConfig?.preferences?.positionBroadcastSecs?.times(1000L) ?: 5 * 60 * 1000L
|
||||
}
|
||||
stopLocationRequests()
|
||||
val mi = myNodeInfo
|
||||
val prefs = radioConfig?.preferences
|
||||
if (mi != null && prefs != null) {
|
||||
var broadcastSecs = prefs.positionBroadcastSecs
|
||||
|
||||
debug("desired location request $desiredInterval, current $locationRequestInterval")
|
||||
var desiredInterval = if (broadcastSecs == 0) // unset by device, use default
|
||||
15 * 60 * 1000L
|
||||
else
|
||||
broadcastSecs * 1000L
|
||||
|
||||
if (desiredInterval != locationRequestInterval) {
|
||||
if (locationRequestInterval > 0) stopLocationRequests()
|
||||
if (desiredInterval > 0) startLocationRequests(desiredInterval)
|
||||
locationRequestInterval = desiredInterval
|
||||
if (prefs.locationShare == RadioConfigProtos.LocationSharing.LocDisabled) {
|
||||
info("GPS location sharing is disabled")
|
||||
desiredInterval = 0
|
||||
}
|
||||
|
||||
if (desiredInterval != 0L) {
|
||||
info("desired GPS assistance interval $desiredInterval")
|
||||
startLocationRequests(desiredInterval)
|
||||
} else {
|
||||
info("No GPS assistance desired, but sending UTC time to mesh")
|
||||
sendPosition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1079,7 +1149,7 @@ class MeshService : Service(), Logging {
|
|||
// claim we have a valid connection still
|
||||
connectionState = ConnectionState.DEVICE_SLEEP
|
||||
startDeviceSleep()
|
||||
throw ex; // Important to rethrow so that we don't tell the app all is well
|
||||
throw ex // Important to rethrow so that we don't tell the app all is well
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1315,13 +1385,45 @@ class MeshService : Service(), Logging {
|
|||
radioConfig = null
|
||||
|
||||
// prefill the channel array with null channels
|
||||
channels = Array(myInfo.maxChannels) {
|
||||
val b = ChannelProtos.Channel.newBuilder()
|
||||
b.index = it
|
||||
b.build()
|
||||
}
|
||||
channels = fixupChannelList(listOf<ChannelProtos.Channel>())
|
||||
}
|
||||
|
||||
/// scan the channel list and make sure it has one PRIMARY channel and is maxChannels long
|
||||
private fun fixupChannelList(lIn: List<ChannelProtos.Channel>): Array<ChannelProtos.Channel> {
|
||||
// When updating old firmware, we will briefly be told that there is zero channels
|
||||
val maxChannels =
|
||||
max(myNodeInfo?.maxChannels ?: 8, 8) // If we don't have my node info, assume 8 channels
|
||||
var l = lIn
|
||||
while (l.size < maxChannels) {
|
||||
val b = ChannelProtos.Channel.newBuilder()
|
||||
b.index = l.size
|
||||
l += b.build()
|
||||
}
|
||||
return l.toTypedArray()
|
||||
}
|
||||
|
||||
|
||||
private fun setRegionOnDevice() {
|
||||
val curConfigRegion =
|
||||
radioConfig?.preferences?.region ?: RadioConfigProtos.RegionCode.Unset
|
||||
|
||||
if (curConfigRegion.number != curRegionValue && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE)
|
||||
if (deviceVersion >= minFirmwareVersion) {
|
||||
info("Telling device to upgrade region")
|
||||
|
||||
// Tell the device to set the new region field (old devices will simply ignore this)
|
||||
radioConfig?.let { currentConfig ->
|
||||
val newConfig = currentConfig.toBuilder()
|
||||
|
||||
val newPrefs = currentConfig.preferences.toBuilder()
|
||||
newPrefs.regionValue = curRegionValue
|
||||
newConfig.preferences = newPrefs.build()
|
||||
|
||||
sendRadioConfig(newConfig.build())
|
||||
}
|
||||
} else
|
||||
warn("Device is too old to understand region changes")
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are updating nodes we might need to use old (fixed by firmware build)
|
||||
|
@ -1356,31 +1458,14 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
// If nothing was set in our (new style radio preferences, but we now have a valid setting - slam it in)
|
||||
if (curConfigRegion == RadioConfigProtos.RegionCode.Unset && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE) {
|
||||
if (deviceVersion >= minFirmwareVersion) {
|
||||
info("Telling device to upgrade region")
|
||||
|
||||
// Tell the device to set the new region field (old devices will simply ignore this)
|
||||
radioConfig?.let { currentConfig ->
|
||||
val newConfig = currentConfig.toBuilder()
|
||||
|
||||
val newPrefs = currentConfig.preferences.toBuilder()
|
||||
newPrefs.regionValue = curRegionValue
|
||||
newConfig.preferences = newPrefs.build()
|
||||
|
||||
sendRadioConfig(newConfig.build())
|
||||
}
|
||||
}
|
||||
else
|
||||
warn("Device is too old to understand region changes")
|
||||
}
|
||||
setRegionOnDevice()
|
||||
}
|
||||
}
|
||||
|
||||
/// If we've received our initial config, our radio settings and all of our channels, send any queueed packets and broadcast connected to clients
|
||||
private fun onHasSettings() {
|
||||
|
||||
processEarlyPackets() // send receive any packets that were queued up
|
||||
processEarlyPackets() // send any packets that were queued up
|
||||
|
||||
// broadcast an intent with our new connection state
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
|
@ -1388,6 +1473,8 @@ class MeshService : Service(), Logging {
|
|||
reportConnection()
|
||||
|
||||
updateRegion()
|
||||
|
||||
setupLocationRequest() // start sending location packets if needed
|
||||
}
|
||||
|
||||
private fun handleConfigComplete(configCompleteId: Int) {
|
||||
|
@ -1432,13 +1519,13 @@ class MeshService : Service(), Logging {
|
|||
private fun requestRadioConfig() {
|
||||
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) {
|
||||
getRadioRequest = true
|
||||
})
|
||||
}, requireConnected = false)
|
||||
}
|
||||
|
||||
private fun requestChannel(channelIndex: Int) {
|
||||
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) {
|
||||
getChannelRequest = channelIndex + 1
|
||||
})
|
||||
}, requireConnected = false)
|
||||
}
|
||||
|
||||
private fun setChannel(channel: ChannelProtos.Channel) {
|
||||
|
@ -1466,48 +1553,41 @@ class MeshService : Service(), Logging {
|
|||
* Must be called from serviceScope. Use sendPositionScoped() for direct calls.
|
||||
*/
|
||||
private fun sendPosition(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
alt: Int,
|
||||
lat: Double = 0.0,
|
||||
lon: Double = 0.0,
|
||||
alt: Int = 0,
|
||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) {
|
||||
debug("Sending our position to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
||||
|
||||
val position = MeshProtos.Position.newBuilder().also {
|
||||
it.longitudeI = Position.degI(lon)
|
||||
it.latitudeI = Position.degI(lat)
|
||||
|
||||
it.altitude = alt
|
||||
it.time = currentSecond() // Include our current timestamp
|
||||
}.build()
|
||||
|
||||
// Also update our own map for our nodenum, by handling the packet just like packets from other users
|
||||
handleReceivedPosition(myNodeInfo!!.myNodeNum, position)
|
||||
|
||||
val fullPacket =
|
||||
newMeshPacketTo(destNum).buildMeshPacket(priority = MeshProtos.MeshPacket.Priority.BACKGROUND) {
|
||||
// Use the new position as data format
|
||||
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
||||
payload = position.toByteString()
|
||||
|
||||
this.wantResponse = wantResponse
|
||||
}
|
||||
|
||||
// send the packet into the mesh
|
||||
sendToRadio(fullPacket)
|
||||
}
|
||||
|
||||
private fun sendPositionScoped(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
alt: Int,
|
||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) = serviceScope.handledLaunch {
|
||||
try {
|
||||
sendPosition(lat, lon, alt, destNum, wantResponse)
|
||||
} catch (ex: RadioNotConnectedException) {
|
||||
val mi = myNodeInfo
|
||||
if (mi != null) {
|
||||
debug("Sending our position/time to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
||||
|
||||
val position = MeshProtos.Position.newBuilder().also {
|
||||
it.longitudeI = Position.degI(lon)
|
||||
it.latitudeI = Position.degI(lat)
|
||||
|
||||
it.altitude = alt
|
||||
it.time = currentSecond() // Include our current timestamp
|
||||
}.build()
|
||||
|
||||
// Also update our own map for our nodenum, by handling the packet just like packets from other users
|
||||
handleReceivedPosition(mi.myNodeNum, position)
|
||||
|
||||
val fullPacket =
|
||||
newMeshPacketTo(destNum).buildMeshPacket(priority = MeshProtos.MeshPacket.Priority.BACKGROUND) {
|
||||
// Use the new position as data format
|
||||
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
||||
payload = position.toByteString()
|
||||
|
||||
this.wantResponse = wantResponse
|
||||
}
|
||||
|
||||
// send the packet into the mesh
|
||||
sendToRadio(fullPacket)
|
||||
}
|
||||
} catch (ex: BLEException) {
|
||||
warn("Ignoring disconnected radio during gps location update")
|
||||
}
|
||||
}
|
||||
|
@ -1539,9 +1619,7 @@ class MeshService : Service(), Logging {
|
|||
val myNode = myNodeInfo
|
||||
if (myNode != null) {
|
||||
|
||||
|
||||
val myInfo = toNodeInfo(myNode.myNodeNum)
|
||||
if (longName == myInfo.user?.longName && shortName == myInfo.user?.shortName)
|
||||
if (longName == localNodeInfo?.user?.longName && shortName == localNodeInfo?.user?.shortName)
|
||||
debug("Ignoring nop owner change")
|
||||
else {
|
||||
debug("SetOwner $myId : ${longName.anonymize} : $shortName")
|
||||
|
@ -1627,8 +1705,10 @@ class MeshService : Service(), Logging {
|
|||
} else {
|
||||
debug("Creating firmware update coroutine")
|
||||
updateJob = serviceScope.handledLaunch {
|
||||
debug("Starting firmware update coroutine")
|
||||
SoftwareUpdateService.doUpdate(this@MeshService, safe, filename)
|
||||
exceptionReporter {
|
||||
debug("Starting firmware update coroutine")
|
||||
SoftwareUpdateService.doUpdate(this@MeshService, safe, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1660,7 +1740,7 @@ class MeshService : Service(), Logging {
|
|||
offlineSentPackets.add(p)
|
||||
}
|
||||
|
||||
val binder = object : IMeshService.Stub() {
|
||||
private val binder = object : IMeshService.Stub() {
|
||||
|
||||
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
|
||||
debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
|
||||
|
@ -1684,6 +1764,12 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
override fun getUpdateStatus(): Int = SoftwareUpdateService.progress
|
||||
override fun getRegion(): Int = curRegionValue
|
||||
|
||||
override fun setRegion(regionCode: Int) = toRemoteExceptions {
|
||||
curRegionValue = regionCode
|
||||
setRegionOnDevice()
|
||||
}
|
||||
|
||||
override fun startFirmwareUpdate() = toRemoteExceptions {
|
||||
doFirmwareUpdate()
|
||||
|
@ -1788,6 +1874,5 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
fun updateNodeInfoTime(it: NodeInfo, rxTime: Int) {
|
||||
if (it.position?.time == null || it.position?.time!! < rxTime)
|
||||
it.position = it.position?.copy(time = rxTime)
|
||||
it.lastHeard = rxTime
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Intent
|
|||
import android.os.Parcelable
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.Portnums
|
||||
|
||||
class MeshServiceBroadcasts(
|
||||
private val context: Context,
|
||||
|
@ -18,7 +17,12 @@ class MeshServiceBroadcasts(
|
|||
*/
|
||||
fun broadcastReceivedData(payload: DataPacket) {
|
||||
|
||||
explicitBroadcast(Intent(MeshService.actionReceived(payload.dataType)).putExtra(EXTRA_PAYLOAD, payload))
|
||||
explicitBroadcast(
|
||||
Intent(MeshService.actionReceived(payload.dataType)).putExtra(
|
||||
EXTRA_PAYLOAD,
|
||||
payload
|
||||
)
|
||||
)
|
||||
|
||||
/*
|
||||
// For the time being we ALSO broadcast using old ACTION_RECEIVED_DATA field for any oldschool opaque packets
|
||||
|
|
|
@ -21,8 +21,7 @@ typealias GetNodeNum = () -> Int
|
|||
class MeshServiceLocationCallback(
|
||||
private val onSendPosition: SendPosition,
|
||||
private val onSendPositionFailed: OnSendFailure,
|
||||
private val getNodeNum: GetNodeNum,
|
||||
private val sendRateLimitInSeconds: Int = DEFAULT_SEND_RATE_LIMIT
|
||||
private val getNodeNum: GetNodeNum
|
||||
) : LocationCallback() {
|
||||
|
||||
companion object {
|
||||
|
@ -40,7 +39,8 @@ class MeshServiceLocationCallback(
|
|||
|
||||
try {
|
||||
// Do we want to broadcast this position globally, or are we just telling the local node what its current position is (
|
||||
val shouldBroadcast = isAllowedToSend()
|
||||
val shouldBroadcast =
|
||||
true // no need to rate limit, because we are just sending at the interval requested by the preferences
|
||||
val destinationNumber =
|
||||
if (shouldBroadcast) DataPacket.NODENUM_BROADCAST else getNodeNum()
|
||||
|
||||
|
@ -69,17 +69,4 @@ class MeshServiceLocationCallback(
|
|||
wantResponse // wantResponse?
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting function.
|
||||
*/
|
||||
private fun isAllowedToSend(): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
// we limit our sends onto the lora net to a max one once every FIXME
|
||||
val sendLora = (now - lastSendTimeMs >= sendRateLimitInSeconds * 1000)
|
||||
if (sendLora) {
|
||||
lastSendTimeMs = now
|
||||
}
|
||||
return sendLora
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,9 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MainActivity
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.notificationManager
|
||||
import com.geeksville.mesh.ui.SLogging
|
||||
import com.geeksville.mesh.utf8
|
||||
import java.io.Closeable
|
||||
|
||||
|
||||
|
@ -29,6 +26,7 @@ class MeshServiceNotifications(
|
|||
private val context: Context
|
||||
) : Closeable {
|
||||
private val notificationManager: NotificationManager get() = context.notificationManager
|
||||
|
||||
// We have two notification channels: one for general service status and another one for messages
|
||||
val notifyId = 101
|
||||
private val messageNotifyId = 102
|
||||
|
@ -96,12 +94,16 @@ class MeshServiceNotifications(
|
|||
}
|
||||
|
||||
fun updateServiceStateNotification(summaryString: String) =
|
||||
notificationManager.notify(notifyId,
|
||||
createServiceStateNotification(summaryString))
|
||||
notificationManager.notify(
|
||||
notifyId,
|
||||
createServiceStateNotification(summaryString)
|
||||
)
|
||||
|
||||
fun updateMessageNotification(name: String, message: String) =
|
||||
notificationManager.notify(messageNotifyId,
|
||||
createMessageNotifcation(name, message))
|
||||
notificationManager.notify(
|
||||
messageNotifyId,
|
||||
createMessageNotifcation(name, message)
|
||||
)
|
||||
|
||||
private val openAppIntent: PendingIntent by lazy {
|
||||
PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0)
|
||||
|
@ -126,7 +128,7 @@ class MeshServiceNotifications(
|
|||
return bitmap
|
||||
}
|
||||
|
||||
fun commonBuilder(channel: String) : NotificationCompat.Builder {
|
||||
fun commonBuilder(channel: String): NotificationCompat.Builder {
|
||||
val builder = NotificationCompat.Builder(context, channel)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(openAppIntent)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.RadioConfigProtos
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/// Our saved preferences as stored on disk
|
||||
|
|
|
@ -3,18 +3,14 @@ package com.geeksville.mesh.service
|
|||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.*
|
||||
import com.geeksville.andlib.BuildConfig
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Helper that calls MeshService.startService()
|
||||
*/
|
||||
public class ServiceStarter(
|
||||
class ServiceStarter(
|
||||
appContext: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : Worker(appContext, workerParams) {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.Context
|
||||
import com.geeksville.android.BuildUtils
|
||||
import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.model.getInitials
|
||||
|
@ -8,9 +11,18 @@ import okhttp3.internal.toHexString
|
|||
|
||||
/** A simulated interface that is used for testing in the simulator */
|
||||
class MockInterface(private val service: RadioInterfaceService) : Logging, IRadioInterface {
|
||||
companion object : Logging {
|
||||
companion object : Logging, InterfaceFactory('m') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = MockInterface(service)
|
||||
|
||||
const val interfaceName = "m"
|
||||
override fun addressValid(context: Context, rest: String): Boolean =
|
||||
BuildUtils.isEmulator || ((context.applicationContext as GeeksvilleApplication).isInTestLab)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
}
|
||||
|
||||
private var messageCount = 50
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.android.Logging
|
||||
|
||||
class NopInterface : IRadioInterface {
|
||||
companion object : Logging, InterfaceFactory('n') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = NopInterface()
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSendToRadio(p: ByteArray) {
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ open class RadioNotConnectedException(message: String = "Not connected to radio"
|
|||
BLEException(message)
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handles the bluetooth link with a mesh radio device. Does not cache any device state,
|
||||
* just does bluetooth comms etc...
|
||||
|
@ -50,9 +49,11 @@ class RadioInterfaceService : Service(), Logging {
|
|||
*/
|
||||
const val RADIO_CONNECTED_ACTION = "$prefix.CONNECT_CHANGED"
|
||||
|
||||
const val DEVADDR_KEY_OLD = "devAddr"
|
||||
const val DEVADDR_KEY = "devAddr2" // the new name for devaddr
|
||||
|
||||
/// We keep this var alive so that the following factory objects get created and not stripped during the android build
|
||||
private val factories = arrayOf<InterfaceFactory>(BluetoothInterface, SerialInterface, TCPInterface, MockInterface, NopInterface)
|
||||
|
||||
/// This is public only so that SimRadio can bootstrap our message flow
|
||||
fun broadcastReceivedFromRadio(context: Context, payload: ByteArray) {
|
||||
val intent = Intent(RECEIVE_FROMRADIO_ACTION)
|
||||
|
@ -77,27 +78,13 @@ class RadioInterfaceService : Service(), Logging {
|
|||
val prefs = getPrefs(context)
|
||||
var address = prefs.getString(DEVADDR_KEY, null)
|
||||
|
||||
if (address == null) { /// Check for the old preferences name we used to use
|
||||
var rest = prefs.getString(DEVADDR_KEY_OLD, null)
|
||||
if(rest == "null")
|
||||
rest = null
|
||||
|
||||
if (rest != null)
|
||||
address = BluetoothInterface.toInterfaceName(rest) // Add the bluetooth prefix
|
||||
}
|
||||
|
||||
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the user
|
||||
if(address == null && isMockInterfaceAvailable(context))
|
||||
address = MockInterface.interfaceName
|
||||
if (address == null && MockInterface.addressValid(context, ""))
|
||||
address = MockInterface.prefix.toString()
|
||||
|
||||
return address
|
||||
}
|
||||
|
||||
/** return true if we should show the mock interface on this device
|
||||
* (ie are we in an emulator or in testlab
|
||||
*/
|
||||
fun isMockInterfaceAvailable(context: Context) = isEmulator || ((context.applicationContext as GeeksvilleApplication).isInTestLab)
|
||||
|
||||
/** Like getDeviceAddress, but filtered to return only devices we are currently bonded with
|
||||
*
|
||||
* at
|
||||
|
@ -114,13 +101,7 @@ class RadioInterfaceService : Service(), Logging {
|
|||
if (address != null) {
|
||||
val c = address[0]
|
||||
val rest = address.substring(1)
|
||||
val isValid = when (c) {
|
||||
'x' -> BluetoothInterface.addressValid(context, rest)
|
||||
's' -> SerialInterface.addressValid(context, rest)
|
||||
'n' -> true
|
||||
'm' -> true
|
||||
else -> TODO("Unexpected interface type $c")
|
||||
}
|
||||
val isValid = InterfaceFactory.getFactory(c)?.addressValid(context, rest) ?: false
|
||||
if (!isValid)
|
||||
return null
|
||||
}
|
||||
|
@ -141,8 +122,7 @@ class RadioInterfaceService : Service(), Logging {
|
|||
*/
|
||||
var serviceScope = CoroutineScope(Dispatchers.IO + Job())
|
||||
|
||||
private val nopIf = NopInterface()
|
||||
private var radioIf: IRadioInterface = nopIf
|
||||
private var radioIf: IRadioInterface = NopInterface()
|
||||
|
||||
/** true if we have started our interface
|
||||
*
|
||||
|
@ -221,13 +201,13 @@ class RadioInterfaceService : Service(), Logging {
|
|||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return binder;
|
||||
return binder
|
||||
}
|
||||
|
||||
|
||||
/** Start our configured interface (if it isn't already running) */
|
||||
private fun startInterface() {
|
||||
if (radioIf != nopIf)
|
||||
if (radioIf !is NopInterface)
|
||||
warn("Can't start interface - $radioIf is already running")
|
||||
else {
|
||||
val address = getBondedDeviceAddress(this)
|
||||
|
@ -244,26 +224,17 @@ class RadioInterfaceService : Service(), Logging {
|
|||
|
||||
val c = address[0]
|
||||
val rest = address.substring(1)
|
||||
radioIf = when (c) {
|
||||
'x' -> BluetoothInterface(this, rest)
|
||||
's' -> SerialInterface(this, rest)
|
||||
'm' -> MockInterface(this)
|
||||
'n' -> nopIf
|
||||
else -> {
|
||||
errormsg("Unexpected radio interface type")
|
||||
nopIf
|
||||
}
|
||||
}
|
||||
radioIf =
|
||||
InterfaceFactory.getFactory(c)?.createInterface(this, rest) ?: NopInterface()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun stopInterface() {
|
||||
val r = radioIf
|
||||
info("stopping interface $r")
|
||||
isStarted = false
|
||||
radioIf = nopIf
|
||||
radioIf = NopInterface()
|
||||
r.close()
|
||||
|
||||
// cancel any old jobs and get ready for the new ones
|
||||
|
@ -276,7 +247,7 @@ class RadioInterfaceService : Service(), Logging {
|
|||
receivedPacketsLog.close()
|
||||
|
||||
// Don't broadcast disconnects if we were just using the nop device
|
||||
if (r != nopIf)
|
||||
if (r !is NopInterface)
|
||||
onDisconnect(isPermanent = true) // Tell any clients we are now offline
|
||||
}
|
||||
|
||||
|
@ -307,8 +278,6 @@ class RadioInterfaceService : Service(), Logging {
|
|||
debug("Setting bonded device to ${address.anonymize}")
|
||||
|
||||
getPrefs(this).edit(commit = true) {
|
||||
this.remove(DEVADDR_KEY_OLD) // remove any old version of the key
|
||||
|
||||
if (address == null)
|
||||
this.remove(DEVADDR_KEY)
|
||||
else
|
||||
|
|
|
@ -229,7 +229,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
|||
if (reliable != null)
|
||||
if (!characteristic.value.contentEquals(reliable)) {
|
||||
errormsg("A reliable write failed!")
|
||||
gatt.abortReliableWrite();
|
||||
gatt.abortReliableWrite()
|
||||
completeWork(
|
||||
STATUS_RELIABLE_WRITE_FAILED,
|
||||
characteristic
|
||||
|
@ -325,7 +325,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
|||
if (newWork.timeoutMillis != 0L) {
|
||||
|
||||
activeTimeout = serviceScope.launch {
|
||||
debug("Starting failsafe timer ${newWork.timeoutMillis}")
|
||||
// debug("Starting failsafe timer ${newWork.timeoutMillis}")
|
||||
delay(newWork.timeoutMillis)
|
||||
errormsg("Failsafe BLE timer expired!")
|
||||
completeWork(
|
||||
|
@ -415,7 +415,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
|||
if (work == null)
|
||||
warn("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
|
||||
else {
|
||||
debug("work ${work.tag} is completed, resuming status=$status, res=$res")
|
||||
// debug("work ${work.tag} is completed, resuming status=$status, res=$res")
|
||||
if (status != 0)
|
||||
work.completion.resumeWithException(
|
||||
BLEStatusException(
|
||||
|
@ -773,7 +773,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
|||
|
||||
closeGatt()
|
||||
|
||||
failAllWork(BLEException("Connection closing"))
|
||||
failAllWork(BLEConnectionClosing())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,19 +16,27 @@ import com.hoho.android.usbserial.driver.UsbSerialProber
|
|||
import com.hoho.android.usbserial.util.SerialInputOutputManager
|
||||
|
||||
|
||||
class SerialInterface(private val service: RadioInterfaceService, val address: String) : Logging,
|
||||
IRadioInterface, SerialInputOutputManager.Listener {
|
||||
companion object : Logging {
|
||||
private const val START1 = 0x94.toByte()
|
||||
private const val START2 = 0xc3.toByte()
|
||||
private const val MAX_TO_FROM_RADIO_SIZE = 512
|
||||
/**
|
||||
* An interface that assumes we are talking to a meshtastic device via USB serial
|
||||
*/
|
||||
class SerialInterface(service: RadioInterfaceService, private val address: String) :
|
||||
StreamInterface(service), Logging, SerialInputOutputManager.Listener {
|
||||
companion object : Logging, InterfaceFactory('s') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = SerialInterface(service, rest)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
|
||||
/**
|
||||
* according to https://stackoverflow.com/questions/12388914/usb-device-access-pop-up-suppression/15151075#15151075
|
||||
* we should never ask for USB permissions ourselves, instead we should rely on the external dialog printed by the system. If
|
||||
* we do that the system will remember we have accesss
|
||||
*/
|
||||
val assumePermission = true
|
||||
const val assumePermission = true
|
||||
|
||||
fun toInterfaceName(deviceName: String) = "s$deviceName"
|
||||
|
||||
|
@ -41,14 +49,14 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
|||
return drivers
|
||||
}
|
||||
|
||||
fun addressValid(context: Context, rest: String): Boolean {
|
||||
override fun addressValid(context: Context, rest: String): Boolean {
|
||||
findSerial(context, rest)?.let { d ->
|
||||
return assumePermission || context.usbManager.hasPermission(d.device)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findSerial(context: Context, rest: String): UsbSerialDriver? {
|
||||
private fun findSerial(context: Context, rest: String): UsbSerialDriver? {
|
||||
val drivers = findDrivers(context)
|
||||
|
||||
return if (drivers.isEmpty())
|
||||
|
@ -61,7 +69,7 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
|||
private var uart: UsbSerialDriver? = null
|
||||
private var ioManager: SerialInputOutputManager? = null
|
||||
|
||||
var usbReceiver = object : BroadcastReceiver() {
|
||||
private var usbReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
|
||||
if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) {
|
||||
|
@ -85,16 +93,6 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
|||
}
|
||||
}
|
||||
|
||||
private val debugLineBuf = kotlin.text.StringBuilder()
|
||||
|
||||
/** The index of the next byte we are hoping to receive */
|
||||
private var ptr = 0
|
||||
|
||||
/** The two halves of our length */
|
||||
private var msb = 0
|
||||
private var lsb = 0
|
||||
private var packetLen = 0
|
||||
|
||||
init {
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||
|
@ -105,16 +103,15 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
|||
}
|
||||
|
||||
override fun close() {
|
||||
debug("Closing serial port for good")
|
||||
service.unregisterReceiver(usbReceiver)
|
||||
onDeviceDisconnect(true)
|
||||
super.close()
|
||||
}
|
||||
|
||||
/** Tell MeshService our device has gone away, but wait for it to come back
|
||||
*
|
||||
* @param waitForStopped if true we should wait for the manager to finish - must be false if called from inside the manager callbacks
|
||||
* */
|
||||
fun onDeviceDisconnect(waitForStopped: Boolean) {
|
||||
override fun onDeviceDisconnect(waitForStopped: Boolean) {
|
||||
ignoreException {
|
||||
ioManager?.let {
|
||||
debug("USB device disconnected, but it might come back")
|
||||
|
@ -143,10 +140,10 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
|||
}
|
||||
}
|
||||
|
||||
service.onDisconnect(isPermanent = true) // if USB device disconnects it is definitely permantently gone, not sleeping)
|
||||
super.onDeviceDisconnect(waitForStopped)
|
||||
}
|
||||
|
||||
private fun connect() {
|
||||
override fun connect() {
|
||||
val manager = service.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
val device = findSerial(service, address)
|
||||
|
||||
|
@ -175,101 +172,20 @@ class SerialInterface(private val service: RadioInterfaceService, val address: S
|
|||
thread.name = "serial reader"
|
||||
thread.start() // No need to keep reference to thread around, we quit by asking the ioManager to quit
|
||||
|
||||
// Before telling mesh service, send a few START1s to wake a sleeping device
|
||||
val wakeBytes = byteArrayOf(START1, START1, START1, START1)
|
||||
io.writeAsync(wakeBytes)
|
||||
|
||||
// Now tell clients they can (finally use the api)
|
||||
service.onConnect()
|
||||
super.connect()
|
||||
}
|
||||
} else {
|
||||
errormsg("Can't find device")
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSendToRadio(p: ByteArray) {
|
||||
// This method is called from a continuation and it might show up late, so check for uart being null
|
||||
|
||||
val header = ByteArray(4)
|
||||
header[0] = START1
|
||||
header[1] = START2
|
||||
header[2] = (p.size shr 8).toByte()
|
||||
header[3] = (p.size and 0xff).toByte()
|
||||
override fun sendBytes(p: ByteArray) {
|
||||
ioManager?.apply {
|
||||
writeAsync(header)
|
||||
writeAsync(p)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Print device serial debug output somewhere */
|
||||
private fun debugOut(b: Byte) {
|
||||
when (val c = b.toChar()) {
|
||||
'\r' -> {
|
||||
} // ignore
|
||||
'\n' -> {
|
||||
debug("DeviceLog: $debugLineBuf")
|
||||
debugLineBuf.clear()
|
||||
}
|
||||
else ->
|
||||
debugLineBuf.append(c)
|
||||
}
|
||||
}
|
||||
|
||||
private val rxPacket = ByteArray(MAX_TO_FROM_RADIO_SIZE)
|
||||
|
||||
private fun readChar(c: Byte) {
|
||||
// Assume we will be advancing our pointer
|
||||
var nextPtr = ptr + 1
|
||||
|
||||
fun lostSync() {
|
||||
errormsg("Lost protocol sync")
|
||||
nextPtr = 0
|
||||
}
|
||||
|
||||
/// Deliver our current packet and restart our reader
|
||||
fun deliverPacket() {
|
||||
val buf = rxPacket.copyOf(packetLen)
|
||||
service.handleFromRadio(buf)
|
||||
|
||||
nextPtr = 0 // Start parsing the next packet
|
||||
}
|
||||
|
||||
when (ptr) {
|
||||
0 -> // looking for START1
|
||||
if (c != START1) {
|
||||
debugOut(c)
|
||||
nextPtr = 0 // Restart from scratch
|
||||
}
|
||||
1 -> // Looking for START2
|
||||
if (c != START2)
|
||||
lostSync() // Restart from scratch
|
||||
2 -> // Looking for MSB of our 16 bit length
|
||||
msb = c.toInt() and 0xff
|
||||
3 -> { // Looking for LSB of our 16 bit length
|
||||
lsb = c.toInt() and 0xff
|
||||
|
||||
// We've read our header, do one big read for the packet itself
|
||||
packetLen = (msb shl 8) or lsb
|
||||
if (packetLen > MAX_TO_FROM_RADIO_SIZE)
|
||||
lostSync() // If packet len is too long, the bytes must have been corrupted, start looking for START1 again
|
||||
else if (packetLen == 0)
|
||||
deliverPacket() // zero length packets are valid and should be delivered immediately (because there won't be a next byte of payload)
|
||||
}
|
||||
else -> {
|
||||
// We are looking at the packet bytes now
|
||||
rxPacket[ptr - 4] = c
|
||||
|
||||
// Note: we have to check if ptr +1 is equal to packet length (for example, for a 1 byte packetlen, this code will be run with ptr of4
|
||||
if (ptr - 4 + 1 == packetLen) {
|
||||
deliverPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
ptr = nextPtr
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when [SerialInputOutputManager.run] aborts due to an error.
|
||||
*/
|
||||
|
|
|
@ -17,7 +17,7 @@ import java.util.zip.CRC32
|
|||
/**
|
||||
* Some misformatted ESP32s have problems
|
||||
*/
|
||||
class DeviceRejectedException() : BLEException("Device rejected filesize")
|
||||
class DeviceRejectedException : BLEException("Device rejected filesize")
|
||||
|
||||
/**
|
||||
* Move this somewhere as a generic network byte order function
|
||||
|
@ -211,18 +211,18 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
|||
* @param isAppload if false, we don't report failure indications (because we consider spiffs non critical for now). But do report to analytics
|
||||
*/
|
||||
fun sendProgress(context: Context, p: Int, isAppload: Boolean) {
|
||||
if(!isAppload && p < 0)
|
||||
reportError("Error while writing spiffs $progress") // See if this is happening in the wild
|
||||
if (!isAppload && p < 0)
|
||||
errormsg("Error while writing spiffs $p") // treat errors writing spiffs as non fatal for now (user partition probably missized and most people don't need it)
|
||||
else
|
||||
if (progress != p) {
|
||||
progress = p
|
||||
|
||||
if(progress != p && (p >= 0 || isAppload)) {
|
||||
progress = p
|
||||
|
||||
val intent = Intent(ACTION_UPDATE_PROGRESS).putExtra(
|
||||
EXTRA_PROGRESS,
|
||||
p
|
||||
)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
val intent = Intent(ACTION_UPDATE_PROGRESS).putExtra(
|
||||
EXTRA_PROGRESS,
|
||||
p
|
||||
)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/** Return true if we thing the firmwarte shoulde be updated
|
||||
|
@ -293,14 +293,12 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
|||
// we must attempt spiffs first, because if we update the appload the device will reboot afterwards
|
||||
try {
|
||||
assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS) }
|
||||
}
|
||||
catch(_: BLECharacteristicNotFoundException) {
|
||||
} catch (_: BLECharacteristicNotFoundException) {
|
||||
// If we can't update spiffs (because not supported by target), do not fail
|
||||
errormsg("Ignoring failure to update spiffs on old appload")
|
||||
}
|
||||
catch(_: DeviceRejectedException) {
|
||||
// the spi filesystem of this device is malformatted
|
||||
reportError("Device rejected invalid spiffs partition")
|
||||
} catch (_: DeviceRejectedException) {
|
||||
// the spi filesystem of this device is malformatted, fail silently because most users don't need the web server
|
||||
errormsg("Device rejected invalid spiffs partition")
|
||||
}
|
||||
|
||||
assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) }
|
||||
|
@ -315,9 +313,14 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
|||
* A public function so that if you have your own SafeBluetooth connection already open
|
||||
* you can use it for the software update.
|
||||
*/
|
||||
private fun doUpdate(context: Context, sync: SafeBluetooth, assetName: String, flashRegion: Int = FLASH_REGION_APPLOAD) {
|
||||
private fun doUpdate(
|
||||
context: Context,
|
||||
sync: SafeBluetooth,
|
||||
assetName: String,
|
||||
flashRegion: Int = FLASH_REGION_APPLOAD
|
||||
) {
|
||||
val isAppload = flashRegion == FLASH_REGION_APPLOAD
|
||||
|
||||
|
||||
try {
|
||||
val g = sync.gatt!!
|
||||
val service = g.services.find { it.uuid == SW_UPDATE_UUID }
|
||||
|
@ -332,7 +335,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
|||
|
||||
info("Starting firmware update for $assetName, flash region $flashRegion")
|
||||
|
||||
sendProgress(context,0, isAppload)
|
||||
sendProgress(context, 0, isAppload)
|
||||
val totalSizeDesc = getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)
|
||||
val dataDesc = getCharacteristic(SW_UPDATE_DATA_CHARACTER)
|
||||
val crc32Desc = getCharacteristic(SW_UPDATE_CRC32_CHARACTER)
|
||||
|
@ -347,10 +350,9 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
|||
updateRegionDesc,
|
||||
toNetworkByteArray(flashRegion, BluetoothGattCharacteristic.FORMAT_UINT8)
|
||||
)
|
||||
}
|
||||
catch(ex: BLECharacteristicNotFoundException) {
|
||||
} catch (ex: BLECharacteristicNotFoundException) {
|
||||
errormsg("Can't set flash programming region (old appload?")
|
||||
if(flashRegion != FLASH_REGION_APPLOAD) {
|
||||
if (flashRegion != FLASH_REGION_APPLOAD) {
|
||||
throw ex
|
||||
}
|
||||
warn("Ignoring setting appload flashRegion")
|
||||
|
@ -374,13 +376,21 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
|||
throw DeviceRejectedException()
|
||||
|
||||
// Send all the blocks
|
||||
var oldProgress = -1 // used to limit # of log spam
|
||||
while (firmwareNumSent < firmwareSize) {
|
||||
// If we are doing the spiffs partition, we limit progress to a max of 50%, so that the user doesn't think we are done
|
||||
// yet
|
||||
val maxProgress = if(flashRegion != FLASH_REGION_APPLOAD)
|
||||
val maxProgress = if (flashRegion != FLASH_REGION_APPLOAD)
|
||||
50 else 100
|
||||
sendProgress(context, firmwareNumSent * maxProgress / firmwareSize, isAppload)
|
||||
debug("sending block ${progress}%")
|
||||
sendProgress(
|
||||
context,
|
||||
firmwareNumSent * maxProgress / firmwareSize,
|
||||
isAppload
|
||||
)
|
||||
if (progress != oldProgress) {
|
||||
debug("sending block ${progress}%")
|
||||
oldProgress = progress
|
||||
}
|
||||
var blockSize = 512 - 3 // Max size MTU excluding framing
|
||||
|
||||
if (blockSize > firmwareStream.available())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.util.Exceptions
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.InetAddress
|
||||
import java.net.Socket
|
||||
import java.net.SocketTimeoutException
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
class TCPInterface(service: RadioInterfaceService, private val address: String) :
|
||||
StreamInterface(service) {
|
||||
|
||||
companion object : Logging, InterfaceFactory('t') {
|
||||
override fun createInterface(
|
||||
service: RadioInterfaceService,
|
||||
rest: String
|
||||
): IRadioInterface = TCPInterface(service, rest)
|
||||
|
||||
init {
|
||||
registerFactory()
|
||||
}
|
||||
}
|
||||
|
||||
var socket: Socket? = null
|
||||
lateinit var outStream: OutputStream
|
||||
lateinit var inStream: InputStream
|
||||
|
||||
init {
|
||||
connect()
|
||||
}
|
||||
|
||||
override fun sendBytes(p: ByteArray) {
|
||||
outStream.write(p)
|
||||
}
|
||||
|
||||
override fun flushBytes() {
|
||||
outStream.flush()
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnect(waitForStopped: Boolean) {
|
||||
val s = socket
|
||||
if (s != null) {
|
||||
debug("Closing TCP socket")
|
||||
socket = null
|
||||
outStream.close()
|
||||
inStream.close()
|
||||
s.close()
|
||||
}
|
||||
super.onDeviceDisconnect(waitForStopped)
|
||||
}
|
||||
|
||||
override fun connect() {
|
||||
//here you must put your computer's IP address.
|
||||
//here you must put your computer's IP address.
|
||||
|
||||
// No need to keep a reference to this thread - it will exit when we close inStream
|
||||
thread(start = true, isDaemon = true, name = "TCP reader") {
|
||||
try {
|
||||
val a = InetAddress.getByName(address)
|
||||
debug("TCP connecting to $address")
|
||||
|
||||
//create a socket to make the connection with the server
|
||||
val port = 4403
|
||||
val s = Socket(a, port)
|
||||
s.tcpNoDelay = true
|
||||
s.soTimeout = 500
|
||||
socket = s
|
||||
outStream = BufferedOutputStream(s.getOutputStream())
|
||||
inStream = s.getInputStream()
|
||||
|
||||
// Note: we call the super method FROM OUR NEW THREAD
|
||||
super.connect()
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
val c = inStream.read()
|
||||
if (c == -1) {
|
||||
warn("Got EOF on TCP stream")
|
||||
onDeviceDisconnect(false)
|
||||
break
|
||||
} else
|
||||
readChar(c.toByte())
|
||||
} catch (ex: SocketTimeoutException) {
|
||||
// Ignore and start another read
|
||||
}
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
errormsg("IOException in TCP reader: $ex") // FIXME, show message to user
|
||||
onDeviceDisconnect(false)
|
||||
} catch (ex: Throwable) {
|
||||
Exceptions.report(ex, "Exception in TCP reader")
|
||||
onDeviceDisconnect(false)
|
||||
}
|
||||
debug("Exiting TCP reader")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
|
@ -18,7 +19,6 @@ import com.geeksville.android.Logging
|
|||
import com.geeksville.android.hideKeyboard
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.databinding.ChannelFragmentBinding
|
||||
import com.geeksville.mesh.model.Channel
|
||||
|
@ -50,6 +50,7 @@ fun ImageView.setOpaque() {
|
|||
class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||
|
||||
private var _binding: ChannelFragmentBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
|
@ -81,6 +82,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
val channels = model.channels.value
|
||||
val channel = channels?.primaryChannel
|
||||
|
||||
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
||||
|
||||
// Only let buttons work if we are connected to the radio
|
||||
binding.shareButton.isEnabled = connected
|
||||
binding.resetButton.isEnabled = connected && Channel.default != channel
|
||||
|
||||
binding.editableCheckbox.isChecked = false // start locked
|
||||
if (channel != null) {
|
||||
binding.qrView.visibility = View.VISIBLE
|
||||
|
@ -89,7 +96,6 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
|
||||
// For now, we only let the user edit/save channels while the radio is awake - because the service
|
||||
// doesn't cache radioconfig writes.
|
||||
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
||||
binding.editableCheckbox.isEnabled = connected
|
||||
|
||||
binding.qrView.setImageBitmap(channels.getChannelQR())
|
||||
|
@ -138,8 +144,38 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
type = "text/plain"
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
requireActivity().startActivity(shareIntent)
|
||||
try {
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
requireActivity().startActivity(shareIntent)
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Snackbar.make(
|
||||
binding.shareButton,
|
||||
R.string.no_app_found,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send new channel settings to the device
|
||||
private fun installSettings(newChannel: ChannelProtos.ChannelSettings) {
|
||||
val newSet =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
|
||||
// Try to change the radio, if it fails, tell the user why and throw away their redits
|
||||
try {
|
||||
model.setChannels(newSet)
|
||||
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("ignoring channel problem", ex)
|
||||
|
||||
setGUIfromModel() // Throw away user edits
|
||||
|
||||
// Tell the user to try again
|
||||
Snackbar.make(
|
||||
binding.editableCheckbox,
|
||||
R.string.radio_sleeping,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,13 +186,34 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
requireActivity().hideKeyboard()
|
||||
}
|
||||
|
||||
binding.resetButton.setOnClickListener { _ ->
|
||||
// User just locked it, we should warn and then apply changes to radio
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.reset_to_defaults)
|
||||
.setMessage(R.string.are_you_shure_change_default)
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
setGUIfromModel() // throw away any edits
|
||||
}
|
||||
.setPositiveButton(R.string.apply) { _, _ ->
|
||||
debug("Switching back to default channel")
|
||||
installSettings(Channel.default.settings)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
|
||||
binding.editableCheckbox.setOnClickListener { _ ->
|
||||
|
||||
/// We use this to determine if the user tried to install a custom name
|
||||
var originalName = ""
|
||||
|
||||
val checked = binding.editableCheckbox.isChecked
|
||||
if (checked) {
|
||||
// User just unlocked for editing - remove the # goo around the channel name
|
||||
model.channels.value?.primaryChannel?.let { ch ->
|
||||
binding.channelNameEdit.setText(ch.name)
|
||||
// Note: We are careful to show the emptystring here if the user was on a default channel, so the user knows they should it for any changes
|
||||
originalName = ch.settings.name
|
||||
binding.channelNameEdit.setText(originalName)
|
||||
}
|
||||
} else {
|
||||
// User just locked it, we should warn and then apply changes to radio
|
||||
|
@ -170,49 +227,33 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
// Generate a new channel with only the changes the user can change in the GUI
|
||||
model.channels.value?.primaryChannel?.let { oldPrimary ->
|
||||
var newSettings = oldPrimary.settings.toBuilder()
|
||||
newSettings.name = binding.channelNameEdit.text.toString().trim()
|
||||
val newName = binding.channelNameEdit.text.toString().trim()
|
||||
|
||||
// Generate a new AES256 key unleess the user is trying to go back to stock
|
||||
if (!newSettings.name.equals(
|
||||
Channel.defaultChannel.name,
|
||||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
// Find the new modem config
|
||||
val selectedChannelOptionString =
|
||||
binding.filledExposedDropdown.editableText.toString()
|
||||
var modemConfig = getModemConfig(selectedChannelOptionString)
|
||||
if (modemConfig == ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED) // Huh? didn't find it - keep same
|
||||
modemConfig = oldPrimary.settings.modemConfig
|
||||
|
||||
// Generate a new AES256 key if the user changes channel name or the name is non-default and the settings changed
|
||||
if (newName != originalName || (newName.isNotEmpty() && modemConfig != oldPrimary.settings.modemConfig)) {
|
||||
// Install a new customized channel
|
||||
debug("ASSIGNING NEW AES256 KEY")
|
||||
val random = SecureRandom()
|
||||
val bytes = ByteArray(32)
|
||||
random.nextBytes(bytes)
|
||||
newSettings.name = newName
|
||||
newSettings.psk = ByteString.copyFrom(bytes)
|
||||
} else {
|
||||
debug("Switching back to default channel")
|
||||
newSettings = Channel.defaultChannel.settings.toBuilder()
|
||||
newSettings = Channel.default.settings.toBuilder()
|
||||
}
|
||||
|
||||
val selectedChannelOptionString =
|
||||
binding.filledExposedDropdown.editableText.toString()
|
||||
val modemConfig = getModemConfig(selectedChannelOptionString)
|
||||
// No matter what apply the speed selection from the user
|
||||
newSettings.modemConfig = modemConfig
|
||||
|
||||
if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
|
||||
newSettings.modemConfig = modemConfig
|
||||
|
||||
val newChannel = newSettings.build()
|
||||
val newSet = ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
|
||||
// Try to change the radio, if it fails, tell the user why and throw away their redits
|
||||
try {
|
||||
model.setChannels(newSet)
|
||||
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("ignoring channel problem", ex)
|
||||
|
||||
setGUIfromModel() // Throw away user edits
|
||||
|
||||
// Tell the user to try again
|
||||
Snackbar.make(
|
||||
binding.editableCheckbox,
|
||||
R.string.radio_sleeping,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
installSettings(newSettings.build())
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.geeksville.mesh.model.UIViewModel
|
|||
class DebugFragment : Fragment() {
|
||||
|
||||
private var _binding: DebugFragmentBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
|
@ -44,11 +45,11 @@ class DebugFragment : Fragment() {
|
|||
model.deleteAllPacket()
|
||||
}
|
||||
|
||||
binding.closeButton.setOnClickListener{
|
||||
parentFragmentManager.popBackStack();
|
||||
binding.closeButton.setOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
model.allPackets.observe(viewLifecycleOwner, Observer {
|
||||
packets -> packets?.let { adapter.setPackets(it) }
|
||||
model.allPackets.observe(viewLifecycleOwner, Observer { packets ->
|
||||
packets?.let { adapter.setPackets(it) }
|
||||
})
|
||||
}
|
||||
}
|
|
@ -131,8 +131,10 @@ fun positionToMeter(a: MeshProtos.Position, b: MeshProtos.Position): Double {
|
|||
a.latitudeI * 1e-7,
|
||||
a.longitudeI * 1e-7,
|
||||
b.latitudeI * 1e-7,
|
||||
b.longitudeI * 1e-7)
|
||||
b.longitudeI * 1e-7
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees/mins/secs to a single double
|
||||
*
|
||||
|
@ -186,7 +188,7 @@ fun bearing(
|
|||
val y = sin(deltaLonRad) * cos(lat2Rad)
|
||||
val x =
|
||||
cos(lat1Rad) * sin(lat2Rad) - (sin(lat1Rad) * cos(lat2Rad)
|
||||
* Math.cos(deltaLonRad))
|
||||
* Math.cos(deltaLonRad))
|
||||
return radToBearing(Math.atan2(y, x))
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
||||
import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
||||
|
@ -25,7 +24,6 @@ import com.geeksville.mesh.model.UIViewModel
|
|||
import com.geeksville.mesh.service.MeshService
|
||||
import com.google.android.material.chip.Chip
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.util.*
|
||||
|
||||
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
|
||||
|
@ -54,9 +52,9 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
|||
private val timeFormat: DateFormat =
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||
|
||||
private fun getShortDateTime(time : Date): String {
|
||||
private fun getShortDateTime(time: Date): String {
|
||||
// return time if within 24 hours, otherwise date/time
|
||||
val one_day = 60*60*24*100L
|
||||
val one_day = 60 * 60 * 24 * 100L
|
||||
if (System.currentTimeMillis() - time.time > one_day) {
|
||||
return dateTimeFormat.format(time)
|
||||
} else return timeFormat.format(time)
|
||||
|
@ -152,11 +150,25 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
|||
if (isMe) {
|
||||
marginParams.leftMargin = messageOffset
|
||||
marginParams.rightMargin = 0
|
||||
context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg)) }
|
||||
context?.let {
|
||||
holder.card.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
it,
|
||||
R.color.colorMyMsg
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
marginParams.rightMargin = messageOffset
|
||||
marginParams.leftMargin = 0
|
||||
context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg)) }
|
||||
context?.let {
|
||||
holder.card.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
it,
|
||||
R.color.colorMsg
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Hide the username chip for my messages
|
||||
if (isMe) {
|
||||
|
@ -240,20 +252,26 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
|||
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
||||
fun updateTextEnabled() {
|
||||
binding.textInputLayout.isEnabled =
|
||||
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null && model.radioConfig.value != null
|
||||
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED
|
||||
|
||||
// Just being connected is enough to allow sending texts I think
|
||||
// && model.nodeDB.myId.value != null && model.radioConfig.value != null
|
||||
}
|
||||
|
||||
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled() })
|
||||
updateTextEnabled()
|
||||
})
|
||||
|
||||
model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
|
||||
/* model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled() })
|
||||
updateTextEnabled()
|
||||
})
|
||||
|
||||
model.radioConfig.observe(viewLifecycleOwner, Observer { _ ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
updateTextEnabled() })
|
||||
updateTextEnabled()
|
||||
}) */
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,49 +1,50 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import java.text.DateFormat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import java.util.*
|
||||
|
||||
class PacketListAdapter internal constructor(
|
||||
context: Context
|
||||
) : RecyclerView.Adapter<PacketListAdapter.PacketViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var packets = emptyList<Packet>()
|
||||
|
||||
private val timeFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
|
||||
inner class PacketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val packetTypeView: TextView = itemView.findViewById(R.id.type)
|
||||
val packetDateReceivedView: TextView = itemView.findViewById(R.id.dateReceived)
|
||||
val packetRawMessage : TextView = itemView.findViewById(R.id.rawMessage)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PacketViewHolder {
|
||||
val itemView = inflater.inflate(R.layout.adapter_packet_layout, parent, false)
|
||||
return PacketViewHolder(itemView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PacketViewHolder, position: Int) {
|
||||
val current = packets[position]
|
||||
holder.packetTypeView.text = current.message_type
|
||||
holder.packetRawMessage.text = current.raw_message
|
||||
val date = Date(current.received_date)
|
||||
holder.packetDateReceivedView.text = timeFormat.format(date)
|
||||
}
|
||||
|
||||
internal fun setPackets(packets: List<Packet>) {
|
||||
this.packets = packets
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemCount() = packets.size
|
||||
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class PacketListAdapter internal constructor(
|
||||
context: Context
|
||||
) : RecyclerView.Adapter<PacketListAdapter.PacketViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var packets = emptyList<Packet>()
|
||||
|
||||
private val timeFormat: DateFormat =
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
|
||||
inner class PacketViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val packetTypeView: TextView = itemView.findViewById(R.id.type)
|
||||
val packetDateReceivedView: TextView = itemView.findViewById(R.id.dateReceived)
|
||||
val packetRawMessage: TextView = itemView.findViewById(R.id.rawMessage)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PacketViewHolder {
|
||||
val itemView = inflater.inflate(R.layout.adapter_packet_layout, parent, false)
|
||||
return PacketViewHolder(itemView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PacketViewHolder, position: Int) {
|
||||
val current = packets[position]
|
||||
holder.packetTypeView.text = current.message_type
|
||||
holder.packetRawMessage.text = current.raw_message
|
||||
val date = Date(current.received_date)
|
||||
holder.packetDateReceivedView.text = timeFormat.format(date)
|
||||
}
|
||||
|
||||
internal fun setPackets(packets: List<Packet>) {
|
||||
this.packets = packets
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemCount() = packets.size
|
||||
|
||||
}
|
|
@ -38,10 +38,7 @@ import com.geeksville.mesh.android.bluetoothManager
|
|||
import com.geeksville.mesh.android.usbManager
|
||||
import com.geeksville.mesh.databinding.SettingsFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.BluetoothInterface
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.service.RadioInterfaceService
|
||||
import com.geeksville.mesh.service.SerialInterface
|
||||
import com.geeksville.mesh.service.*
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted
|
||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressSuccess
|
||||
|
@ -59,7 +56,7 @@ import kotlinx.coroutines.Job
|
|||
import java.util.regex.Pattern
|
||||
|
||||
|
||||
object SLogging : Logging {}
|
||||
object SLogging : Logging
|
||||
|
||||
/// Change to a new macaddr selection, updating GUI and radio
|
||||
fun changeDeviceSelection(context: MainActivity, newAddr: String?) {
|
||||
|
@ -186,7 +183,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
|||
// if that device later disconnects remove it as a candidate
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
|
||||
if ((result.device.name?.startsWith("Mesh") ?: false)) {
|
||||
if ((result.device.name?.startsWith("Mesh") == true)) {
|
||||
val addr = result.device.address
|
||||
val fullAddr = "x$addr" // full address with the bluetooh prefix
|
||||
// prevent logspam because weill get get lots of redundant scan results
|
||||
|
@ -245,14 +242,12 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
|||
debug("BTScan component active")
|
||||
selectedAddress = RadioInterfaceService.getDeviceAddress(context)
|
||||
|
||||
return if (bluetoothAdapter == null || RadioInterfaceService.isMockInterfaceAvailable(
|
||||
context
|
||||
)
|
||||
) {
|
||||
return if (bluetoothAdapter == null || MockInterface.addressValid(context, "")) {
|
||||
warn("No bluetooth adapter. Running under emulation?")
|
||||
|
||||
val testnodes = listOf(
|
||||
DeviceListEntry("Simulated interface", "m", true),
|
||||
DeviceListEntry("Included simulator", "m", true),
|
||||
DeviceListEntry("Complete simulator", "t10.0.2.2", true),
|
||||
DeviceListEntry(context.getString(R.string.none), "n", true)
|
||||
/* Don't populate fake bluetooth devices, because we don't want testlab inside of google
|
||||
to try and use them.
|
||||
|
@ -494,7 +489,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
}
|
||||
|
||||
/// Set the correct update button configuration based on current progress
|
||||
private fun refreshUpdateButton() {
|
||||
private fun refreshUpdateButton(enable: Boolean) {
|
||||
debug("Reiniting the udpate button")
|
||||
val info = model.myNodeInfo.value
|
||||
val service = model.meshService
|
||||
|
@ -505,7 +500,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
val progress = service.updateStatus
|
||||
|
||||
binding.updateFirmwareButton.isEnabled =
|
||||
binding.updateFirmwareButton.isEnabled = enable &&
|
||||
(progress < 0) // if currently doing an upgrade disable button
|
||||
|
||||
if (progress >= 0) {
|
||||
|
@ -542,7 +537,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
val connected = model.isConnected.value
|
||||
|
||||
val isConnected = connected == MeshService.ConnectionState.CONNECTED
|
||||
binding.nodeSettings.visibility = if(isConnected) View.VISIBLE else View.GONE
|
||||
binding.nodeSettings.visibility = if (isConnected) View.VISIBLE else View.GONE
|
||||
|
||||
if (connected == MeshService.ConnectionState.DISCONNECTED)
|
||||
model.ownerName.value = ""
|
||||
|
@ -552,25 +547,19 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
val spinner = binding.regionSpinner
|
||||
val unsetIndex = regions.indexOf(RadioConfigProtos.RegionCode.Unset.name)
|
||||
spinner.onItemSelectedListener = null
|
||||
if(region != null) {
|
||||
debug("current region is $region")
|
||||
var regionIndex = regions.indexOf(region.name)
|
||||
if(regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
|
||||
regionIndex = unsetIndex
|
||||
|
||||
// We don't want to be notified of our own changes, so turn off listener while making them
|
||||
spinner.setSelection(regionIndex, false)
|
||||
spinner.onItemSelectedListener = regionSpinnerListener
|
||||
spinner.isEnabled = true
|
||||
}
|
||||
else {
|
||||
warn("region is unset!")
|
||||
spinner.setSelection(unsetIndex, false)
|
||||
spinner.isEnabled = false // leave disabled, because we can't get our region
|
||||
}
|
||||
debug("current region is $region")
|
||||
var regionIndex = regions.indexOf(region.name)
|
||||
if (regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
|
||||
regionIndex = unsetIndex
|
||||
|
||||
// We don't want to be notified of our own changes, so turn off listener while making them
|
||||
spinner.setSelection(regionIndex, false)
|
||||
spinner.onItemSelectedListener = regionSpinnerListener
|
||||
spinner.isEnabled = true
|
||||
|
||||
// If actively connected possibly let the user update firmware
|
||||
refreshUpdateButton()
|
||||
refreshUpdateButton(region != RadioConfigProtos.RegionCode.Unset)
|
||||
|
||||
// Update the status string (highest priority messages first)
|
||||
val info = model.myNodeInfo.value
|
||||
|
@ -590,14 +579,14 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener{
|
||||
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>,
|
||||
view: View,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val item = parent.getItemAtPosition(position) as String
|
||||
val item = parent.getItemAtPosition(position) as String?
|
||||
val asProto = item!!.let { RadioConfigProtos.RegionCode.valueOf(it) }
|
||||
exceptionToSnackbar(requireView()) {
|
||||
model.region = asProto
|
||||
|
@ -622,7 +611,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
// init our region spinner
|
||||
val spinner = binding.regionSpinner
|
||||
val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
||||
val regionAdapter =
|
||||
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
||||
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = regionAdapter
|
||||
|
||||
|
@ -632,7 +622,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
|
||||
// Only let user edit their name or set software update while connected to a radio
|
||||
model.isConnected.observe(viewLifecycleOwner, Observer { connectionState ->
|
||||
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
|
||||
updateNodeInfo()
|
||||
})
|
||||
|
||||
|
@ -702,7 +692,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
scanModel.onSelected(requireActivity() as MainActivity, device)
|
||||
|
||||
if (!b.isSelected)
|
||||
binding.scanStatusText.setText(getString(R.string.please_pair))
|
||||
binding.scanStatusText.text = getString(R.string.please_pair)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -765,7 +755,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
// get rid of the warning text once at least one device is paired.
|
||||
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
|
||||
binding.warningNotPaired.visibility =
|
||||
if (hasBonded && !RadioInterfaceService.isMockInterfaceAvailable(requireContext())) View.GONE else View.VISIBLE
|
||||
if (hasBonded && !MockInterface.addressValid(requireContext(), ""))
|
||||
View.GONE
|
||||
else
|
||||
View.VISIBLE
|
||||
}
|
||||
|
||||
/// Setup the GUI to do a classic (pre SDK 26 BLE scan)
|
||||
|
@ -935,7 +928,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
refreshUpdateButton()
|
||||
refreshUpdateButton(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
val name = n.user?.longName ?: n.user?.id ?: "Unknown node"
|
||||
holder.nodeNameView.text = name
|
||||
|
||||
val pos = n.validPosition;
|
||||
val pos = n.validPosition
|
||||
if (pos != null) {
|
||||
val coords =
|
||||
String.format("%.5f %.5f", pos.latitude, pos.longitude).replace(",", ".")
|
||||
|
@ -141,7 +141,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
}
|
||||
renderBattery(n.batteryPctLevel, holder)
|
||||
|
||||
holder.lastTime.text = formatAgo(n.lastSeen);
|
||||
holder.lastTime.text = formatAgo(n.lastHeard)
|
||||
|
||||
if ((n.num == ourNodeInfo?.num) || (n.snr > 100f)) {
|
||||
holder.signalView.visibility = View.INVISIBLE
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 820fa497dfde07e129cad6955bf2f4b2b9cecebc
|
||||
Subproject commit f9c4f875818c9aa6995e6e25803d52557a1779c7
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14.99,4.59V5c0,1.1 -0.9,2 -2,2h-2v2c0,0.55 -0.45,1 -1,1h-2v2h6c0.55,0 1,0.45 1,1v3h1c0.89,0 1.64,0.59 1.9,1.4C19.19,15.98 20,14.08 20,12c0,-3.35 -2.08,-6.23 -5.01,-7.41zM8.99,16v-1l-4.78,-4.78C4.08,10.79 4,11.39 4,12c0,4.07 3.06,7.43 6.99,7.93V18c-1.1,0 -2,-0.9 -2,-2z"
|
||||
android:strokeAlpha="0.3"
|
||||
android:fillAlpha="0.3"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10.99,19.93C7.06,19.43 4,16.07 4,12c0,-0.61 0.08,-1.21 0.21,-1.78L8.99,15v1c0,1.1 0.9,2 2,2v1.93zM17.89,17.4c-0.26,-0.81 -1,-1.4 -1.9,-1.4h-1v-3c0,-0.55 -0.45,-1 -1,-1h-6v-2h2c0.55,0 1,-0.45 1,-1L10.99,7h2c1.1,0 2,-0.9 2,-2v-0.41C17.92,5.77 20,8.65 20,12c0,2.08 -0.81,3.98 -2.11,5.4z"/>
|
||||
</vector>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -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" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
@ -91,30 +92,46 @@
|
|||
<AutoCompleteTextView
|
||||
android:id="@+id/filled_exposed_dropdown"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/set_channel_options" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/resetButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/reset"
|
||||
app:icon="@drawable/ic_twotone_public_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/editableCheckbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="96dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:button="@drawable/sl_lock_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/shareButton"
|
||||
app:layout_constraintStart_toStartOf="parent"></CheckBox>
|
||||
app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
|
||||
app:layout_constraintStart_toEndOf="@+id/resetButton"></CheckBox>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/shareButton"
|
||||
style="@android:style/Widget.Material.ImageButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="96dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:contentDescription="@string/share"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/editableCheckbox"
|
||||
app:srcCompat="@drawable/ic_twotone_share_24" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/bottomButtonsGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_end="16dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- translation by @artemisoftnian and @smack815 -->
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
<!-- Created by The Translator <https://play.google.com/store/apps/details?id=com.sunilpaulmathew.translator> -->
|
||||
<string name="action_settings">Configuración</string>
|
||||
<string name="channel_name">Nombre del canal</string>
|
||||
<string name="channel_options">Opciones del canal</string>
|
||||
|
@ -10,9 +10,9 @@
|
|||
<string name="application_icon">icono de la aplicación</string>
|
||||
<string name="unknown_username">Nombre de usuario desconocido</string>
|
||||
<string name="user_avatar">Avatar de usuario</string>
|
||||
<string name="sample_message">hey encontré el caché, está aquí al lado del tigre grande. Estoy un poco asustado.</string>
|
||||
<string name="sample_message">hey encontré el caché está aquí al lado del tigre grande. Estoy un poco asustado.</string>
|
||||
<string name="send_text">Enviar texto</string>
|
||||
<string name="warning_not_paired">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.</string>
|
||||
<string name="warning_not_paired">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.</string>
|
||||
<string name="username_unset">Nombre de usuario sin configurar</string>
|
||||
<string name="your_name">Tu nombre</string>
|
||||
<string name="analytics_okay">Estadísticas de uso anónimo e informes de fallos.</string>
|
||||
|
@ -28,4 +28,69 @@
|
|||
<string name="are_you_sure_channel">¿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.</string>
|
||||
<string name="new_channel_rcvd">Nueva URL de canal recibida</string>
|
||||
<string name="do_you_want_switch">¿Quieres cambiar cambiar al canal de \'%s\'?</string>
|
||||
</resources>
|
||||
<string name="permission_missing">Falta un permiso necesario Meshtastic no podrá funcionar correctamente. Por favor habilite en la configuración de la aplicación Android.</string>
|
||||
<string name="radio_sleeping">La radio estaba en reposo no podía cambiar de canal</string>
|
||||
<string name="report_bug">informe de fallos</string>
|
||||
<string name="report_a_bug">Informar de un error</string>
|
||||
<string name="report_bug_text">¿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ó.</string>
|
||||
<string name="report">Reportar</string>
|
||||
<string name="select_radio">Seleccione la radio</string>
|
||||
<string name="current_pair">Actualmente estás emparejado con la radio %s</string>
|
||||
<string name="not_paired_yet">Todavía no has emparejado una radio.</string>
|
||||
<string name="change_radio">Cambiar la radio</string>
|
||||
<string name="please_pair">Por favor empareja el dispositivo en los Ajustes de Android.</string>
|
||||
<string name="pairing_completed">Emparejamiento completado iniciando el servicio</string>
|
||||
<string name="pairing_failed_try_again">El emparejamiento ha fallado por favor seleccione de nuevo</string>
|
||||
<string name="location_disabled">El acceso a la localización está desactivado no puede proporcionar la posición a la malla.</string>
|
||||
<string name="share">Compartir</string>
|
||||
<string name="disconnected">Desconectado</string>
|
||||
<string name="device_sleeping">Dispositivo en reposo</string>
|
||||
<string name="connected_count">Conectado: %s de %s en línea</string>
|
||||
<string name="list_of_nodes">Una lista de nodos en la red</string>
|
||||
<string name="update_firmware">Actualizar el firmware</string>
|
||||
<string name="connected">Conectado a la radio</string>
|
||||
<string name="connected_to">Conectado a la radio (%s)</string>
|
||||
<string name="not_connected">No está conectado seleccione la radio de abajo</string>
|
||||
<string name="connected_sleeping">Conectado a la radio pero está en reposo</string>
|
||||
<string name="update_to">Actualizar a %s</string>
|
||||
<string name="app_too_old">Es necesario actualizar la aplicación</string>
|
||||
<string name="none">Ninguno (desactivado)</string>
|
||||
<string name="modem_config_short">Corto alcance (pero rápido)</string>
|
||||
<string name="modem_config_medium">Alcance medio (pero rápido)</string>
|
||||
<string name="modem_config_long">Largo alcance (pero más lento)</string>
|
||||
<string name="modem_config_very_long">Muy largo alcance (pero lento)</string>
|
||||
<string name="modem_config_unrecognized">SIN RECONOCIMIENTO</string>
|
||||
<string name="meshtastic_service_notifications">Notificaciones de servicio de Meshtastic</string>
|
||||
<string name="location_disabled_warning">Debes activar los servicios de localización (de alta precisión) en los Ajustes de Android</string>
|
||||
<string name="about">Acerca de</string>
|
||||
<string name="a_list_of_nodes_in_the_mesh">Una lista de nodos en la malla</string>
|
||||
<string name="text_messages">Mensajes de texto</string>
|
||||
<string name="channel_invalid">La URL de este canal no es válida y no puede utilizarse</string>
|
||||
<string name="debug_panel">Panel de depuración</string>
|
||||
<string name="debug_last_messages">500 últimos mensajes</string>
|
||||
<string name="clear_last_messages">Limpiar</string>
|
||||
<string name="updating_firmware">Actualizando el firmware espera hasta ocho minutos...</string>
|
||||
<string name="update_successful">Actualización exitosa</string>
|
||||
<string name="update_failed">Actualización fallida</string>
|
||||
<string name="message_reception_time">tiempo de recepción del mensaje</string>
|
||||
<string name="message_reception_state">estado de recepción de mensajes</string>
|
||||
<string name="message_delivery_status">Estado de entrega del mensaje</string>
|
||||
<string name="broadcast_position_secs">Periodo de emisión de la posición (en segundos) 0 - deshabilitar</string>
|
||||
<string name="ls_sleep_secs">Período de reposo del dispositivo (en segundos)</string>
|
||||
<string name="meshtastic_messages_notifications">Notificaciones de mensajes</string>
|
||||
<string name="broadcast_period_too_small">El periodo mínimo de emisión de este canal es %d</string>
|
||||
<string name="protocol_stress_test">Protocolo de prueba de esfuerzo</string>
|
||||
<string name="advanced_settings">Configuración avanzada</string>
|
||||
<string name="firmware_too_old">Es necesario actualizar el firmware</string>
|
||||
<string name="okay">Vale</string>
|
||||
<string name="must_set_region">¡Debe establecer una región!</string>
|
||||
<string name="region">Región</string>
|
||||
<string name="cant_change_no_radio">No se puede cambiar de canal porque la radio aún no está conectada. Por favor inténtelo de nuevo.</string>
|
||||
<string name="sample_coords">55.332244 34.442211</string>
|
||||
<string name="save_messages">Guardar mensajes como csv...</string>
|
||||
<string name="set_channel_options">Establecer las opciones de los canales</string>
|
||||
<string name="reset">Reiniciar</string>
|
||||
<string name="are_you_shure_change_default">¿Estás seguro de que quieres cambiar al canal por defecto?</string>
|
||||
<string name="reset_to_defaults">Restablecer los valores predeterminados</string>
|
||||
<string name="apply">Aplique</string>
|
||||
</resources>
|
|
@ -76,4 +76,36 @@
|
|||
<string name="location_disabled_warning">Musíte zapnúť služby o polohe zariadenia (GPS) v nastaveniach systému Android.</string>
|
||||
<string name="modem_config_unrecognized">UNRECOGNIZED</string>
|
||||
<string name="rate_dialog_message_en">Tá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!</string>
|
||||
<string name="a_list_of_nodes_in_the_mesh">Zoznam zariadení v mesh sieti</string>
|
||||
<string name="text_messages">Textové správy</string>
|
||||
<string name="channel_invalid">URL tohoto kanála nie je platá a nemôže byť použitá</string>
|
||||
<string name="debug_panel">Debug okno</string>
|
||||
<string name="debug_last_messages">500 posledných správ</string>
|
||||
<string name="clear_last_messages">Zmazať</string>
|
||||
<string name="updating_firmware">Aktualizácia firmvéru, môže to trvať do 8 minút...</string>
|
||||
<string name="update_successful">Aktualizácia úspešná</string>
|
||||
<string name="update_failed">Aktualizácia zlyhala</string>
|
||||
<string name="message_reception_time">čas prijatia správy</string>
|
||||
<string name="message_reception_state">stav prijatia správy</string>
|
||||
<string name="message_delivery_status">stav doručenia správy</string>
|
||||
<string name="broadcast_position_secs">Interval rozosielania pozície (v sekundách)</string>
|
||||
<string name="ls_sleep_secs">Interval uspávania zariadenia (v sekundách)</string>
|
||||
<string name="meshtastic_messages_notifications">Upozornenia na správy</string>
|
||||
<string name="broadcast_period_too_small">Najkratší interval rozosielania pre tento kanál je %d</string>
|
||||
<string name="protocol_stress_test">Stres test protokolu</string>
|
||||
<string name="advanced_settings">Rozšírené nastavenia</string>
|
||||
<string name="firmware_too_old">Nutná aktualizácia formvéru</string>
|
||||
<string name="okay">Ok</string>
|
||||
<string name="must_set_region">Musíte nastaviť región!</string>
|
||||
<string name="region">Región</string>
|
||||
<string name="sample_coords">48.14816 17.10674</string>
|
||||
<string name="save_messages">Ulož spávy ako CSV súbor</string>
|
||||
<string name="set_channel_options">Nastaviť kanál</string>
|
||||
<string name="reset">Resetovať</string>
|
||||
<string name="reset_to_defaults">Resetovať na základné (default) nastavenia</string>
|
||||
<string name="apply">Použiť</string>
|
||||
<string name="are_you_shure_change_default">Ste si istý, že chcete preladiť na základný (default) kanál?</string>
|
||||
<string name="cant_change_no_radio">Nie je možné zmeniť kanál, pretože vysielač ešte nie je pripojený. Skúste to neskôr.</string>
|
||||
<string name="firmware_old">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 <a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation">našom sprievodcovi inštaláciou firmvéru</a> na Github-e.</string>
|
||||
<string name="min_firmware_version">0.1.01</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<resources>
|
||||
<string name="min_firmware_version">0.1.01</string>
|
||||
<string name="min_firmware_version" translatable="false">0.1.01</string>
|
||||
</resources>
|
|
@ -90,11 +90,17 @@
|
|||
<string name="protocol_stress_test">Protocol stress test</string>
|
||||
<string name="advanced_settings">Advanced settings</string>
|
||||
<string name="firmware_too_old">Firmware update required</string>
|
||||
<string name="firmware_old">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 <a href="https://www.meshtastic.org/software/firmware-too-old.html">our wiki</a>.</string>
|
||||
<string name="firmware_old">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 <a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation">our Firmware Installation guide</a> on Github.</string>
|
||||
<string name="okay">Okay</string>
|
||||
<string name="must_set_region">You must set a region!</string>
|
||||
<string name="region">Region</string>
|
||||
<string name="cant_change_no_radio">Couldn\'t change channel, because radio is not yet connected. Please try again.</string>
|
||||
<string name="sample_coords">55.332244 34.442211</string>
|
||||
<string name="save_messages">Save messages as csv...</string>
|
||||
<string name="set_channel_options">Set channel options</string>
|
||||
<string name="reset">Reset</string>
|
||||
<string name="are_you_shure_change_default">Are you sure you want to change to the default channel?</string>
|
||||
<string name="reset_to_defaults">Reset to defaults</string>
|
||||
<string name="apply">Apply</string>
|
||||
<string name="no_app_found">No application found to send URLs</string>
|
||||
</resources>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 158f6f2dd5dfe81833ed035d54045d7b34394e51
|
||||
Subproject commit f9bb14be97e5d04f73758e806ee6cc7a32a6f43d
|
Ładowanie…
Reference in New Issue