diff --git a/app/build.gradle b/app/build.gradle
index 6d4c9c306..8292c32a8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -25,13 +25,14 @@ android {
}
} */
compileSdkVersion 29
- buildToolsVersion "30.0.2" // Note: 30.0.0.2 doesn't yet work on Github actions CI
+ // leave undefined to use version plugin wants
+ // buildToolsVersion "30.0.2" // Note: 30.0.2 doesn't yet work on Github actions CI
defaultConfig {
applicationId "com.geeksville.mesh"
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
targetSdkVersion 29
- versionCode 20122 // format is Mmmss (where M is 1+the numeric major number
- versionName "1.1.22"
+ versionCode 20133 // format is Mmmss (where M is 1+the numeric major number
+ versionName "1.1.33"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -107,7 +108,7 @@ protobuf {
dependencies {
- def room_version = "2.2.5"
+ def room_version = '2.2.6'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 69e08be69..8bee48f88 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -144,6 +144,10 @@
android:scheme="https"
android:host="www.meshtastic.org"
android:pathPrefix="/c/" />
+
diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
index a1afe5edd..f2fa418e6 100644
--- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
+++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt
@@ -27,20 +27,27 @@ class MeshUtilApplication : GeeksvilleApplication() {
val pref = AppPrefs(this)
crashlytics.setUserId(pref.getInstallId()) // be able to group all bugs per anonymous user
+ // We always send our log messages to the crashlytics lib, but they only get sent to the server if we report an exception
+ // This makes log messages work properly if someone turns on analytics just before they click report bug.
+ // send all log messages through crashyltics, so if we do crash we'll have those in the report
+ val standardLogger = Logging.printlog
+ Logging.printlog = { level, tag, message ->
+ crashlytics.log("$tag: $message")
+ standardLogger(level, tag, message)
+ }
+
+ fun sendCrashReports() {
+ if(isAnalyticsAllowed)
+ crashlytics.sendUnsentReports()
+ }
+
+ // Send any old reports if user approves
+ sendCrashReports()
+
// Attach to our exception wrapper
Exceptions.reporter = { exception, _, _ ->
crashlytics.recordException(exception)
- crashlytics.sendUnsentReports()
- }
-
- if (isAnalyticsAllowed) {
- val standardLogger = Logging.printlog
-
- // send all log messages through crashyltics, so if we do crash we'll have those in the report
- Logging.printlog = { level, tag, message ->
- crashlytics.log("$tag: $message")
- standardLogger(level, tag, message)
- }
+ sendCrashReports() // Send the new report
}
}
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 1af4da1e5..54886c7e5 100644
--- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
@@ -19,6 +19,7 @@ data class Channel(
) {
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"
// These bytes must match the well known and not secret bytes used the default channel AES128 key device code
@@ -32,17 +33,17 @@ data class Channel(
MeshProtos.ChannelSettings.newBuilder().setName(defaultChannelName)
.setModemConfig(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
)
-
+
const val prefix = "https://www.meshtastic.org/c/#"
- private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP
+ 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(.*)")
+ 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)
@@ -54,24 +55,39 @@ data class Channel(
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()) defaultChannelName else settings.name
+ val name: String
+ get() = if (settings.name.isEmpty()) {
+ // We have a new style 'empty' channel name. Use the same logic from the device to convert that to a human readable name
+ 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"
+ else -> "Invalid"
+ }
+ } else
+ settings.name
+
val modemConfig: MeshProtos.ChannelSettings.ModemConfig get() = settings.modemConfig
- val psk get() = if(settings.psk.size() != 1)
- settings.psk // A standard PSK
- else {
- // One of our special 1 byte PSKs, see mesh.proto for docs.
- val pskIndex = settings.psk.byteAt(0).toInt()
-
- if(pskIndex == 0)
- ByteString.EMPTY // Treat as an empty PSK (no encryption)
+ val psk
+ get() = if (settings.psk.size() != 1)
+ settings.psk // A standard PSK
else {
- // Treat an index of 1 as the old channelDefaultKey and work up from there
- val bytes = channelDefaultKey.clone()
- bytes[bytes.size - 1] = (0xff and (bytes[bytes.size - 1] + pskIndex - 1)).toByte()
- ByteString.copyFrom(bytes)
+ // One of our special 1 byte PSKs, see mesh.proto for docs.
+ val pskIndex = settings.psk.byteAt(0).toInt()
+
+ if (pskIndex == 0)
+ ByteString.EMPTY // Treat as an empty PSK (no encryption)
+ else {
+ // Treat an index of 1 as the old channelDefaultKey and work up from there
+ val bytes = channelDefaultKey.clone()
+ bytes[bytes.size - 1] = (0xff and (bytes[bytes.size - 1] + pskIndex - 1)).toByte()
+ ByteString.copyFrom(bytes)
+ }
}
- }
/**
* Return a name that is formatted as #channename-suffix
@@ -80,28 +96,41 @@ data class Channel(
*/
val humanName: String
get() {
- val code = settings.psk.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) })
- return "#${settings.name}-${'A' + (code % 26)}"
+ val suffix: Char = if (settings.psk.size() != 1) {
+ // we have a full PSK, so hash it to generate the suffix
+ val code = settings.psk.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) })
+
+ 'A' + (code % 26)
+ } else
+ '0' + settings.psk.byteAt(0).toInt()
+
+ return "#${name}-${suffix}"
}
/// Can this channel be changed right now?
var editable = false
/// Return an URL that represents the current channel values
- fun getChannelUrl(): Uri {
+ /// @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)
- return Uri.parse("$prefix$enc")
+ 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().toString(), BarcodeFormat.QR_CODE, 192, 192);
+ 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/Constants.kt b/app/src/main/java/com/geeksville/mesh/service/Constants.kt
index 4d4b68b9b..3b99c917a 100644
--- a/app/src/main/java/com/geeksville/mesh/service/Constants.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/Constants.kt
@@ -9,6 +9,7 @@ const val prefix = "com.geeksville.mesh"
// a bool true means now connected, false means not
const val EXTRA_CONNECTED = "$prefix.Connected"
+const val EXTRA_PROGRESS = "$prefix.Progress"
/// a bool true means we expect this condition to continue until, false means device might come back
const val EXTRA_PERMANENT = "$prefix.Permanent"
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 30d2e8087..bde6821fc 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.service.SoftwareUpdateService.Companion.ProgressNotStarted
import com.geeksville.util.*
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.ResolvableApiException
@@ -891,6 +892,7 @@ class MeshService : Service(), Logging {
// Do our startup init
try {
connectTimeMsec = System.currentTimeMillis()
+ SoftwareUpdateService.sendProgress(this, ProgressNotStarted) // Kinda crufty way of reiniting software update
startConfig()
} catch (ex: InvalidProtocolBufferException) {
@@ -1097,10 +1099,10 @@ class MeshService : Service(), Logging {
DataPair("dev_error_count", myInfo.errorCount)
)
- if (myInfo.errorCode != 0) {
+ if (myInfo.errorCode.number != 0) {
GeeksvilleApplication.analytics.track(
"dev_error",
- DataPair("code", myInfo.errorCode),
+ 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
@@ -1262,7 +1264,12 @@ class MeshService : Service(), Logging {
destNum: Int = NODENUM_BROADCAST,
wantResponse: Boolean = false
) = serviceScope.handledLaunch {
- sendPosition(lat, lon, alt, destNum, wantResponse)
+ try {
+ sendPosition(lat, lon, alt, destNum, wantResponse)
+ }
+ catch(ex: RadioNotConnectedException) {
+ warn("Ignoring disconnected radio during gps location update")
+ }
}
/** Send our current radio config to the device
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
index d69a4d883..d765c21aa 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceStarter.kt
@@ -8,6 +8,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
+import com.geeksville.andlib.BuildConfig
import java.util.concurrent.TimeUnit
/**
@@ -65,6 +66,6 @@ fun MeshService.Companion.startService(context: Context) {
// Before binding we want to explicitly create - so the service stays alive forever (so it can keep
// listening for the bluetooth packets arriving from the radio. And when they arrive forward them
// to Signal or whatever.
- info("Trying to start service")
+ info("Trying to start service debug=${BuildConfig.DEBUG}")
requireNotNull(context.startMeshService()) { "Failed to start service" }
}
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 69d57639c..84bcfbc1a 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
@@ -147,6 +147,8 @@ class SoftwareUpdateService : JobIntentService(), Logging {
const val ACTION_START_UPDATE = "$prefix.START_UPDATE"
+ const val ACTION_UPDATE_PROGRESS = "$prefix.UPDATE_PROGRESS"
+
const val EXTRA_MACADDR = "macaddr"
private const val SCAN_PERIOD: Long = 10000
@@ -170,10 +172,15 @@ class SoftwareUpdateService : JobIntentService(), Logging {
private val MANUFACTURE_CHARACTER = longBLEUUID("2a29")
private val HW_VERSION_CHARACTER = longBLEUUID("2a27")
+ const val ProgressSuccess = -1
+ const val ProgressUpdateFailed = -2
+ const val ProgressBleException = -3
+ const val ProgressNotStarted = -4
+
/**
* % progress through the update
*/
- var progress = 0
+ var progress = ProgressNotStarted
/**
* Convenience method for enqueuing work in to this service.
@@ -187,6 +194,17 @@ class SoftwareUpdateService : JobIntentService(), Logging {
}
+ fun sendProgress(context: Context, p: Int) {
+ if(progress != p) {
+ progress = p
+
+ val intent = Intent(ACTION_UPDATE_PROGRESS).putExtra(
+ EXTRA_PROGRESS,
+ p
+ )
+ context.sendBroadcast(intent)
+ }
+ }
/** Return true if we thing the firmwarte shoulde be updated
*
@@ -200,7 +218,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
val minVer =
DeviceVersion("0.7.8") // The oldest device version with a working software update service
- (curVer > deviceVersion) && (deviceVersion >= minVer)
+ ((curVer > deviceVersion) && (deviceVersion >= minVer))
} catch (ex: Exception) {
errormsg("Error finding swupdate info", ex)
false // If we fail parsing our update info
@@ -262,7 +280,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
errormsg("Ignoring failure to update spiffs on old appload")
}
assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) }
- progress = -1 // success
+ sendProgress(context, ProgressSuccess)
}
// writable region codes in the ESP32 update code
@@ -288,7 +306,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
info("Starting firmware update for $assetName, flash region $flashRegion")
- progress = 0
+ sendProgress(context,0)
val totalSizeDesc = getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)
val dataDesc = getCharacteristic(SW_UPDATE_DATA_CHARACTER)
val crc32Desc = getCharacteristic(SW_UPDATE_CRC32_CHARACTER)
@@ -336,7 +354,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
// yet
val maxProgress = if(flashRegion != FLASH_REGION_APPLOAD)
50 else 100
- progress = firmwareNumSent * maxProgress / firmwareSize
+ sendProgress(context, firmwareNumSent * maxProgress / firmwareSize)
debug("sending block ${progress}%")
var blockSize = 512 - 3 // Max size MTU excluding framing
@@ -366,7 +384,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
sync.readCharacteristic(updateResultDesc)
.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0)
if (updateResult != 0) {
- progress = -2
+ sendProgress(context, ProgressUpdateFailed)
throw Exception("Device update failed, reason=$updateResult")
}
@@ -377,7 +395,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
}
}
} catch (ex: BLEException) {
- progress = -3
+ sendProgress(context, ProgressBleException)
throw ex // Unexpected BLE exception
}
}
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 936dfff5f..ec8f2ac53 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -29,7 +29,6 @@ import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.android.hideKeyboard
import com.geeksville.android.isGooglePlayAvailable
-import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
import com.geeksville.mesh.android.bluetoothManager
@@ -40,6 +39,9 @@ import com.geeksville.mesh.service.BluetoothInterface
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.RadioInterfaceService
import com.geeksville.mesh.service.SerialInterface
+import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
+import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted
+import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressSuccess
import com.geeksville.util.anonymize
import com.geeksville.util.exceptionReporter
import com.google.android.gms.location.LocationRequest
@@ -50,9 +52,9 @@ import com.hoho.android.usbserial.driver.UsbSerialDriver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
import java.util.regex.Pattern
+
object SLogging : Logging {}
/// Change to a new macaddr selection, updating GUI and radio
@@ -484,27 +486,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private fun doFirmwareUpdate() {
model.meshService?.let { service ->
- mainScope.handledLaunch {
- debug("User started firmware update")
- binding.updateFirmwareButton.isEnabled = false // Disable until things complete
- binding.updateProgressBar.visibility = View.VISIBLE
- binding.updateProgressBar.progress = 0 // start from scratch
+ debug("User started firmware update")
+ binding.updateFirmwareButton.isEnabled = false // Disable until things complete
+ binding.updateProgressBar.visibility = View.VISIBLE
+ binding.updateProgressBar.progress = 0 // start from scratch
- binding.scanStatusText.text = "Updating firmware, wait up to eight minutes..."
- try {
- service.startFirmwareUpdate()
- while (service.updateStatus >= 0) {
- binding.updateProgressBar.progress = service.updateStatus
- delay(2000) // Only check occasionally
- }
- } finally {
- val isSuccess = (service.updateStatus == -1)
- binding.scanStatusText.text =
- if (isSuccess) "Update successful" else "Update failed"
- binding.updateProgressBar.isEnabled = false
- binding.updateFirmwareButton.isEnabled = !isSuccess
- }
- }
+ // We rely on our broadcast receiver to show progress as this progresses
+ service.startFirmwareUpdate()
}
}
@@ -516,20 +504,55 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
return binding.root
}
- private fun initNodeInfo() {
- val connected = model.isConnected.value
-
- // If actively connected possibly let the user update firmware
+ /// Set the correct update button configuration based on current progress
+ private fun refreshUpdateButton() {
+ debug("Reiniting the udpate button")
val info = model.myNodeInfo.value
- if (connected == MeshService.ConnectionState.CONNECTED && info != null && info.shouldUpdate && info.couldUpdate) {
+ val service = model.meshService
+ if (model.isConnected.value == MeshService.ConnectionState.CONNECTED && info != null && info.shouldUpdate && info.couldUpdate && service != null) {
binding.updateFirmwareButton.visibility = View.VISIBLE
- binding.updateFirmwareButton.isEnabled = true
binding.updateFirmwareButton.text =
getString(R.string.update_to).format(getString(R.string.cur_firmware_version))
+
+ val progress = service.updateStatus
+
+ binding.updateFirmwareButton.isEnabled =
+ (progress < 0) // if currently doing an upgrade disable button
+
+ if (progress >= 0) {
+ binding.updateProgressBar.progress = progress // update partial progress
+ binding.scanStatusText.setText(R.string.updating_firmware)
+ binding.updateProgressBar.visibility = View.VISIBLE
+ } else
+ when (progress) {
+ ProgressSuccess -> {
+ binding.scanStatusText.setText(R.string.update_successful)
+ binding.updateProgressBar.visibility = View.GONE
+ }
+ ProgressNotStarted -> {
+ // Do nothing - because we don't want to overwrite the status text in this case
+ binding.updateProgressBar.visibility = View.GONE
+ }
+ else -> {
+ binding.scanStatusText.setText(R.string.update_failed)
+ binding.updateProgressBar.visibility = View.VISIBLE
+ }
+ }
+ binding.updateProgressBar.isEnabled = false
+
} else {
binding.updateFirmwareButton.visibility = View.GONE
binding.updateProgressBar.visibility = View.GONE
}
+ }
+
+ private fun initNodeInfo() {
+ val connected = model.isConnected.value
+
+ refreshUpdateButton()
+
+ // If actively connected possibly let the user update firmware
+ val info = model.myNodeInfo.value
when (connected) {
MeshService.ConnectionState.CONNECTED -> {
@@ -784,11 +807,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
initClassicScan()
}
- override fun onPause() {
- super.onPause()
- scanModel.stopScan()
- }
-
/**
* If the user has not turned on location access throw up a toast warning
*/
@@ -842,11 +860,29 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
+ private val updateProgressFilter = IntentFilter(ACTION_UPDATE_PROGRESS)
+
+ private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ refreshUpdateButton()
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ scanModel.stopScan()
+
+ requireActivity().unregisterReceiver(updateProgressReceiver)
+ }
+
override fun onResume() {
super.onResume()
+
if (!hasCompanionDeviceApi)
scanModel.startScan()
+ requireActivity().registerReceiver(updateProgressReceiver, updateProgressFilter)
+
// Keep reminding user BLE is still off
val hasUSB = activity?.let { SerialInterface.findDrivers(it).isNotEmpty() } ?: true
if (!hasUSB) {
diff --git a/app/src/main/proto b/app/src/main/proto
index 8729bad7f..dfe7bc121 160000
--- a/app/src/main/proto
+++ b/app/src/main/proto
@@ -1 +1 @@
-Subproject commit 8729bad7f6cfa461be02e3ea65fbde29435b3fe3
+Subproject commit dfe7bc1217a00c23eecb9dfcf1d56fe95ebddc3b
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 35e624739..427e2efb9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -77,4 +77,7 @@
Debug Panel
500 last messages
Clear
+ Updating firmware, wait up to eight minutes...
+ Update successful
+ Update failed