diff --git a/.idea/dictionaries/kevinh.xml b/.idea/dictionaries/kevinh.xml
index 9de6060e7..56d3af4f2 100644
--- a/.idea/dictionaries/kevinh.xml
+++ b/.idea/dictionaries/kevinh.xml
@@ -5,6 +5,7 @@
errormsg
geeksville
meshtastic
+ protobuf
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index ffb8e7599..557ecb57b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,8 +31,8 @@ android {
applicationId "com.geeksville.mesh"
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
targetSdkVersion 29
- versionCode 20150 // format is Mmmss (where M is 1+the numeric major number
- versionName "1.1.50"
+ versionCode 20204 // format is Mmmss (where M is 1+the numeric major number
+ versionName "1.2.04"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// per https://developer.android.com/studio/write/vector-asset-studio
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 97d4bf2f1..5bf37ee1f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -133,7 +133,7 @@
@@ -143,11 +143,11 @@
+ android:pathPrefix="/d/" />
+ android:pathPrefix="/D/" />
diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
index fcb44874a..a9ee757d0 100644
--- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
+++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
@@ -71,18 +71,26 @@ interface IMeshService {
*/
List getNodes();
- /// This method is only intended for use in our GUI, so the user can set radio options
- /// It returns a RadioConfig protobuf.
- byte []getRadioConfig();
-
/// Return an list of MeshPacket protobuf (byte arrays) which were received while your client app was offline (recent messages only).
/// Also includes any messages we have sent recently (useful for finding current message status)
List getOldMessages();
+ /// This method is only intended for use in our GUI, so the user can set radio options
+ /// It returns a RadioConfig protobuf.
+ byte []getRadioConfig();
+
/// This method is only intended for use in our GUI, so the user can set radio options
/// It sets a RadioConfig protobuf
void setRadioConfig(in byte []payload);
+ /// This method is only intended for use in our GUI, so the user can set radio options
+ /// It returns a ChannelSet protobuf.
+ byte []getChannels();
+
+ /// This method is only intended for use in our GUI, so the user can set radio options
+ /// It sets a ChannelSet protobuf
+ void setChannels(in byte []payload);
+
/**
Is the packet radio currently connected to the phone? Returns a ConnectionState string.
*/
@@ -94,8 +102,8 @@ interface IMeshService {
/// Returns true if the device address actually changed, or false if no change was needed
boolean setDeviceAddress(String deviceAddr);
- /// Get basic device hardware info about our connected radio. Will never return NULL. Will throw
- /// RemoteException if no my node info is available
+ /// Get basic device hardware info about our connected radio. Will never return NULL. Will return NULL
+ /// if no my node info is available (i.e. it will not throw an exception)
MyNodeInfo getMyNodeInfo();
/// Start updating the radios firmware
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 8e5a7230a..136169f67 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -45,6 +45,8 @@ import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.Channel
+import com.geeksville.mesh.model.ChannelSet
+import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.*
import com.geeksville.mesh.ui.*
@@ -612,6 +614,29 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
+ /** Show an alert that may contain HTML */
+ private fun showAlert(titleText: Int, messageText: Int) {
+ // make links clickable per https://stackoverflow.com/a/62642807
+ // val messageStr = getText(messageText)
+
+ val builder = MaterialAlertDialogBuilder(this)
+ .setTitle(titleText)
+ .setMessage(messageText)
+ .setPositiveButton(R.string.okay) { _, _ ->
+ info("User acknowledged")
+ }
+
+ val dialog = builder.show()
+
+ // Make the textview clickable. Must be called after show()
+ val view = (dialog.findViewById(android.R.id.message) as TextView?)!!
+ // Linkify.addLinks(view, Linkify.ALL) // not needed with this method
+ view.movementMethod = LinkMovementMethod.getInstance()
+
+ showSettingsPage() // Default to the settings page in this case
+ }
+
+
/// Called when we gain/lose a connection to our mesh radio
private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) {
debug("connchange ${model.isConnected.value} -> $connected")
@@ -628,33 +653,28 @@ class MainActivity : AppCompatActivity(), Logging,
model.myNodeInfo.value = info
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
- if (isOld) {
- // make links clickable per https://stackoverflow.com/a/62642807
- val messageStr = getText(R.string.must_update)
+ if (isOld)
+ showAlert(R.string.app_too_old, R.string.must_update)
+ else {
- val builder = MaterialAlertDialogBuilder(this)
- .setTitle(getString(R.string.app_too_old))
- .setMessage(messageStr)
- .setPositiveButton("Okay") { _, _ ->
- info("User acknowledged app is old")
- }
+ val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
+ val minVer = DeviceVersion("1.2.0")
+ if(curVer < minVer)
+ 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
- val dialog = builder.show()
+ model.radioConfig.value =
+ RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
- // Make the textview clickable. Must be called after show()
- val view = (dialog.findViewById(android.R.id.message) as TextView?)!!
- // Linkify.addLinks(view, Linkify.ALL) // not needed with this method
- view.movementMethod = LinkMovementMethod.getInstance()
- } else {
- // If our app is too old, we probably don't understand the new radioconfig messages
+ model.channels.value =
+ ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
- model.radioConfig.value =
- MeshProtos.RadioConfig.parseFrom(service.radioConfig)
+ updateNodesFromDevice()
- updateNodesFromDevice()
-
- // we have a connection to our device now, do the channel change
- perhapsChangeChannel()
+ // we have a connection to our device now, do the channel change
+ perhapsChangeChannel()
+ }
}
} catch (ex: RemoteException) {
warn("Abandoning connect $ex, because we probably just lost device connection")
@@ -671,19 +691,20 @@ class MainActivity : AppCompatActivity(), Logging,
// If the is opening a channel URL, handle it now
requestedChannelUrl?.let { url ->
try {
- val channel = Channel(url)
+ val channels = ChannelSet(url)
+ val primary = channels.primaryChannel
requestedChannelUrl = null
MaterialAlertDialogBuilder(this)
.setTitle(R.string.new_channel_rcvd)
- .setMessage(getString(R.string.do_you_want_switch).format(channel.name))
+ .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.setChannel(channel.settings)
+ model.setChannels(channels)
} catch (ex: RemoteException) {
errormsg("Couldn't change channel ${ex.message}")
Toast.makeText(
@@ -828,8 +849,10 @@ class MainActivity : AppCompatActivity(), Logging,
val allMsgs = service.oldMessages
val msgs =
allMsgs.filter { p -> p.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE }
- debug("Service provided ${msgs.size} messages and myNodeNum ${service.myNodeInfo?.myNodeNum}")
- model.myNodeInfo.value = service.myNodeInfo
+
+ model.myNodeInfo.value = service.myNodeInfo // Note: this could be NULL!
+ debug("Service provided ${msgs.size} messages and myNodeNum ${model.myNodeInfo.value?.myNodeNum}")
+
model.messagesState.setMessages(msgs)
val connectionState =
MeshService.ConnectionState.valueOf(service.connectionState())
@@ -839,7 +862,6 @@ class MainActivity : AppCompatActivity(), Logging,
if (connectionState != MeshService.ConnectionState.CONNECTED)
updateNodesFromDevice()
-
// We won't receive a notify for the initial state of connection, so we force an update here
onMeshConnectionChanged(connectionState)
} catch (ex: RemoteException) {
@@ -848,9 +870,11 @@ class MainActivity : AppCompatActivity(), Logging,
model.isConnected.value =
MeshService.ConnectionState.valueOf(service.connectionState())
}
+ finally {
+ connectionJob = null
+ }
debug("connected to mesh service, isConnected=${model.isConnected.value}")
- connectionJob = null
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt
index e5a447b1c..da76ccfaa 100644
--- a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt
+++ b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt
@@ -9,32 +9,28 @@ import kotlinx.serialization.Serializable
data class MyNodeInfo(
val myNodeNum: Int,
val hasGPS: Boolean,
- val region: String?,
val model: String?,
val firmwareVersion: String?,
val couldUpdate: Boolean, // this application contains a software load we _could_ install if you want
val shouldUpdate: Boolean, // this device has old firmware
val currentPacketId: Long,
- val nodeNumBits: Int,
- val packetIdBits: Int,
val messageTimeoutMsec: Int,
- val minAppVersion: Int
+ val minAppVersion: Int,
+ val maxChannels: Int
) : Parcelable {
/** A human readable description of the software/hardware version */
- val firmwareString: String get() = "$model $region/$firmwareVersion"
+ val firmwareString: String get() = "$model $firmwareVersion"
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readByte() != 0.toByte(),
parcel.readString(),
parcel.readString(),
- parcel.readString(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readLong(),
parcel.readInt(),
parcel.readInt(),
- parcel.readInt(),
parcel.readInt()
) {
}
@@ -42,16 +38,14 @@ data class MyNodeInfo(
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(myNodeNum)
parcel.writeByte(if (hasGPS) 1 else 0)
- parcel.writeString(region)
parcel.writeString(model)
parcel.writeString(firmwareVersion)
parcel.writeByte(if (couldUpdate) 1 else 0)
parcel.writeByte(if (shouldUpdate) 1 else 0)
parcel.writeLong(currentPacketId)
- parcel.writeInt(nodeNumBits)
- parcel.writeInt(packetIdBits)
parcel.writeInt(messageTimeoutMsec)
parcel.writeInt(minAppVersion)
+ parcel.writeInt(maxChannels)
}
override fun describeContents(): Int {
diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
index 54886c7e5..5686bb209 100644
--- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
@@ -3,6 +3,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
@@ -15,12 +16,12 @@ fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].
data class Channel(
- val settings: MeshProtos.ChannelSettings = MeshProtos.ChannelSettings.getDefaultInstance()
+ 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
- val defaultChannelName = "Default"
+ 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(
@@ -30,30 +31,11 @@ data class Channel(
// Placeholder when emulating
val emulated = Channel(
- MeshProtos.ChannelSettings.newBuilder().setName(defaultChannelName)
- .setModemConfig(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
+ ChannelProtos.ChannelSettings.newBuilder().setName(defaultChannelName)
+ .setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
)
-
- const val prefix = "https://www.meshtastic.org/c/#"
-
- private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING
-
- private fun urlToSettings(url: Uri): MeshProtos.ChannelSettings {
- val urlStr = url.toString()
-
- // We no longer support the super old (about 0.8ish? verison of the URLs that don't use the # separator - I doubt
- // anyone is still using that old format
- val pathRegex = Regex("$prefix(.*)", RegexOption.IGNORE_CASE)
- val (base64) = pathRegex.find(urlStr)?.destructured
- ?: throw MalformedURLException("Not a meshtastic URL: ${urlStr.take(40)}")
- val bytes = Base64.decode(base64, base64Flags)
-
- return MeshProtos.ChannelSettings.parseFrom(bytes)
- }
}
- constructor(url: Uri) : this(urlToSettings(url))
-
/// Return the name of our channel as a human readable string. If empty string, assume "Default" per mesh.proto spec
val name: String
get() = if (settings.name.isEmpty()) {
@@ -61,16 +43,16 @@ data class Channel(
if (settings.bandwidth != 0)
"Unset"
else when (settings.modemConfig) {
- MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128 -> "Medium"
- MeshProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128 -> "ShortFast"
- MeshProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512 -> "LongAlt"
- MeshProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096 -> "LongSlow"
+ ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128 -> "Medium"
+ ChannelProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128 -> "ShortFast"
+ ChannelProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512 -> "LongAlt"
+ ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096 -> "LongSlow"
else -> "Invalid"
}
} else
settings.name
- val modemConfig: MeshProtos.ChannelSettings.ModemConfig get() = settings.modemConfig
+ val modemConfig: ChannelProtos.ChannelSettings.ModemConfig get() = settings.modemConfig
val psk
get() = if (settings.psk.size() != 1)
@@ -106,32 +88,4 @@ data class Channel(
return "#${name}-${suffix}"
}
-
- /// Can this channel be changed right now?
- var editable = false
-
- /// 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
- fun getChannelUrl(upperCasePrefix: Boolean = false): Uri {
- // If we have a valid radio config use it, othterwise use whatever we have saved in the prefs
-
- val channelBytes = settings.toByteArray() ?: ByteArray(0) // if unset just use empty
- val enc = Base64.encodeToString(channelBytes, base64Flags)
-
- val p = if(upperCasePrefix)
- prefix.toUpperCase()
- else
- prefix
- return Uri.parse("$p$enc")
- }
-
- fun getChannelQR(): Bitmap {
- val multiFormatWriter = MultiFormatWriter()
-
- // 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);
- val barcodeEncoder = BarcodeEncoder()
- return barcodeEncoder.createBitmap(bitMatrix)
- }
}
diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt
index 827624cd5..62409f4ff 100644
--- a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt
@@ -1,22 +1,23 @@
-package com.geeksville.mesh.model
-
-import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.R
-
-enum class ChannelOption(val modemConfig: MeshProtos.ChannelSettings.ModemConfig, val configRes: Int, val minBroadcastPeriodSecs: Int) {
- SHORT(MeshProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128, R.string.modem_config_short, 3),
- MEDIUM(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128, R.string.modem_config_medium, 12),
- LONG(MeshProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512, R.string.modem_config_long, 240),
- VERY_LONG(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096, R.string.modem_config_very_long, 375);
-
- companion object {
- fun fromConfig(modemConfig: MeshProtos.ChannelSettings.ModemConfig?): ChannelOption? {
- for (option in values()) {
- if (option.modemConfig == modemConfig)
- return option
- }
- return null
- }
- val defaultMinBroadcastPeriod = VERY_LONG.minBroadcastPeriodSecs
- }
+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) {
+ 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);
+
+ companion object {
+ fun fromConfig(modemConfig: ChannelProtos.ChannelSettings.ModemConfig?): ChannelOption? {
+ for (option in values()) {
+ if (option.modemConfig == modemConfig)
+ return option
+ }
+ return null
+ }
+ val defaultMinBroadcastPeriod = VERY_LONG.minBroadcastPeriodSecs
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt
new file mode 100644
index 000000000..d6b0bcf5e
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt
@@ -0,0 +1,78 @@
+package com.geeksville.mesh.model
+
+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
+import java.net.MalformedURLException
+
+data class ChannelSet(
+ val protobuf: AppOnlyProtos.ChannelSet = AppOnlyProtos.ChannelSet.getDefaultInstance()
+) {
+ 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
+
+ private fun urlToChannels(url: Uri): AppOnlyProtos.ChannelSet {
+ val urlStr = url.toString()
+
+ // We no longer support the super old (about 0.8ish? verison of the URLs that don't use the # separator - I doubt
+ // anyone is still using that old format
+ val pathRegex = Regex("$prefix(.*)", RegexOption.IGNORE_CASE)
+ val (base64) = pathRegex.find(urlStr)?.destructured
+ ?: throw MalformedURLException("Not a meshtastic URL: ${urlStr.take(40)}")
+ val bytes = Base64.decode(base64, base64Flags)
+
+ return AppOnlyProtos.ChannelSet.parseFrom(bytes)
+ }
+ }
+
+ constructor(url: Uri) : this(urlToChannels(url))
+
+ /// Can this channel be changed right now?
+ var editable = false
+
+ /**
+ * Return the primary channel info
+ */
+ val primaryChannel: Channel get() {
+ return Channel(protobuf.getSettings(0))
+ }
+
+ /// 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
+ fun getChannelUrl(upperCasePrefix: Boolean = false): Uri {
+ // If we have a valid radio config use it, othterwise use whatever we have saved in the prefs
+
+ val channelBytes = protobuf.toByteArray() ?: ByteArray(0) // if unset just use empty
+ val enc = Base64.encodeToString(channelBytes, base64Flags)
+
+ val p = if(upperCasePrefix)
+ prefix.toUpperCase()
+ else
+ prefix
+ return Uri.parse("$p$enc")
+ }
+
+ fun getChannelQR(): Bitmap {
+ val multiFormatWriter = MultiFormatWriter()
+
+ // 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);
+ val barcodeEncoder = BarcodeEncoder()
+ return barcodeEncoder.createBitmap(bitMatrix)
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt b/app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt
similarity index 96%
rename from app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt
rename to app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt
index 13ef461e0..961524b66 100644
--- a/app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt
@@ -1,4 +1,4 @@
-package com.geeksville.mesh.service
+package com.geeksville.mesh.model
import com.geeksville.android.Logging
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index cdfb805bc..fe4175e9b 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -12,9 +12,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.geeksville.android.Logging
-import com.geeksville.mesh.IMeshService
-import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.MyNodeInfo
+import com.geeksville.mesh.*
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
@@ -69,17 +67,6 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
}
companion object {
- /**
- * Return the current channel info
- * FIXME, we should sim channels at the MeshService level if we are running on an emulator,
- * for now I just fake it by returning a canned channel.
- */
- fun getChannel(c: MeshProtos.RadioConfig?): Channel? {
- val channel = c?.channelSettings?.let { Channel(it) }
-
- return channel
- }
-
fun getPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
}
@@ -100,13 +87,16 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
}
/// various radio settings (including the channel)
- val radioConfig = object : MutableLiveData(null) {
+ val radioConfig = object : MutableLiveData(null) {
+ }
+
+ val channels = object : MutableLiveData(null) {
}
var positionBroadcastSecs: Int?
get() {
radioConfig.value?.preferences?.let {
- if (it.locationShare == MeshProtos.LocationSharing.LocDisabled) return 0
+ if (it.locationShare == RadioConfigProtos.LocationSharing.LocDisabled) return 0
if (it.positionBroadcastSecs > 0) return it.positionBroadcastSecs
// These default values are borrowed from the device code.
if (it.isRouter) return 60 * 60
@@ -123,23 +113,18 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
builder.preferencesBuilder.gpsUpdateInterval = value
builder.preferencesBuilder.sendOwnerInterval = max(1, 3600 / value).toInt()
builder.preferencesBuilder.locationShare =
- MeshProtos.LocationSharing.LocEnabled
+ RadioConfigProtos.LocationSharing.LocEnabled
} else {
builder.preferencesBuilder.positionBroadcastSecs = Int.MAX_VALUE
builder.preferencesBuilder.locationShare =
- MeshProtos.LocationSharing.LocDisabled
+ RadioConfigProtos.LocationSharing.LocDisabled
}
setRadioConfig(builder.build())
}
}
var lsSleepSecs: Int?
- get() {
- radioConfig.value?.preferences?.let {
- return it.lsSecs
- }
- return null
- }
+ get() = radioConfig.value?.preferences?.lsSecs
set(value) {
val config = radioConfig.value
if (value != null && config != null) {
@@ -149,32 +134,48 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
}
}
- /// hardware info about our local device
- val myNodeInfo = object : MutableLiveData(null) {}
+ var region: RadioConfigProtos.RegionCode?
+ get() = radioConfig.value?.preferences?.region
+ set(value) {
+ val config = radioConfig.value
+ if (value != null && config != null) {
+ val builder = config.toBuilder()
+ builder.preferencesBuilder.region = value
+ setRadioConfig(builder.build())
+ }
+ }
+
+ /// hardware info about our local device (can be null)
+ val myNodeInfo = object : MutableLiveData(null) {}
override fun onCleared() {
super.onCleared()
debug("ViewModel cleared")
}
- /// Set the radio config (also updates our saved copy in preferences)
- fun setRadioConfig(c: MeshProtos.RadioConfig) {
+ /**
+ * Return the primary channel info
+ */
+ val primaryChannel: Channel? get() = channels.value?.primaryChannel
+
+ ///
+ // Set the radio config (also updates our saved copy in preferences)
+ private fun setRadioConfig(c: RadioConfigProtos.RadioConfig) {
debug("Setting new radio config!")
meshService?.radioConfig = c.toByteArray()
radioConfig.value =
c // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings)
-
- getPreferences(context).edit(commit = true) {
- this.putString("channel-url", getChannel(c)!!.getChannelUrl().toString())
- }
}
- /** Update just the channel settings portion of our config (both in the device and in saved preferences) */
- fun setChannel(c: MeshProtos.ChannelSettings) {
- // When running on the emulator, radio config might not really be available, in that case, just ignore attempts to change the config
- radioConfig.value?.toBuilder()?.let { config ->
- config.channelSettings = c
- setRadioConfig(config.build())
+ /// Set the radio config (also updates our saved copy in preferences)
+ fun setChannels(c: ChannelSet) {
+ debug("Setting new channels!")
+ meshService?.channels = c.protobuf.toByteArray()
+ channels.value =
+ c // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings)
+
+ getPreferences(context).edit(commit = true) {
+ this.putString("channel-url", c.getChannelUrl().toString())
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index 13b314737..5b61c8b55 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -23,6 +23,7 @@ import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
+import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted
import com.geeksville.util.*
import com.google.android.gms.common.api.ApiException
@@ -51,8 +52,8 @@ class MeshService : Service(), Logging {
/// Intents broadcast by MeshService
- @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"
+ /* @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"
@@ -110,11 +111,14 @@ class MeshService : Service(), Logging {
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private var connectionState = ConnectionState.DISCONNECTED
+
+ /// A database of received packets - used only for debug log
private var packetRepo: PacketRepository? = null
+
private var fusedLocationClient: FusedLocationProviderClient? = null
// If we've ever read a valid region code from our device it will be here
- var curRegionValue = MeshProtos.RegionCode.Unset_VALUE
+ var curRegionValue = RadioConfigProtos.RegionCode.Unset_VALUE
val radio = ServiceClient {
IRadioInterfaceService.Stub.asInterface(it).apply {
@@ -420,7 +424,9 @@ class MeshService : Service(), Logging {
var myNodeInfo: MyNodeInfo? = null
- private var radioConfig: MeshProtos.RadioConfig? = null
+ private var radioConfig: RadioConfigProtos.RadioConfig? = null
+
+ private var channels = arrayOf()
/// True after we've done our initial node db init
@Volatile
@@ -513,17 +519,45 @@ class MeshService : Service(), Logging {
/// My node ID string
private val myNodeID get() = toNodeID(myNodeNum)
+ /// Convert the channels array into a ChannelSet
+ private var channelSet: AppOnlyProtos.ChannelSet
+ get() {
+ val cs = channels.filter {
+ it.role != ChannelProtos.Channel.Role.DISABLED
+ }.map {
+ it.settings
+ }
+
+ return AppOnlyProtos.ChannelSet.newBuilder().apply {
+ addAllSettings(cs)
+ }.build()
+ }
+ set(value) {
+ val asChannels = value.settingsList.mapIndexed { i, c ->
+ ChannelProtos.Channel.newBuilder().apply {
+ role =
+ if (i == 0) ChannelProtos.Channel.Role.PRIMARY else ChannelProtos.Channel.Role.SECONDARY
+ index = i
+ settings = c
+ }.build()
+ }
+
+ debug("Sending channels to device")
+ asChannels.forEach {
+ setChannel(it)
+ }
+
+ channels = asChannels.toTypedArray()
+ }
+
/// Generate a new mesh packet builder with our node as the sender, and the specified node num
private fun newMeshPacketTo(idNum: Int) = MeshPacket.newBuilder().apply {
- val useShortAddresses = (myNodeInfo?.nodeNumBits ?: 8) != 32
-
if (myNodeInfo == null)
throw RadioNotConnectedException()
from = myNodeNum
- // We might need to change broadcast addresses to work with old device loads
- to = if (useShortAddresses && idNum == DataPacket.NODENUM_BROADCAST) 255 else idNum
+ to = idNum
}
/**
@@ -536,23 +570,44 @@ class MeshService : Service(), Logging {
/**
* Helper to make it easy to build a subpacket in the proper protobufs
- *
- * If destId is null we assume a broadcast message
*/
- private fun buildMeshPacket(
- destId: String,
+ private fun MeshProtos.MeshPacket.Builder.buildMeshPacket(
wantAck: Boolean = false,
- id: Int = 0,
+ id: Int = generatePacketId(), // always assign a packet ID if we didn't already have one
hopLimit: Int = 0,
- initFn: MeshProtos.SubPacket.Builder.() -> Unit
- ): MeshPacket = newMeshPacketTo(destId).apply {
+ priority: MeshPacket.Priority = MeshPacket.Priority.UNSET,
+ initFn: MeshProtos.Data.Builder.() -> Unit
+ ): MeshPacket {
this.wantAck = wantAck
this.id = id
this.hopLimit = hopLimit
- decoded = MeshProtos.SubPacket.newBuilder().also {
+ this.priority = priority
+ decoded = MeshProtos.Data.newBuilder().also {
initFn(it)
}.build()
- }.build()
+
+ return build()
+ }
+
+
+ /**
+ * Helper to make it easy to build a subpacket in the proper protobufs
+ */
+ private fun MeshProtos.MeshPacket.Builder.buildAdminPacket(
+ wantResponse: Boolean = false,
+ initFn: AdminProtos.AdminMessage.Builder.() -> Unit
+ ): MeshPacket = buildMeshPacket(
+ wantAck = true,
+ priority = MeshPacket.Priority.RELIABLE
+ )
+ {
+ this.wantResponse = wantResponse
+ portnumValue = Portnums.PortNum.ADMIN_APP_VALUE
+ payload = AdminProtos.AdminMessage.newBuilder().also {
+ initFn(it)
+ }.build().toByteString()
+ }
+
// FIXME - possible kotlin bug in 1.3.72 - it seems that if we start with the (globally shared) emptyList,
// then adding items are affecting that shared list rather than a copy. This was causing aliasing of
@@ -561,11 +616,11 @@ class MeshService : Service(), Logging {
/// Generate a DataPacket from a MeshPacket, or null if we didn't have enough data to do so
private fun toDataPacket(packet: MeshPacket): DataPacket? {
- return if (!packet.hasDecoded() || !packet.decoded.hasData()) {
+ return if (!packet.hasDecoded()) {
// We never convert packets that are not DataPackets
null
} else {
- val data = packet.decoded.data
+ val data = packet.decoded
val bytes = data.payload.toByteArray()
val fromId = toNodeID(packet.from)
val toId = toNodeID(packet.to)
@@ -598,15 +653,14 @@ class MeshService : Service(), Logging {
}
}
- /// Syntactic sugar to create data subpackets
- private fun makeData(portNum: Int, bytes: ByteString) = MeshProtos.Data.newBuilder().also {
- it.portnumValue = portNum
- it.payload = bytes
- }.build()
-
private fun toMeshPacket(p: DataPacket): MeshPacket {
- return buildMeshPacket(p.to!!, id = p.id, wantAck = true, hopLimit = p.hopLimit) {
- data = makeData(p.dataType, ByteString.copyFrom(p.bytes))
+ return newMeshPacketTo(p.to!!).buildMeshPacket(
+ id = p.id,
+ wantAck = true,
+ hopLimit = p.hopLimit
+ ) {
+ portnumValue = p.dataType
+ payload = ByteString.copyFrom(p.bytes)
}
}
@@ -631,62 +685,118 @@ class MeshService : Service(), Logging {
/// Update our model and resend as needed for a MeshPacket we just received from the radio
private fun handleReceivedData(packet: MeshPacket) {
myNodeInfo?.let { myInfo ->
- val data = packet.decoded.data
+ val data = packet.decoded
val bytes = data.payload.toByteArray()
val fromId = toNodeID(packet.from)
val dataPacket = toDataPacket(packet)
if (dataPacket != null) {
- if (myInfo.myNodeNum == packet.from) {
- // Handle position updates from the device
- if (data.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
- handleReceivedPosition(
- packet.from,
- MeshProtos.Position.parseFrom(data.payload),
- dataPacket.time
- )
- } else
- debug("Ignoring packet sent from our node, portnum=${data.portnumValue} ${bytes.size} bytes")
- } else {
- debug("Received data from $fromId, portnum=${data.portnumValue} ${bytes.size} bytes")
+ // We ignore most messages that we sent
+ val fromUs = myInfo.myNodeNum == packet.from
- dataPacket.status = MessageStatus.RECEIVED
- rememberDataPacket(dataPacket)
+ debug("Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes")
- when (data.portnumValue) {
- Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> {
+ dataPacket.status = MessageStatus.RECEIVED
+ rememberDataPacket(dataPacket)
+
+ // if (p.hasUser()) handleReceivedUser(fromNum, p.user)
+
+ /// We tell other apps about most message types, but some may have sensitve data, so that is not shared'
+ var shouldBroadcast = !fromUs
+
+ when (data.portnumValue) {
+ Portnums.PortNum.TEXT_MESSAGE_APP_VALUE ->
+ if (!fromUs) {
debug("Received CLEAR_TEXT from $fromId")
updateMessageNotification(dataPacket)
}
- // Handle new style position info
- Portnums.PortNum.POSITION_APP_VALUE -> {
- val u = MeshProtos.Position.parseFrom(data.payload)
- handleReceivedPosition(packet.from, u, dataPacket.time)
- }
+ // Handle new style position info
+ Portnums.PortNum.POSITION_APP_VALUE -> {
+ val u = MeshProtos.Position.parseFrom(data.payload)
+ handleReceivedPosition(packet.from, u, dataPacket.time)
+ }
- // Handle new style user info
- Portnums.PortNum.NODEINFO_APP_VALUE -> {
+ // Handle new style user info
+ Portnums.PortNum.NODEINFO_APP_VALUE ->
+ if (!fromUs) {
val u = MeshProtos.User.parseFrom(data.payload)
handleReceivedUser(packet.from, u)
}
+
+ // 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
+ val u = MeshProtos.Routing.parseFrom(data.payload)
+ if (u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE)
+ handleAckNak(true, data.requestId)
+ else
+ handleAckNak(false, data.requestId)
}
- // We always tell other apps when new data packets arrive
+ Portnums.PortNum.ADMIN_APP_VALUE -> {
+ val u = AdminProtos.AdminMessage.parseFrom(data.payload)
+ handleReceivedAdmin(packet.from, u)
+ shouldBroadcast = false
+ }
+
+ else ->
+ debug("No custom processing needed for ${data.portnumValue}")
+ }
+
+ // We always tell other apps when new data packets arrive
+ if (shouldBroadcast)
serviceBroadcasts.broadcastReceivedData(dataPacket)
- GeeksvilleApplication.analytics.track(
- "num_data_receive",
- DataPair(1)
- )
+ GeeksvilleApplication.analytics.track(
+ "num_data_receive",
+ DataPair(1)
+ )
- GeeksvilleApplication.analytics.track(
- "data_receive",
- DataPair("num_bytes", bytes.size),
- DataPair("type", data.portnumValue)
- )
+ GeeksvilleApplication.analytics.track(
+ "data_receive",
+ DataPair("num_bytes", bytes.size),
+ DataPair("type", data.portnumValue)
+ )
+ }
+ }
+ }
+
+ private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage) {
+ // For the time being we only care about admin messages from our local node
+ if (fromNodeNum == myNodeNum) {
+ when (a.variantCase) {
+ AdminProtos.AdminMessage.VariantCase.GET_RADIO_RESPONSE -> {
+ debug("Admin: received radioConfig")
+ radioConfig = a.getRadioResponse
+ requestChannel(0) // Now start reading channels
}
+
+ AdminProtos.AdminMessage.VariantCase.GET_CHANNEL_RESPONSE -> {
+ val mi = myNodeInfo
+ if (mi != null) {
+ val ch = a.getChannelResponse
+ channels[ch.index] = ch
+ debug("Admin: Received channel ${ch.index}")
+ if (ch.index + 1 < mi.maxChannels) {
+ if(ch.hasSettings()) {
+ // Not done yet, request next channel
+ requestChannel(ch.index + 1)
+ }
+ else {
+ debug("We've received the primary channel, allowing rest of app to start...")
+ onHasSettings()
+ }
+ } else {
+ debug("Received max channels, starting app")
+ onHasSettings()
+ }
+ }
+ }
+ else ->
+ warn("No special processing needed for ${a.variantCase}")
+
}
}
}
@@ -705,7 +815,7 @@ class MeshService : Service(), Logging {
/** Update our DB of users based on someone sending out a Position subpacket
* @param defaultTime in msecs since 1970
- */
+ */
private fun handleReceivedPosition(
fromNum: Int,
p: MeshProtos.Position,
@@ -785,45 +895,33 @@ class MeshService : Service(), Logging {
//val toNum = packet.to
// debug("Recieved: $packet")
- val p = packet.decoded
+ if (packet.hasDecoded()) {
+ val packetToSave = Packet(
+ UUID.randomUUID().toString(),
+ "packet",
+ System.currentTimeMillis(),
+ packet.toString()
+ )
+ insertPacket(packetToSave)
- val packetToSave = Packet(
- UUID.randomUUID().toString(),
- "packet",
- System.currentTimeMillis(),
- packet.toString()
- )
- insertPacket(packetToSave)
+ // 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
- // 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())
+ }
- updateNodeInfo(myNodeNum) {
- it.position = it.position?.copy(time = currentSecond())
- }
+ // if (p.hasPosition()) handleReceivedPosition(fromNum, p.position, rxTime)
- // 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 (p.hasPosition()) {
- handleReceivedPosition(fromNum, p.position, rxTime.toLong() * 1000)
- }
- else
+ // 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)
}
- if (p.hasData())
handleReceivedData(packet)
-
- if (p.hasUser())
- handleReceivedUser(fromNum, p.user)
-
- if (p.successId != 0)
- handleAckNak(true, p.successId)
-
- if (p.failId != 0)
- handleAckNak(false, p.failId)
+ }
}
private fun insertPacket(packetToSave: Packet) {
@@ -1043,7 +1141,7 @@ class MeshService : Service(), Logging {
MeshProtos.FromRadio.NODE_INFO_FIELD_NUMBER -> handleNodeInfo(proto.nodeInfo)
- MeshProtos.FromRadio.RADIO_FIELD_NUMBER -> handleRadioConfig(proto.radio)
+ // MeshProtos.FromRadio.RADIO_FIELD_NUMBER -> handleRadioConfig(proto.radio)
else -> errormsg("Unexpected FromRadio variant")
}
@@ -1067,7 +1165,7 @@ class MeshService : Service(), Logging {
private var configNonce = 1
- private fun handleRadioConfig(radio: MeshProtos.RadioConfig) {
+ private fun handleRadioConfig(radio: RadioConfigProtos.RadioConfig) {
val packetToSave = Packet(
UUID.randomUUID().toString(),
"RadioConfig",
@@ -1137,7 +1235,6 @@ class MeshService : Service(), Logging {
MyNodeInfo(
myNodeNum,
hasGps,
- region,
hwModel,
firmwareVersion,
firmwareUpdateFilename != null,
@@ -1146,18 +1243,27 @@ class MeshService : Service(), Logging {
DeviceVersion(firmwareVersion)
),
currentPacketId.toLong() and 0xffffffffL,
- if (nodeNumBits == 0) 8 else nodeNumBits,
- if (packetIdBits == 0) 8 else packetIdBits,
if (messageTimeoutMsec == 0) 5 * 60 * 1000 else messageTimeoutMsec, // constants from current device code
- minAppVersion
+ minAppVersion,
+ maxChannels
)
}
newMyNodeInfo = mi
+ // 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) {
+ 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("region", mi.region),
DataPair("firmware", mi.firmwareVersion),
DataPair("has_gps", mi.hasGPS),
DataPair("hw_model", mi.model),
@@ -1172,8 +1278,8 @@ class MeshService : Service(), Logging {
// 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)
+ DataPair("hw_model", mi.model)
+ // DataPair("region", mi.region)
)
}
}
@@ -1190,28 +1296,30 @@ class MeshService : Service(), Logging {
ignoreException {
// Try to pull our region code from the new preferences field
// FIXME - do not check net - figuring out why board is rebooting
- val curConfigRegion = radioConfig?.preferences?.region ?: MeshProtos.RegionCode.Unset
- if (curConfigRegion != MeshProtos.RegionCode.Unset) {
+ val curConfigRegion =
+ radioConfig?.preferences?.region ?: RadioConfigProtos.RegionCode.Unset
+ if (curConfigRegion != RadioConfigProtos.RegionCode.Unset) {
info("Using device region $curConfigRegion (code ${curConfigRegion.number})")
curRegionValue = curConfigRegion.number
}
- if (curRegionValue == MeshProtos.RegionCode.Unset_VALUE) {
- // look for a legacy region
+ if (curRegionValue == RadioConfigProtos.RegionCode.Unset_VALUE) {
+ TODO("Need gui for setting region")
+ /* // look for a legacy region
val legacyRegex = Regex(".+-(.+)")
myNodeInfo?.region?.let { legacyRegion ->
val matches = legacyRegex.find(legacyRegion)
if (matches != null) {
val (region) = matches.destructured
- val newRegion = MeshProtos.RegionCode.valueOf(region)
+ 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 == MeshProtos.RegionCode.Unset && curRegionValue != MeshProtos.RegionCode.Unset_VALUE) {
+ if (curConfigRegion == RadioConfigProtos.RegionCode.Unset && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE) {
info("Telling device to upgrade region")
// Tell the device to set the new region field (old devices will simply ignore this)
@@ -1228,6 +1336,19 @@ class MeshService : Service(), Logging {
}
}
+ /// 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
+
+ // broadcast an intent with our new connection state
+ serviceBroadcasts.broadcastConnection()
+ onNodeDBChanged()
+ reportConnection()
+
+ updateRegion()
+ }
+
private fun handleConfigComplete(configCompleteId: Int) {
if (configCompleteId == configNonce) {
@@ -1251,19 +1372,30 @@ class MeshService : Service(), Logging {
newNodes.clear() // Just to save RAM ;-)
haveNodeDB = true // we now have nodes from real hardware
- processEarlyPackets() // send receive any packets that were queued up
-
- // broadcast an intent with our new connection state
- serviceBroadcasts.broadcastConnection()
- onNodeDBChanged()
- reportConnection()
-
- updateRegion()
+ requestRadioConfig()
}
} else
warn("Ignoring stale config complete")
}
+ private fun requestRadioConfig() {
+ sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) {
+ getRadioRequest = true
+ })
+ }
+
+ private fun requestChannel(channelIndex: Int) {
+ sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) {
+ getChannelRequest = channelIndex + 1
+ })
+ }
+
+ private fun setChannel(channel: ChannelProtos.Channel) {
+ sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) {
+ setChannel = channel
+ })
+ }
+
/**
* Start the modern (REV2) API configuration flow
*/
@@ -1299,30 +1431,20 @@ class MeshService : Service(), Logging {
it.time = currentSecond() // Include our current timestamp
}.build()
- // encapsulate our payload in the proper protobufs and fire it off
- val packet = newMeshPacketTo(destNum)
-
- packet.decoded = MeshProtos.SubPacket.newBuilder().also {
- val isNewPositionAPI =
- deviceVersion >= DeviceVersion("1.20.0") // We changed position APIs with this version
- if (isNewPositionAPI) {
- // Use the new position as data format
- it.data = makeData(Portnums.PortNum.POSITION_APP_VALUE, position.toByteString())
- } else {
- // Send the old dedicated position subpacket
- it.position = position
- }
- it.wantResponse = wantResponse
- }.build()
-
- // Assume our position packets are not critical
- packet.priority = MeshProtos.MeshPacket.Priority.BACKGROUND
-
// 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(packet.build())
+ sendToRadio(fullPacket)
}
private fun sendPositionScoped(
@@ -1341,10 +1463,10 @@ class MeshService : Service(), Logging {
/** Send our current radio config to the device
*/
- private fun sendRadioConfig(c: MeshProtos.RadioConfig) {
- // Update our device
- sendToRadio(ToRadio.newBuilder().apply {
- this.setRadio = c
+ private fun sendRadioConfig(c: RadioConfigProtos.RadioConfig) {
+ // send the packet into the mesh
+ sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
+ setRadio = c
})
// Update our cached copy
@@ -1354,7 +1476,7 @@ class MeshService : Service(), Logging {
/** Set our radio config
*/
private fun setRadioConfig(payload: ByteArray) {
- val parsed = MeshProtos.RadioConfig.parseFrom(payload)
+ val parsed = RadioConfigProtos.RadioConfig.parseFrom(payload)
sendRadioConfig(parsed)
}
@@ -1384,47 +1506,36 @@ class MeshService : Service(), Logging {
handleReceivedUser(myNode.myNodeNum, user)
- // set my owner info
- sendToRadio(ToRadio.newBuilder().apply {
- this.setOwner = user
- })
+ // encapsulate our payload in the proper protobufs and fire it off
+ val packet = newMeshPacketTo(myNodeNum).buildAdminPacket {
+ setOwner = user
+ }
+
+ // send the packet into the mesh
+ sendToRadio(packet)
}
} else
throw Exception("Can't set user without a node info") // this shouldn't happen
}
+
/// Do not use directly, instead call generatePacketId()
- private var currentPacketId = 0L
+ private var currentPacketId = Random(System.currentTimeMillis()).nextLong().absoluteValue
/**
* Generate a unique packet ID (if we know enough to do so - otherwise return 0 so the device will do it)
*/
+ @Synchronized
private fun generatePacketId(): Int {
+ val numPacketIds =
+ ((1L shl 32) - 1).toLong() // A mask for only the valid packet ID bits, either 255 or maxint
- myNodeInfo?.let {
- val numPacketIds =
- ((1L shl it.packetIdBits) - 1).toLong() // A mask for only the valid packet ID bits, either 255 or maxint
+ currentPacketId++
- if (currentPacketId == 0L) {
- logAssert(it.packetIdBits == 8 || it.packetIdBits == 32) // Only values I'm expecting (though we don't require this)
+ currentPacketId = currentPacketId and 0xffffffff // keep from exceeding 32 bits
- // We now always pick a random initial packet id (odds of collision with the device is insanely low with 32 bit ids)
- val random = Random(System.currentTimeMillis())
- val devicePacketId = random.nextLong().absoluteValue
-
- // Not inited - pick a number on the opposite side of what the device is using
- currentPacketId = devicePacketId + numPacketIds / 2
- } else {
- currentPacketId++
- }
-
- currentPacketId = currentPacketId and 0xffffffff // keep from exceeding 32 bits
-
- // Use modulus and +1 to ensure we skip 0 on any values we return
- return ((currentPacketId % numPacketIds) + 1L).toInt()
- }
-
- return 0 // We don't have mynodeinfo yet, so just let the radio eventually assign an ID
+ // Use modulus and +1 to ensure we skip 0 on any values we return
+ return ((currentPacketId % numPacketIds) + 1L).toInt()
}
var firmwareUpdateFilename: UpdateFilenames? = null
@@ -1527,9 +1638,7 @@ class MeshService : Service(), Logging {
doFirmwareUpdate()
}
- override fun getMyNodeInfo(): MyNodeInfo = toRemoteExceptions {
- this@MeshService.myNodeInfo ?: throw RadioNotConnectedException("No MyNodeInfo")
- }
+ override fun getMyNodeInfo(): MyNodeInfo? = this@MeshService.myNodeInfo
override fun getMyId() = toRemoteExceptions { myNodeID }
@@ -1600,6 +1709,15 @@ class MeshService : Service(), Logging {
this@MeshService.setRadioConfig(payload)
}
+ override fun getChannels(): ByteArray = toRemoteExceptions {
+ channelSet.toByteArray()
+ }
+
+ override fun setChannels(payload: ByteArray?) {
+ val parsed = AppOnlyProtos.ChannelSet.parseFrom(payload)
+ channelSet = parsed
+ }
+
override fun getNodes(): MutableList = toRemoteExceptions {
val r = nodeDBbyID.values.toMutableList()
info("in getOnline, count=${r.size}")
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
index 831ab1bd2..6d782269e 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
@@ -20,10 +20,12 @@ class MeshServiceBroadcasts(
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
// newer packets (that have a non zero portnum) are only broadcast using the standard mechanism.
if(payload.dataType == Portnums.PortNum.UNKNOWN_APP_VALUE)
explicitBroadcast(Intent(MeshService.ACTION_RECEIVED_DATA).putExtra(EXTRA_PAYLOAD, payload))
+ */
}
fun broadcastNodeChange(info: NodeInfo) {
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt
index 2b6e455a4..c1477b718 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt
@@ -1,9 +1,6 @@
package com.geeksville.mesh.service
-import com.geeksville.mesh.DataPacket
-import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.MyNodeInfo
-import com.geeksville.mesh.NodeInfo
+import com.geeksville.mesh.*
import kotlinx.serialization.Serializable
/// Our saved preferences as stored on disk
@@ -12,7 +9,7 @@ data class MeshServiceSettingsData(
val nodeDB: Array,
val myInfo: MyNodeInfo,
val messages: Array,
- val regionCode: Int = MeshProtos.RegionCode.Unset_VALUE
+ val regionCode: Int = RadioConfigProtos.RegionCode.Unset_VALUE
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
diff --git a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt
index 5eaf012c6..7c76458e5 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt
@@ -26,13 +26,47 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi
override fun handleSendToRadio(p: ByteArray) {
val pr = MeshProtos.ToRadio.parseFrom(p)
+ val data = if (pr.hasPacket()) pr.packet.decoded else null
+
when {
pr.wantConfigId != 0 -> sendConfigResponse(pr.wantConfigId)
+ data != null && data.portnum == Portnums.PortNum.ADMIN_APP -> handleAdminPacket(
+ pr,
+ AdminProtos.AdminMessage.parseFrom(data.payload)
+ )
pr.hasPacket() && pr.packet.wantAck -> sendFakeAck(pr)
else -> info("Ignoring data sent to mock interface $pr")
}
}
+ private fun handleAdminPacket(pr: MeshProtos.ToRadio, d: AdminProtos.AdminMessage) {
+ when {
+ d.getRadioRequest ->
+ sendAdmin(pr.packet.to, pr.packet.from, pr.packet.id) {
+ getRadioResponse = RadioConfigProtos.RadioConfig.newBuilder().apply {
+
+ preferences =
+ RadioConfigProtos.RadioConfig.UserPreferences.newBuilder().apply {
+ region = RadioConfigProtos.RegionCode.TW
+ // FIXME set critical times?
+ }.build()
+ }.build()
+ }
+
+ d.getChannelRequest != 0 ->
+ sendAdmin(pr.packet.to, pr.packet.from, pr.packet.id) {
+ getChannelResponse = ChannelProtos.Channel.newBuilder().apply {
+ index = d.getChannelRequest - 1 // 0 based on the response
+ role =
+ if (d.getChannelRequest == 1) ChannelProtos.Channel.Role.PRIMARY else ChannelProtos.Channel.Role.DISABLED
+ }.build()
+ }
+
+ else ->
+ info("Ignoring admin sent to mock interface $d")
+ }
+ }
+
override fun close() {
info("Closing the mock interface")
}
@@ -46,17 +80,14 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi
to = 0xffffffff.toInt() // ugly way of saying broadcast
rxTime = (System.currentTimeMillis() / 1000).toInt()
rxSnr = 1.5f
- decoded = MeshProtos.SubPacket.newBuilder().apply {
- data = MeshProtos.Data.newBuilder().apply {
- portnum = Portnums.PortNum.TEXT_MESSAGE_APP
- payload = ByteString.copyFromUtf8("This simulated node sends Hi!")
- }.build()
+ decoded = MeshProtos.Data.newBuilder().apply {
+ portnum = Portnums.PortNum.TEXT_MESSAGE_APP
+ payload = ByteString.copyFromUtf8("This simulated node sends Hi!")
}.build()
}.build()
}
-
- private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) =
+ private fun makeDataPacket(fromIn: Int, toIn: Int, data: MeshProtos.Data.Builder) =
MeshProtos.FromRadio.newBuilder().apply {
packet = MeshProtos.MeshPacket.newBuilder().apply {
id = messageNumSequence.next()
@@ -64,17 +95,39 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi
to = toIn
rxTime = (System.currentTimeMillis() / 1000).toInt()
rxSnr = 1.5f
- decoded = MeshProtos.SubPacket.newBuilder().apply {
- data = MeshProtos.Data.newBuilder().apply {
- successId = msgId
- }.build()
- }.build()
+ decoded = data.build()
}.build()
}
+ private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) =
+ makeDataPacket(fromIn, toIn, MeshProtos.Data.newBuilder().apply {
+ portnum = Portnums.PortNum.ROUTING_APP
+ payload = MeshProtos.Routing.newBuilder().apply {
+ }.build().toByteString()
+ requestId = msgId
+ })
+
+ private fun sendAdmin(
+ fromIn: Int,
+ toIn: Int,
+ reqId: Int,
+ initFn: AdminProtos.AdminMessage.Builder.() -> Unit
+ ) {
+ val p = makeDataPacket(fromIn, toIn, MeshProtos.Data.newBuilder().apply {
+ portnum = Portnums.PortNum.ADMIN_APP
+ payload = AdminProtos.AdminMessage.newBuilder().also {
+ initFn(it)
+ }.build().toByteString()
+ requestId = reqId
+ })
+ service.handleFromRadio(p.build().toByteArray())
+ }
+
/// Send a fake ack packet back if the sender asked for want_ack
private fun sendFakeAck(pr: MeshProtos.ToRadio) {
- service.handleFromRadio(makeAck(pr.packet.to, pr.packet.from, pr.packet.id).build().toByteArray())
+ service.handleFromRadio(
+ makeAck(pr.packet.to, pr.packet.from, pr.packet.id).build().toByteArray()
+ )
}
private fun sendConfigResponse(configId: Int) {
@@ -101,35 +154,17 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi
}
// Simulated network data to feed to our app
- val MY_NODE = 0x42424242
+ val MY_NODE = 0x42424242
val packets = arrayOf(
// MyNodeInfo
MeshProtos.FromRadio.newBuilder().apply {
myInfo = MeshProtos.MyNodeInfo.newBuilder().apply {
myNodeNum = MY_NODE
- region = "TW"
- numChannels = 7
hwModel = "Sim"
- packetIdBits = 32
- nodeNumBits = 32
- currentPacketId = 1
messageTimeoutMsec = 5 * 60 * 1000
firmwareVersion = service.getString(R.string.cur_firmware_version)
- }.build()
- },
-
- // RadioConfig
- MeshProtos.FromRadio.newBuilder().apply {
- radio = MeshProtos.RadioConfig.newBuilder().apply {
-
- preferences = MeshProtos.RadioConfig.UserPreferences.newBuilder().apply {
- region = MeshProtos.RegionCode.TW
- // FIXME set critical times?
- }.build()
-
- channel = MeshProtos.ChannelSettings.newBuilder().apply {
- // we just have an empty listing so that the default channel works
- }.build()
+ numBands = 13
+ maxChannels = 8
}.build()
},
diff --git a/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt b/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt
deleted file mode 100644
index 36b87b40d..000000000
--- a/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.geeksville.mesh
-
-import android.content.Context
-import com.geeksville.mesh.service.RadioInterfaceService
-
-class SimRadio(private val context: Context) {
-
- /**
- * When simulating we parse these MeshPackets as if they arrived at startup
- * Send broadcast them after we receive a ToRadio.WantNodes message.
- *
- * Our fake net has three nodes
- *
- * +16508675309, nodenum 9 - our node
- * +16508675310, nodenum 10 - some other node, name Bob One/BO
- * (eventually) +16508675311, nodenum 11 - some other node
- */
- private val simInitPackets =
- arrayOf(
- """ { "from": 10, "to": 9, "payload": { "user": { "id": "+16508675310", "longName": "Bob One", "shortName": "BO" }}} """,
- """ { "from": 10, "to": 9, "payload": { "data": { "payload": "aGVsbG8gd29ybGQ=", "typ": 0 }}} """, // SIGNAL_OPAQUE
- """ { "from": 10, "to": 9, "payload": { "data": { "payload": "aGVsbG8gd29ybGQ=", "typ": 1 }}} """, // CLEAR_TEXT
- """ { "from": 10, "to": 9, "payload": { "data": { "payload": "", "typ": 2 }}} """ // CLEAR_READACK
- )
-
- fun start() {
- // FIXME, do this sim startup elsewhere, because waiting for a packet from MeshService
- // isn't right, because that service can't successfully send radio packets until it knows
- // our node num
- // Instead a separate sim radio thing can come in at startup and force these broadcasts to happen
- // at the right time
- // Send a fake my_node_num response
- /* FIXME - change to use new radio info message
- RadioInterfaceService.broadcastReceivedFromRadio(
- context,
- MeshProtos.FromRadio.newBuilder().apply {
- myNodeNum = 9
- }.build().toByteArray()
- ) */
-
- simInitPackets.forEach { _ ->
- val fromRadio = MeshProtos.FromRadio.newBuilder().apply {
- packet = MeshProtos.MeshPacket.newBuilder().apply {
- // jsonParser.merge(json, this)
- }.build()
- }.build()
-
- RadioInterfaceService.broadcastReceivedFromRadio(context, fromRadio.toByteArray())
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
index d7b3cc2dd..b9050105e 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
@@ -9,6 +9,7 @@ import androidx.core.app.JobIntentService
import com.geeksville.android.Logging
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
+import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.util.exceptionReporter
import java.util.*
import java.util.zip.CRC32
diff --git a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt
index 75175a14e..6b22b7e6a 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt
@@ -51,7 +51,7 @@ class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging {
val textEdit = binding.positionBroadcastPeriodEditText
val n = textEdit.text.toString().toIntOrNull()
val minBroadcastPeriodSecs =
- ChannelOption.fromConfig(model.radioConfig.value?.channelSettings?.modemConfig)?.minBroadcastPeriodSecs
+ ChannelOption.fromConfig(model.primaryChannel?.modemConfig)?.minBroadcastPeriodSecs
?: ChannelOption.defaultMinBroadcastPeriod
if (n != null && n < MAX_INT_DEVICE && (n == 0 || n >= minBroadcastPeriodSecs)) {
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
index ecc3cfe76..d4c5e8303 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -16,11 +16,14 @@ import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
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
import com.geeksville.mesh.model.ChannelOption
+import com.geeksville.mesh.model.ChannelSet
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -75,11 +78,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
/// Pull the latest data from the model (discarding any user edits)
private fun setGUIfromModel() {
- val radioConfig = model.radioConfig.value
- val channel = UIViewModel.getChannel(radioConfig)
+ val channels = model.channels.value
binding.editableCheckbox.isChecked = false // start locked
- if (channel != null) {
+ if (channels != null) {
+ val channel = channels.primaryChannel
+
binding.qrView.visibility = View.VISIBLE
binding.channelNameEdit.visibility = View.VISIBLE
binding.channelNameEdit.setText(channel.humanName)
@@ -89,9 +93,9 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
binding.editableCheckbox.isEnabled = connected
- binding.qrView.setImageBitmap(channel.getChannelQR())
+ binding.qrView.setImageBitmap(channels.getChannelQR())
- val modemConfig = radioConfig?.channelSettings?.modemConfig
+ val modemConfig = channel.modemConfig
val channelOption = ChannelOption.fromConfig(modemConfig)
binding.filledExposedDropdown.setText(
getString(
@@ -118,7 +122,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
}
private fun shareChannel() {
- UIViewModel.getChannel(model.radioConfig.value)?.let { channel ->
+ model.channels.value?.let { channels ->
GeeksvilleApplication.analytics.track(
"share",
@@ -127,7 +131,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
- putExtra(Intent.EXTRA_TEXT, channel.getChannelUrl().toString())
+ putExtra(Intent.EXTRA_TEXT, channels.getChannelUrl().toString())
putExtra(
Intent.EXTRA_TITLE,
getString(R.string.url_for_join)
@@ -152,8 +156,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
- UIViewModel.getChannel(model.radioConfig.value)?.let { channel ->
- binding.channelNameEdit.setText(channel.name)
+ model.channels.value?.let { channels ->
+ binding.channelNameEdit.setText(channels.primaryChannel.name)
}
} else {
// User just locked it, we should warn and then apply changes to radio
@@ -165,8 +169,9 @@ 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
- UIViewModel.getChannel(model.radioConfig.value)?.let { old ->
- val newSettings = old.settings.toBuilder()
+ model.channels.value?.let { old ->
+ val oldPrimary = old.primaryChannel
+ val newSettings = oldPrimary.settings.toBuilder()
newSettings.name = binding.channelNameEdit.text.toString().trim()
// Generate a new AES256 key (for any channel not named Default)
@@ -191,11 +196,14 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
binding.filledExposedDropdown.editableText.toString()
val modemConfig = getModemConfig(selectedChannelOptionString)
- if (modemConfig != MeshProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
+ 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.setChannel(newSettings.build())
+ 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)
@@ -222,7 +230,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
shareChannel()
}
- model.radioConfig.observe(viewLifecycleOwner, {
+ model.channels.observe(viewLifecycleOwner, {
setGUIfromModel()
})
@@ -232,12 +240,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
})
}
- private fun getModemConfig(selectedChannelOptionString: String): MeshProtos.ChannelSettings.ModemConfig {
+ private fun getModemConfig(selectedChannelOptionString: String): ChannelProtos.ChannelSettings.ModemConfig {
for (item in ChannelOption.values()) {
if (getString(item.configRes) == selectedChannelOptionString)
return item.modemConfig
}
- return MeshProtos.ChannelSettings.ModemConfig.UNRECOGNIZED
+ return ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
index 92c1d0d73..43553a20d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
@@ -9,6 +9,7 @@ import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
+import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
@@ -151,11 +152,11 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
if (isMe) {
marginParams.leftMargin = messageOffset
marginParams.rightMargin = 0
- holder.card.setCardBackgroundColor(resources.getColor(R.color.colorMyMsg))
+ context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg)) }
} else {
marginParams.rightMargin = messageOffset
marginParams.leftMargin = 0
- holder.card.setCardBackgroundColor(resources.getColor(R.color.colorMsg))
+ context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg)) }
}
// Hide the username chip for my messages
if (isMe) {
@@ -237,17 +238,22 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
})
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
- model.isConnected.observe(viewLifecycleOwner, Observer { connected ->
- // If we don't know our node ID and we are offline don't let user try to send
+ fun updateTextEnabled() {
binding.textInputLayout.isEnabled =
- connected != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null
- })
+ model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null && model.radioConfig.value != null
+ }
- model.nodeDB.myId.observe(viewLifecycleOwner, Observer { myId ->
+ model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
// If we don't know our node ID and we are offline don't let user try to send
- binding.textInputLayout.isEnabled =
- model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && myId != null
- })
+ updateTextEnabled() })
+
+ 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() })
+
+ 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() })
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
index 06eb73658..ae01f6808 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -19,6 +19,8 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
import android.widget.RadioButton
import android.widget.Toast
import androidx.fragment.app.activityViewModels
@@ -31,6 +33,7 @@ import com.geeksville.android.hideKeyboard
import com.geeksville.android.isGooglePlayAvailable
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
+import com.geeksville.mesh.RadioConfigProtos
import com.geeksville.mesh.android.bluetoothManager
import com.geeksville.mesh.android.usbManager
import com.geeksville.mesh.databinding.SettingsFragmentBinding
@@ -562,29 +565,95 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
- private fun initNodeInfo() {
+ /**
+ * Pull the latest device info from the model and into the GUI
+ */
+ private fun updateNodeInfo() {
val connected = model.isConnected.value
- refreshUpdateButton()
+ val isConnected = connected == MeshService.ConnectionState.CONNECTED
+ binding.nodeSettings.visibility = if(isConnected) View.VISIBLE else View.GONE
+
+ if (connected == MeshService.ConnectionState.DISCONNECTED)
+ model.ownerName.value = ""
+
+ // update the region selection from the device
+ val region = model.region
+ val spinner = binding.regionSpinner
+ val unsetIndex = regions.indexOf(RadioConfigProtos.RegionCode.Unset.name)
+ spinner.onItemSelectedListener = null
+ if(region != null) {
+ 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 {
+ spinner.setSelection(unsetIndex, false)
+ spinner.isEnabled = false // leave disabled, because we can't get our region
+ }
// If actively connected possibly let the user update firmware
- val info = model.myNodeInfo.value
+ refreshUpdateButton()
- when (connected) {
- MeshService.ConnectionState.CONNECTED -> {
+ // Update the status string (highest priority messages first)
+ val info = model.myNodeInfo.value
+ val statusText = binding.scanStatusText
+ when {
+ region == RadioConfigProtos.RegionCode.Unset ->
+ statusText.text = getString(R.string.must_set_region)
+
+ connected == MeshService.ConnectionState.CONNECTED -> {
val fwStr = info?.firmwareString ?: ""
- binding.scanStatusText.text = getString(R.string.connected_to).format(fwStr)
+ statusText.text = getString(R.string.connected_to).format(fwStr)
}
- MeshService.ConnectionState.DISCONNECTED ->
- binding.scanStatusText.text = getString(R.string.not_connected)
- MeshService.ConnectionState.DEVICE_SLEEP ->
- binding.scanStatusText.text = getString(R.string.connected_sleeping)
+ connected == MeshService.ConnectionState.DISCONNECTED ->
+ statusText.text = getString(R.string.not_connected)
+ connected == MeshService.ConnectionState.DEVICE_SLEEP ->
+ statusText.text = getString(R.string.connected_sleeping)
}
}
+ 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 asProto = item!!.let { RadioConfigProtos.RegionCode.valueOf(it) }
+ exceptionToSnackbar(requireView()) {
+ model.region = asProto
+ }
+ updateNodeInfo() // We might have just changed Unset to set
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>) {
+ //TODO("Not yet implemented")
+ }
+ }
+
+ /// the sorted list of region names like arrayOf("US", "CN", "EU488")
+ private val regions = RadioConfigProtos.RegionCode.values().filter {
+ it != RadioConfigProtos.RegionCode.UNRECOGNIZED
+ }.map {
+ it.name
+ }.sorted()
+
/// Setup the ui widgets unrelated to BLE scanning
private fun initCommonUI() {
+ // init our region spinner
+ val spinner = binding.regionSpinner
+ val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
+ regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+ spinner.adapter = regionAdapter
+
model.ownerName.observe(viewLifecycleOwner, { name ->
binding.usernameEditText.setText(name)
})
@@ -592,18 +661,12 @@ 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 ->
- val connected = connectionState == MeshService.ConnectionState.CONNECTED
- binding.usernameView.isEnabled = connected
-
- if (connectionState == MeshService.ConnectionState.DISCONNECTED)
- model.ownerName.value = ""
-
- initNodeInfo()
+ updateNodeInfo()
})
// Also watch myNodeInfo because it might change later
model.myNodeInfo.observe(viewLifecycleOwner, Observer {
- initNodeInfo()
+ updateNodeInfo()
})
binding.updateFirmwareButton.setOnClickListener {
diff --git a/app/src/main/proto b/app/src/main/proto
index 512d1aca0..7de496ffe 160000
--- a/app/src/main/proto
+++ b/app/src/main/proto
@@ -1 +1 @@
-Subproject commit 512d1aca0a066107de749c0c47397c7f9bf9cb99
+Subproject commit 7de496ffe941f88e9d99c2ef2c7bc01f79efe11e
diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml
index d5f5103d9..352fab9a0 100644
--- a/app/src/main/res/layout/settings_fragment.xml
+++ b/app/src/main/res/layout/settings_fragment.xml
@@ -10,14 +10,14 @@
+ android:layout_height="0dp"
+ android:layout_marginTop="16dp">
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/nodeSettings" />
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/protobufs.xml b/app/src/main/res/values/protobufs.xml
new file mode 100644
index 000000000..190471fc4
--- /dev/null
+++ b/app/src/main/res/values/protobufs.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ - Unset
+ - US
+ - EU433
+ - EU865
+ - CN
+ - JP
+ - ANZ
+ - KR
+ - TW
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 33c6f745f..b953889e2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -89,4 +89,9 @@
Minimum broadcast period for this channel is %d
Protocol stress test
Advanced settings
+ Firmware update required
+ The radio firmware is too old to talk to this application, please go to the settings pane and choose "Update Firmware". For more information on this see our wiki.
+ Okay
+ You must set a region!
+ Region
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 691680617..e36ac68c6 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -13,6 +13,20 @@
- @style/menu_item_color
+
+
+
+
+
diff --git a/geeksville-androidlib b/geeksville-androidlib
index ff93b088b..9b15752bb 160000
--- a/geeksville-androidlib
+++ b/geeksville-androidlib
@@ -1 +1 @@
-Subproject commit ff93b088b4652f099ab99c0359388f2d0541ddc9
+Subproject commit 9b15752bb0ce1fa383a9f4b65e204d4dd0afb03b