Merge branch 'master' into master

pull/276/head
Kevin Hester 2021-04-15 12:04:37 +08:00 zatwierdzone przez GitHub
commit 82ebfd0094
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
48 zmienionych plików z 1099 dodań i 607 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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