Merge pull request #261 from geeksville/dev1.2

changes from while on vacation
pull/262/head
Kevin Hester 2021-03-19 15:16:03 +08:00 zatwierdzone przez GitHub
commit a35714186d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
14 zmienionych plików z 212 dodań i 139 usunięć

Wyświetl plik

@ -24,7 +24,7 @@ The app is also distributed for Amazon Fire devices via the Amazon appstore: [![
If you would like to develop this application we'd love your help! These build instructions are brief
and should be improved, please send a PR if you can.
* Use Android Studio 4.0 RC 1 to build/debug (other versions might work but no promises)
* Use Android Studio 4.1.2 to build/debug (other versions might work but no promises)
* Use "git submodule update --init --recursive" to pull in the various submodules we depend on
* There are a few config files which you'll need to copy from templates included in the project.
Run the following commands to do so:

Wyświetl plik

@ -113,7 +113,7 @@ 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.0'
implementation 'androidx.fragment:fragment-ktx:1.3.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

Wyświetl plik

@ -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
@ -308,11 +309,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)
}
}
@ -700,40 +697,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)
}
}
}
@ -958,8 +967,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()
@ -1058,7 +1073,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}")
}

Wyświetl plik

@ -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
@ -95,8 +110,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
}
}

Wyświetl plik

@ -47,9 +47,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

Wyświetl plik

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

Wyświetl plik

@ -373,10 +373,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 +542,7 @@ class MeshService : Service(), Logging {
debug("Sending channels to device")
asChannels.forEach {
setChannel(it)
setChannel(it)
}
channels = asChannels.toTypedArray()
@ -725,7 +725,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)
@ -778,12 +779,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 {
@ -806,7 +808,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
)
}
}
@ -1194,7 +1197,8 @@ class MeshService : Service(), Logging {
MeshUser(
info.user.id,
info.user.longName,
info.user.shortName
info.user.shortName,
info.user.hwModel
)
if (info.hasPosition()) {
@ -1221,6 +1225,77 @@ 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)
)
}
}
}
/**
* Update the nodeinfo (called from either new API version or the old one)
*/
@ -1233,62 +1308,18 @@ 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,
hwModelDeprecated,
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
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)
)
}
}
@ -1373,12 +1404,18 @@ 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
regenMyNodeInfo() // we have a node db now, so can possibly find a better hwmodel
myNodeInfo = newMyNodeInfo // we might have just updated myNodeInfo
sendAnalytics()
requestRadioConfig()
}
} else
@ -1550,12 +1587,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.hwModelDeprecated != null)
if (info.firmwareVersion != null && info.model != null)
SoftwareUpdateService.getUpdateFilename(
this,
info.hwModelDeprecated
info.model
)
else
null
@ -1667,7 +1704,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
@ -1723,7 +1760,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
}

Wyświetl plik

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

Wyświetl plik

@ -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
hwModelDeprecated = "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()

Wyświetl plik

@ -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,8 +168,7 @@ 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
model.channels.value?.primaryChannel?.let { oldPrimary ->
val newSettings = oldPrimary.settings.toBuilder()
newSettings.name = binding.channelNameEdit.text.toString().trim()

Wyświetl plik

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

Wyświetl plik

@ -94,6 +94,7 @@
<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>
</resources>

2
design

@ -1 +1 @@
Subproject commit d0339f0297c629f1bd6873b4abccfecb98443538
Subproject commit a81074152157fa54b0d02ccbbd6a6357cc3cedcf

@ -1 +1 @@
Subproject commit 6da250358ed13e3c58fd4fa2a123b01b3826d4bf
Subproject commit 158f6f2dd5dfe81833ed035d54045d7b34394e51