Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Vadim Furman 2021-03-19 16:55:22 -07:00
commit 8aa7585fdd
22 changed files with 376 additions and 275 deletions

View file

@ -37,6 +37,7 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.geeksville.android.BindFailedException
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
@ -306,11 +307,7 @@ class MainActivity : AppCompatActivity(), Logging,
if (deniedPermissions.isNotEmpty()) {
errormsg("Denied permissions: ${deniedPermissions.joinToString(",")}")
Toast.makeText(
this,
getString(R.string.permission_missing),
Toast.LENGTH_LONG
).show()
showToast(R.string.permission_missing)
}
}
@ -675,8 +672,7 @@ class MainActivity : AppCompatActivity(), Logging,
else {
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
val minVer = DeviceVersion("1.2.0")
if (curVer < minVer)
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
@ -704,40 +700,52 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
private fun showToast(msgId: Int) {
Toast.makeText(
this,
msgId,
Toast.LENGTH_LONG
).show()
}
private fun showToast(msg: String) {
Toast.makeText(
this,
msg,
Toast.LENGTH_LONG
).show()
}
private fun perhapsChangeChannel() {
// If the is opening a channel URL, handle it now
requestedChannelUrl?.let { url ->
try {
val channels = ChannelSet(url)
val primary = channels.primaryChannel
requestedChannelUrl = null
if (primary == null)
showToast(R.string.channel_invalid)
else {
requestedChannelUrl = null
MaterialAlertDialogBuilder(this)
.setTitle(R.string.new_channel_rcvd)
.setMessage(getString(R.string.do_you_want_switch).format(primary.name))
.setNeutralButton(R.string.cancel) { _, _ ->
// Do nothing
}
.setPositiveButton(R.string.accept) { _, _ ->
debug("Setting channel from URL")
try {
model.setChannels(channels)
} catch (ex: RemoteException) {
errormsg("Couldn't change channel ${ex.message}")
Toast.makeText(
this,
"Couldn't change channel, because radio is not yet connected. Please try again.",
Toast.LENGTH_SHORT
).show()
MaterialAlertDialogBuilder(this)
.setTitle(R.string.new_channel_rcvd)
.setMessage(getString(R.string.do_you_want_switch).format(primary.name))
.setNeutralButton(R.string.cancel) { _, _ ->
// Do nothing
}
}
.show()
.setPositiveButton(R.string.accept) { _, _ ->
debug("Setting channel from URL")
try {
model.setChannels(channels)
} catch (ex: RemoteException) {
errormsg("Couldn't change channel ${ex.message}")
showToast(R.string.cant_change_no_radio)
}
}
.show()
}
} catch (ex: InvalidProtocolBufferException) {
Toast.makeText(
this,
R.string.channel_invalid,
Toast.LENGTH_LONG
).show()
showToast(R.string.channel_invalid)
}
}
}
@ -962,8 +970,14 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
bindMeshService()
try {
bindMeshService()
}
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()
@ -1062,7 +1076,7 @@ class MainActivity : AppCompatActivity(), Logging,
try {
val packageInfo: PackageInfo = packageManager.getPackageInfo(packageName, 0)
val versionName = packageInfo.versionName
Toast.makeText(applicationContext, versionName, Toast.LENGTH_LONG).show()
showToast(versionName)
} catch (e: PackageManager.NameNotFoundException) {
errormsg("Can not find the version: ${e.message}")
}

View file

@ -14,12 +14,27 @@ import kotlinx.serialization.Serializable
@Serializable
@Parcelize
data class MeshUser(val id: String, val longName: String, val shortName: String) :
data class MeshUser(
val id: String,
val longName: String,
val shortName: String,
val hwModel: MeshProtos.HardwareModel
) :
Parcelable {
override fun toString(): String {
return "MeshUser(id=${id.anonymize}, longName=${longName.anonymize}, shortName=${shortName.anonymize})"
return "MeshUser(id=${id.anonymize}, longName=${longName.anonymize}, shortName=${shortName.anonymize}, hwModel=${hwModelString})"
}
/** a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot
* or null if unset
* */
val hwModelString: String?
get() =
if (hwModel == MeshProtos.HardwareModel.UNSET)
null
else
hwModel.name.replace('_', '-').replace('p', '.').toLowerCase()
}
@Serializable
@ -96,8 +111,8 @@ data class NodeInfo(
get() {
return position?.takeIf {
(it.latitude <= 90.0 && it.latitude >= -90) && // If GPS gives a crap position don't crash our app
it.latitude != 0.0 &&
it.longitude != 0.0
it.latitude != 0.0 &&
it.longitude != 0.0
}
}

View file

@ -1,15 +1,7 @@
package com.geeksville.mesh.model
import android.graphics.Bitmap
import android.net.Uri
import android.util.Base64
import com.geeksville.mesh.ChannelProtos
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
import java.net.MalformedURLException
/** Utility function to make it easy to declare byte arrays - FIXME move someplace better */
fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
@ -19,19 +11,15 @@ data class Channel(
val settings: ChannelProtos.ChannelSettings = ChannelProtos.ChannelSettings.getDefaultInstance()
) {
companion object {
// Note: this string _SHOULD NOT BE LOCALIZED_ because it directly hashes to values used on the device for the default channel name.
// FIXME - make this work with new channel name system
const val defaultChannelName = "Default"
// These bytes must match the well known and not secret bytes used the default channel AES128 key device code
val channelDefaultKey = byteArrayOfInts(
0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59,
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf
)
// Placeholder when emulating
val emulated = Channel(
ChannelProtos.ChannelSettings.newBuilder().setName(defaultChannelName)
// TH=he unsecured channel that devices ship with
val defaultChannel = Channel(
ChannelProtos.ChannelSettings.newBuilder()
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
)
}
@ -78,14 +66,13 @@ data class Channel(
*/
val humanName: String
get() {
val suffix: Char = if (settings.psk.size() != 1) {
// we have a full PSK, so hash it to generate the suffix
val code = settings.psk.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) })
'A' + (code % 26)
} else
'0' + settings.psk.byteAt(0).toInt()
// start with the PSK then xor in the name
val pskCode = xorHash(psk.toByteArray())
val nameCode = xorHash(name.toByteArray())
val suffix = 'A' + ((pskCode xor nameCode) % 26)
return "#${name}-${suffix}"
}
}
fun xorHash(b: ByteArray) = b.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) })

View file

@ -16,11 +16,6 @@ data class ChannelSet(
) {
companion object {
// Placeholder when emulating
val emulated = ChannelSet(
AppOnlyProtos.ChannelSet.newBuilder().addSettings(Channel.emulated.settings).build()
)
const val prefix = "https://www.meshtastic.org/d/#"
private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING
@ -47,9 +42,11 @@ data class ChannelSet(
/**
* Return the primary channel info
*/
val primaryChannel: Channel get() {
return Channel(protobuf.getSettings(0))
}
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

View file

@ -1,6 +1,7 @@
package com.geeksville.mesh.model
import androidx.lifecycle.MutableLiveData
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshUser
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Position
@ -25,7 +26,8 @@ class NodeDB(private val ui: UIViewModel) {
MeshUser(
"+16508765308".format(8),
"Kevin MesterNoLoc",
"KLO"
"KLO",
MeshProtos.HardwareModel.ANDROID_SIM
),
null
)
@ -36,7 +38,8 @@ class NodeDB(private val ui: UIViewModel) {
MeshUser(
"+165087653%02d".format(9 + index),
"Kevin Mester$index",
"KM$index"
"KM$index",
MeshProtos.HardwareModel.ANDROID_SIM
),
it
)

View file

@ -94,6 +94,10 @@ class MeshService : Service(), Logging {
"com.geeksville.mesh",
"com.geeksville.mesh.service.MeshService"
)
/** The minimmum firmware version we know how to talk to. We'll still be able to talk to 1.0 firmwares but only well enough to ask them to firmware update
*/
val minFirmwareVersion = DeviceVersion("1.2.0")
}
enum class ConnectionState {
@ -373,10 +377,10 @@ class MeshService : Service(), Logging {
}
}
private fun installNewNodeDB(newMyNodeInfo: MyNodeInfo, nodes: Array<NodeInfo>) {
private fun installNewNodeDB(ni: MyNodeInfo, nodes: Array<NodeInfo>) {
discardNodeDB() // Get rid of any old state
myNodeInfo = newMyNodeInfo
myNodeInfo = ni
// put our node array into our two different map representations
nodeDBbyNodeNum.putAll(nodes.map { Pair(it.num, it) })
@ -542,7 +546,7 @@ class MeshService : Service(), Logging {
debug("Sending channels to device")
asChannels.forEach {
setChannel(it)
setChannel(it)
}
channels = asChannels.toTypedArray()
@ -726,7 +730,8 @@ class MeshService : Service(), Logging {
// Handle new style routing info
Portnums.PortNum.ROUTING_APP_VALUE -> {
shouldBroadcast = true // We always send acks to other apps, because they might care about the messages they sent
shouldBroadcast =
true // We always send acks to other apps, because they might care about the messages they sent
val u = MeshProtos.Routing.parseFrom(data.payload)
if (u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE)
handleAckNak(true, data.requestId)
@ -779,12 +784,13 @@ class MeshService : Service(), Logging {
channels[ch.index] = ch
debug("Admin: Received channel ${ch.index}")
if (ch.index + 1 < mi.maxChannels) {
if(ch.hasSettings()) {
// Stop once we get to the first disabled entry
if (/* ch.hasSettings() || */ ch.role != ChannelProtos.Channel.Role.DISABLED) {
// Not done yet, request next channel
requestChannel(ch.index + 1)
}
else {
debug("We've received the primary channel, allowing rest of app to start...")
} else {
debug("We've received the last channel, allowing rest of app to start...")
onHasSettings()
}
} else {
@ -807,7 +813,8 @@ class MeshService : Service(), Logging {
it.user = MeshUser(
if (p.id.isNotEmpty()) p.id else oldId, // If the new update doesn't contain an ID keep our old value
p.longName,
p.shortName
p.shortName,
p.hwModel
)
}
}
@ -945,13 +952,13 @@ class MeshService : Service(), Logging {
}
private var locationRequestInterval: Long = 0;
private fun setupLocationRequest () {
private fun setupLocationRequest() {
val desiredInterval: Long = if (myNodeInfo?.hasGPS == true) {
0L // no requests when device has GPS
} else if (numOnlineNodes < 2) {
} 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
radioConfig?.preferences?.positionBroadcastSecs?.times(1000L) ?: 5 * 60 * 1000L
}
debug("desired location request $desiredInterval, current $locationRequestInterval")
@ -1174,18 +1181,6 @@ class MeshService : Service(), Logging {
/// Used to make sure we never get foold by old BLE packets
private var configNonce = 1
private fun handleRadioConfig(radio: RadioConfigProtos.RadioConfig) {
val packetToSave = Packet(
UUID.randomUUID().toString(),
"RadioConfig",
System.currentTimeMillis(),
radio.toString()
)
insertPacket(packetToSave)
radioConfig = radio
}
/**
* Convert a protobuf NodeInfo into our model objects and update our node DB
*/
@ -1197,7 +1192,8 @@ class MeshService : Service(), Logging {
MeshUser(
info.user.id,
info.user.longName,
info.user.shortName
info.user.shortName,
info.user.hwModel
)
if (info.hasPosition()) {
@ -1224,6 +1220,80 @@ class MeshService : Service(), Logging {
}
private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null
/** Regenerate the myNodeInfo model. We call this twice. Once after we receive myNodeInfo from the device
* and again after we have the node DB (which might allow us a better notion of our HwModel.
*/
private fun regenMyNodeInfo() {
val myInfo = rawMyNodeInfo
if (myInfo != null) {
val a = RadioInterfaceService.getBondedDeviceAddress(this)
val isBluetoothInterface = a != null && a.startsWith("x")
var hwModelStr = myInfo.hwModelDeprecated
if (hwModelStr.isEmpty()) {
val nodeNum =
myInfo.myNodeNum // Note: can't use the normal property because myNodeInfo not yet setup
val ni = nodeDBbyNodeNum[nodeNum] // can't use toNodeInfo because too early
val asStr = ni?.user?.hwModelString
if (asStr != null)
hwModelStr = asStr
}
val mi = with(myInfo) {
MyNodeInfo(
myNodeNum,
hasGps,
hwModelStr,
firmwareVersion,
firmwareUpdateFilename != null,
isBluetoothInterface && com.geeksville.mesh.service.SoftwareUpdateService.shouldUpdate(
this@MeshService,
DeviceVersion(firmwareVersion)
),
currentPacketId.toLong() and 0xffffffffL,
if (messageTimeoutMsec == 0) 5 * 60 * 1000 else messageTimeoutMsec, // constants from current device code
minAppVersion,
maxChannels
)
}
newMyNodeInfo = mi
setFirmwareUpdateFilename(mi)
}
}
private fun sendAnalytics() {
val myInfo = rawMyNodeInfo
val mi = myNodeInfo
if (myInfo != null && mi != null) {
/// Track types of devices and firmware versions in use
GeeksvilleApplication.analytics.setUserInfo(
// DataPair("region", mi.region),
DataPair("firmware", mi.firmwareVersion),
DataPair("has_gps", mi.hasGPS),
DataPair("hw_model", mi.model),
DataPair("dev_error_count", myInfo.errorCount)
)
if (myInfo.errorCode.number != 0) {
GeeksvilleApplication.analytics.track(
"dev_error",
DataPair("code", myInfo.errorCode.number),
DataPair("address", myInfo.errorAddress),
// We also include this info, because it is required to correctly decode address from the map file
DataPair("firmware", mi.firmwareVersion),
DataPair("hw_model", mi.model)
// DataPair("region", mi.region)
)
}
}
}
/// If found, the old region string of the form 1.0-EU865 etc...
private var legacyRegion: String? = null
/**
* Update the nodeinfo (called from either new API version or the old one)
*/
@ -1236,62 +1306,19 @@ class MeshService : Service(), Logging {
)
insertPacket(packetToSave)
setFirmwareUpdateFilename(myInfo)
val a = RadioInterfaceService.getBondedDeviceAddress(this)
val isBluetoothInterface = a != null && a.startsWith("x")
val mi = with(myInfo) {
MyNodeInfo(
myNodeNum,
hasGps,
hwModel,
firmwareVersion,
firmwareUpdateFilename != null,
isBluetoothInterface && SoftwareUpdateService.shouldUpdate(
this@MeshService,
DeviceVersion(firmwareVersion)
),
currentPacketId.toLong() and 0xffffffffL,
if (messageTimeoutMsec == 0) 5 * 60 * 1000 else messageTimeoutMsec, // constants from current device code
minAppVersion,
maxChannels
)
}
newMyNodeInfo = mi
rawMyNodeInfo = myInfo
legacyRegion = myInfo.region
regenMyNodeInfo()
// We'll need to get a new set of channels and settings now
radioConfig = null
// prefill the channel array with null channels
channels = Array(mi.maxChannels) {
channels = Array(myInfo.maxChannels) {
val b = ChannelProtos.Channel.newBuilder()
b.index = it
b.build()
}
/// Track types of devices and firmware versions in use
GeeksvilleApplication.analytics.setUserInfo(
// DataPair("region", mi.region),
DataPair("firmware", mi.firmwareVersion),
DataPair("has_gps", mi.hasGPS),
DataPair("hw_model", mi.model),
DataPair("dev_error_count", myInfo.errorCount)
)
if (myInfo.errorCode.number != 0) {
GeeksvilleApplication.analytics.track(
"dev_error",
DataPair("code", myInfo.errorCode.number),
DataPair("address", myInfo.errorAddress),
// We also include this info, because it is required to correctly decode address from the map file
DataPair("firmware", mi.firmwareVersion),
DataPair("hw_model", mi.model)
// DataPair("region", mi.region)
)
}
}
@ -1314,34 +1341,37 @@ class MeshService : Service(), Logging {
}
if (curRegionValue == RadioConfigProtos.RegionCode.Unset_VALUE) {
TODO("Need gui for setting region")
/* // look for a legacy region
// look for a legacy region
val legacyRegex = Regex(".+-(.+)")
myNodeInfo?.region?.let { legacyRegion ->
val matches = legacyRegex.find(legacyRegion)
legacyRegion?.let { lr ->
val matches = legacyRegex.find(lr)
if (matches != null) {
val (region) = matches.destructured
val newRegion = RadioConfigProtos.RegionCode.valueOf(region)
info("Upgrading legacy region $newRegion (code ${newRegion.number})")
curRegionValue = newRegion.number
}
} */
}
}
// 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) {
info("Telling device to upgrade region")
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()
// 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()
val newPrefs = currentConfig.preferences.toBuilder()
newPrefs.regionValue = curRegionValue
newConfig.preferences = newPrefs.build()
sendRadioConfig(newConfig.build())
sendRadioConfig(newConfig.build())
}
}
else
warn("Device is too old to understand region changes")
}
}
}
@ -1376,13 +1406,23 @@ class MeshService : Service(), Logging {
else {
discardNodeDB()
debug("Installing new node DB")
myNodeInfo = newMyNodeInfo
myNodeInfo = newMyNodeInfo// Install myNodeInfo as current
newNodes.forEach(::installNodeInfo)
newNodes.clear() // Just to save RAM ;-)
haveNodeDB = true // we now have nodes from real hardware
requestRadioConfig()
regenMyNodeInfo() // we have a node db now, so can possibly find a better hwmodel
myNodeInfo = newMyNodeInfo // we might have just updated myNodeInfo
sendAnalytics()
if (deviceVersion < minFirmwareVersion) {
info("Device firmware is too old, faking config so firmware update can occur")
onHasSettings()
} else
requestRadioConfig()
}
} else
warn("Ignoring stale config complete")
@ -1553,12 +1593,12 @@ class MeshService : Service(), Logging {
/***
* Return the filename we will install on the device
*/
private fun setFirmwareUpdateFilename(info: MeshProtos.MyNodeInfo) {
private fun setFirmwareUpdateFilename(info: MyNodeInfo) {
firmwareUpdateFilename = try {
if (info.region != null && info.firmwareVersion != null && info.hwModel != null)
if (info.firmwareVersion != null && info.model != null)
SoftwareUpdateService.getUpdateFilename(
this,
info.hwModel
info.model
)
else
null
@ -1670,7 +1710,7 @@ class MeshService : Service(), Logging {
info("sendData dest=${p.to}, id=${p.id} <- ${p.bytes!!.size} bytes (connectionState=$connectionState)")
if(p.dataType == 0)
if (p.dataType == 0)
throw Exception("Port numbers must be non-zero!") // we are now more strict
// Keep a record of datapackets, so GUIs can show proper chat history
@ -1726,7 +1766,7 @@ class MeshService : Service(), Logging {
channelSet.toByteArray()
}
override fun setChannels(payload: ByteArray?) {
override fun setChannels(payload: ByteArray?) = toRemoteExceptions {
val parsed = AppOnlyProtos.ChannelSet.parseFrom(payload)
channelSet = parsed
}

View file

@ -38,12 +38,22 @@ class MeshServiceLocationCallback(
MeshService.info("got phone location")
if (location.isAccurateForMesh) { // if within 200 meters, or accuracy is unknown
// 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 destinationNumber = if (shouldBroadcast) DataPacket.NODENUM_BROADCAST else getNodeNum()
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 destinationNumber =
if (shouldBroadcast) DataPacket.NODENUM_BROADCAST else getNodeNum()
// Note: we never want this message sent as a reliable message, because it is low value and we'll be sending one again later anyways
sendPosition(location, destinationNumber, wantResponse = false)
// Note: we never want this message sent as a reliable message, because it is low value and we'll be sending one again later anyways
sendPosition(location, destinationNumber, wantResponse = false)
} catch (ex: RemoteException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
MeshService.warn("Lost connection to radio, stopping location requests")
onSendPositionFailed()
} catch (ex: BLEException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
MeshService.warn("BLE exception, stopping location requests $ex")
onSendPositionFailed()
}
} else {
MeshService.warn("accuracy ${location.accuracy} is too poor to use")
}
@ -51,21 +61,13 @@ class MeshServiceLocationCallback(
}
private fun sendPosition(location: Location, destinationNumber: Int, wantResponse: Boolean) {
try {
onSendPosition(
location.latitude,
location.longitude,
location.altitude.toInt(),
destinationNumber,
wantResponse // wantResponse?
)
} catch (ex: RemoteException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
MeshService.warn("Lost connection to radio, stopping location requests")
onSendPositionFailed()
} catch (ex: BLEException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
MeshService.warn("BLE exception, stopping location requests $ex")
onSendPositionFailed()
}
onSendPosition(
location.latitude,
location.longitude,
location.altitude.toInt(),
destinationNumber,
wantResponse // wantResponse?
)
}
/**

View file

@ -142,6 +142,7 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi
id = DataPacket.nodeNumToDefaultId(numIn)
longName = "Sim " + num.toHexString()
shortName = getInitials(longName)
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
}.build()
position = MeshProtos.Position.newBuilder().apply {
latitudeI = Position.degI(lat)
@ -160,9 +161,8 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi
MeshProtos.FromRadio.newBuilder().apply {
myInfo = MeshProtos.MyNodeInfo.newBuilder().apply {
myNodeNum = MY_NODE
hwModel = "Sim"
messageTimeoutMsec = 5 * 60 * 1000
firmwareVersion = service.getString(R.string.cur_firmware_version)
firmwareVersion = "1.2.8" // Pretend to be running an older 1.2 version
numBands = 13
maxChannels = 8
}.build()

View file

@ -79,11 +79,10 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
/// Pull the latest data from the model (discarding any user edits)
private fun setGUIfromModel() {
val channels = model.channels.value
val channel = channels?.primaryChannel
binding.editableCheckbox.isChecked = false // start locked
if (channels != null) {
val channel = channels.primaryChannel
if (channel != null) {
binding.qrView.visibility = View.VISIBLE
binding.channelNameEdit.visibility = View.VISIBLE
binding.channelNameEdit.setText(channel.humanName)
@ -156,8 +155,8 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
val checked = binding.editableCheckbox.isChecked
if (checked) {
// User just unlocked for editing - remove the # goo around the channel name
model.channels.value?.let { channels ->
binding.channelNameEdit.setText(channels.primaryChannel.name)
model.channels.value?.primaryChannel?.let { ch ->
binding.channelNameEdit.setText(ch.name)
}
} else {
// User just locked it, we should warn and then apply changes to radio
@ -169,14 +168,13 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
}
.setPositiveButton(getString(R.string.accept)) { _, _ ->
// Generate a new channel with only the changes the user can change in the GUI
model.channels.value?.let { old ->
val oldPrimary = old.primaryChannel
val newSettings = oldPrimary.settings.toBuilder()
model.channels.value?.primaryChannel?.let { oldPrimary ->
var newSettings = oldPrimary.settings.toBuilder()
newSettings.name = binding.channelNameEdit.text.toString().trim()
// Generate a new AES256 key (for any channel not named Default)
// Generate a new AES256 key unleess the user is trying to go back to stock
if (!newSettings.name.equals(
Channel.defaultChannelName,
Channel.defaultChannel.name,
ignoreCase = true
)
) {
@ -186,10 +184,8 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
random.nextBytes(bytes)
newSettings.psk = ByteString.copyFrom(bytes)
} else {
debug("ASSIGNING NEW default AES128 KEY")
newSettings.name =
Channel.defaultChannelName // Fix any case errors
newSettings.psk = ByteString.copyFrom(Channel.channelDefaultKey)
debug("Switching back to default channel")
newSettings = Channel.defaultChannel.settings.toBuilder()
}
val selectedChannelOptionString =

View file

@ -13,6 +13,7 @@ import com.geeksville.android.Logging
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.util.formatAgo
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
@ -79,7 +80,7 @@ class MapFragment : ScreenFragment("Map"), Logging {
)
)
node.user?.let {
f.addStringProperty("name", it.longName)
f.addStringProperty("name", it.longName + " " + formatAgo(p.time))
}
f
}
@ -93,7 +94,8 @@ class MapFragment : ScreenFragment("Map"), Logging {
}
fun zoomToNodes(map: MapboxMap) {
val nodesWithPosition = model.nodeDB.nodes.value?.values?.filter { it.validPosition != null }
val nodesWithPosition =
model.nodeDB.nodes.value?.values?.filter { it.validPosition != null }
if (nodesWithPosition != null && nodesWithPosition.isNotEmpty()) {
val update = if (nodesWithPosition.size >= 2) {
// Multiple nodes, make them all fit on the map view
@ -158,7 +160,10 @@ class MapFragment : ScreenFragment("Map"), Logging {
if (view != null) { // it might have gone away by now
// val markerIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_twotone_person_pin_24)
val markerIcon =
ContextCompat.getDrawable(requireActivity(), R.drawable.ic_twotone_person_pin_24)!!
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_twotone_person_pin_24
)!!
map.setStyle(Style.OUTDOORS) { style ->
style.addSource(nodePositions)
@ -176,7 +181,7 @@ class MapFragment : ScreenFragment("Map"), Logging {
// Any times nodes change update our map
model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { nodes ->
if(isViewVisible)
if (isViewVisible)
onNodesChanged(map, nodes.values)
})
zoomToNodes(map)

View file

@ -610,7 +610,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
statusText.text = getString(R.string.must_set_region)
connected == MeshService.ConnectionState.CONNECTED -> {
val fwStr = info?.firmwareString ?: ""
val fwStr = info?.firmwareString ?: "unknown"
statusText.text = getString(R.string.connected_to).format(fwStr)
}
connected == MeshService.ConnectionState.DISCONNECTED ->

View file

@ -2,11 +2,13 @@ package com.geeksville.mesh.ui
import android.os.Bundle
import android.text.format.DateFormat
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
@ -17,13 +19,14 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding
import com.geeksville.mesh.databinding.NodelistFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import java.text.ParseException
import java.util.*
import com.geeksville.util.formatAgo
import java.net.URLEncoder
class UsersFragment : ScreenFragment("Users"), Logging {
private var _binding: NodelistFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
@ -34,6 +37,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
class ViewHolder(itemView: AdapterNodeLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
val nodeNameView = itemView.nodeNameView
val distanceView = itemView.distanceView
val coordsView = itemView.coordsView
val batteryPctView = itemView.batteryPercentageView
val lastTime = itemView.lastConnectionView
val powerIcon = itemView.batteryIcon
@ -105,8 +109,26 @@ class UsersFragment : ScreenFragment("Users"), Logging {
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val n = nodes[position]
val name = n.user?.longName ?: n.user?.id ?: "Unknown node"
holder.nodeNameView.text = name
holder.nodeNameView.text = n.user?.longName ?: n.user?.id ?: "Unknown node"
val pos = n.validPosition;
if (pos != null) {
val coords =
String.format("%.5f %.5f", pos.latitude, pos.longitude).replace(",", ".")
val html =
"<a href='geo:${pos.latitude},${pos.longitude}?z=17&label=${
URLEncoder.encode(
name,
"utf-8"
)
}'>${coords}</a>"
holder.coordsView.text = HtmlCompat.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
holder.coordsView.movementMethod = LinkMovementMethod.getInstance()
holder.coordsView.visibility = View.VISIBLE
} else {
holder.coordsView.visibility = View.INVISIBLE
}
val ourNodeInfo = model.nodeDB.ourNodeInfo
val distance = ourNodeInfo?.distanceStr(n)
@ -116,10 +138,10 @@ class UsersFragment : ScreenFragment("Users"), Logging {
} else {
holder.distanceView.visibility = View.INVISIBLE
}
debug("node=${n.user?.longName} bat=${n.batteryPctLevel}")
renderBattery(n.batteryPctLevel, holder)
holder.lastTime.text = getLastTimeValue(n)
holder.lastTime.text = formatAgo(n.lastSeen);
if ((n.num == ourNodeInfo?.num) || (n.snr > 100f)) {
holder.snrView.visibility = View.INVISIBLE
} else {
@ -157,30 +179,6 @@ class UsersFragment : ScreenFragment("Users"), Logging {
})
}
private fun getLastTimeValue(n: NodeInfo): String {
var lastTimeText = "?"
val currentTime = (System.currentTimeMillis()/1000).toInt()
val threeDaysLong = 3 * 60*60*24
//if the lastSeen is too old
if (n.lastSeen < (currentTime - threeDaysLong))
return lastTimeText
try {
val toLong: Long = n.lastSeen.toLong()
val long1000 = toLong * 1000L
val date = Date(long1000)
val timeFormat = DateFormat.getTimeFormat(context)
lastTimeText = timeFormat.format(date)
} catch (e: ParseException) {
//
}
return lastTimeText
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?