From 0c1e43831612516e40f0304e21c014bc9df86e39 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:53:03 -0500 Subject: [PATCH 1/7] chore(deps): update agp to v8.13.0 (#2943) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c19e2d83a..327ae5f7c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ accompanistPermissions = "0.37.3" adaptive = "1.2.0-beta01" adaptive-navigation-suite = "1.3.2" -agp = "8.12.2" +agp = "8.13.0" appcompat = "1.7.1" awesome-app-rating = "2.8.0" coil = "3.3.0" From 251aa6cabd219990d95c7b3bad73fabc03c2669f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:00:29 +0000 Subject: [PATCH 2/7] chore(deps): update google maps compose to v6.8.0 (#2945) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 327ae5f7c..c2b2c0271 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ kotlinx-coroutines-android = "1.10.2" kotlinx-serialization-json = "1.9.0" lifecycle = "2.9.3" location-services = "21.3.0" -maps-compose = "6.7.2" +maps-compose = "6.8.0" markdownRenderer = "0.35.0" material = "1.12.0" material3 = "1.5.0-alpha03" From 76ddd29114bb4af7d75bffef0810e3c01ea7e2fd Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:12:32 -0500 Subject: [PATCH 3/7] feat: Support the `add` export method on channel url/qr (#2934) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> --- .../com/geeksville/mesh/model/ChannelSet.kt | 59 ++++++++----------- .../com/geeksville/mesh/ui/sharing/Channel.kt | 36 ++++++++++- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt index a3dbe67d1..06d769199 100644 --- a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt +++ b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt @@ -30,72 +30,61 @@ import kotlin.jvm.Throws private const val MESHTASTIC_HOST = "meshtastic.org" private const val CHANNEL_PATH = "/e/" -internal const val URL_PREFIX = "https://$MESHTASTIC_HOST$CHANNEL_PATH#" +internal const val URL_PREFIX = "https://$MESHTASTIC_HOST$CHANNEL_PATH" private const val BASE64FLAGS = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING /** * Return a [ChannelSet] that represents the ChannelSet encoded by the URL. + * * @throws MalformedURLException when not recognized as a valid Meshtastic URL */ @Throws(MalformedURLException::class) fun Uri.toChannelSet(): ChannelSet { - if (fragment.isNullOrBlank() || - !host.equals(MESHTASTIC_HOST, true) || - !path.equals(CHANNEL_PATH, true) - ) { + if (fragment.isNullOrBlank() || !host.equals(MESHTASTIC_HOST, true) || !path.equals(CHANNEL_PATH, true)) { throw MalformedURLException("Not a valid Meshtastic URL: ${toString().take(40)}") } // Older versions of Meshtastic clients (Apple/web) included `?add=true` within the URL fragment. // This gracefully handles those cases until the newer version are generally available/used. val url = ChannelSet.parseFrom(Base64.decode(fragment!!.substringBefore('?'), BASE64FLAGS)) - val shouldAdd = fragment?.substringAfter('?', "") - ?.takeUnless { it.isBlank() } - ?.equals("add=true") - ?: getBooleanQueryParameter("add", false) + val shouldAdd = + fragment?.substringAfter('?', "")?.takeUnless { it.isBlank() }?.equals("add=true") + ?: getBooleanQueryParameter("add", false) return url.toBuilder().apply { if (shouldAdd) clearLoraConfig() }.build() } -/** - * @return A list of globally unique channel IDs usable with MQTT subscribe() - */ +/** @return A list of globally unique channel IDs usable with MQTT subscribe() */ val ChannelSet.subscribeList: List get() = settingsList.filter { it.downlinkEnabled }.map { Channel(it, loraConfig).name } fun ChannelSet.getChannel(index: Int): Channel? = if (settingsCount > index) Channel(getSettings(index), loraConfig) else null -/** - * Return the primary channel info - */ -val ChannelSet.primaryChannel: Channel? get() = getChannel(0) +/** Return the primary channel info */ +val ChannelSet.primaryChannel: Channel? + get() = getChannel(0) /** * Return a URL that represents the [ChannelSet] + * * @param upperCasePrefix portions of the URL can be upper case to make for more efficient QR codes */ -fun ChannelSet.getChannelUrl(upperCasePrefix: Boolean = false): Uri { +fun ChannelSet.getChannelUrl(upperCasePrefix: Boolean = false, shouldAdd: Boolean = false): Uri { val channelBytes = this.toByteArray() ?: ByteArray(0) // if unset just use empty val enc = Base64.encodeToString(channelBytes, BASE64FLAGS) val p = if (upperCasePrefix) URL_PREFIX.uppercase() else URL_PREFIX - return Uri.parse("$p$enc") + val query = if (shouldAdd) "?add=true" else "" + return Uri.parse("$p$query#$enc") } -val ChannelSet.qrCode: Bitmap? - get() = try { - val multiFormatWriter = MultiFormatWriter() - - val bitMatrix = - multiFormatWriter.encode( - getChannelUrl(false).toString(), - BarcodeFormat.QR_CODE, - 960, - 960 - ) - val barcodeEncoder = BarcodeEncoder() - barcodeEncoder.createBitmap(bitMatrix) - } catch (ex: Throwable) { - errormsg("URL was too complex to render as barcode") - null - } +fun ChannelSet.qrCode(shouldAdd: Boolean): Bitmap? = try { + val multiFormatWriter = MultiFormatWriter() + val bitMatrix = + multiFormatWriter.encode(getChannelUrl(false, shouldAdd).toString(), BarcodeFormat.QR_CODE, 960, 960) + val barcodeEncoder = BarcodeEncoder() + barcodeEncoder.createBitmap(bitMatrix) +} catch (ex: Throwable) { + errormsg("URL was too complex to render as barcode") + null +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt index 079014cef..e81dd74e1 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt @@ -50,6 +50,9 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -139,6 +142,8 @@ fun ChannelScreen( var showResetDialog by remember { mutableStateOf(false) } + var shouldAddChannelsState by remember { mutableStateOf(true) } + /* Animate waiting for the configurations */ var isWaiting by remember { mutableStateOf(false) } if (isWaiting) { @@ -269,6 +274,7 @@ fun ChannelScreen( channelSet = channelSet, modemPresetName = modemPresetName, channelSelections = channelSelections, + shouldAddChannel = shouldAddChannelsState, onClick = { isWaiting = true radioConfigViewModel.setResponseStateLoading(ConfigRoute.CHANNELS) @@ -276,10 +282,26 @@ fun ChannelScreen( ) EditChannelUrl( enabled = enabled, - channelUrl = selectedChannelSet.getChannelUrl(), + channelUrl = selectedChannelSet.getChannelUrl(shouldAdd = shouldAddChannelsState), onConfirm = viewModel::requestChannelUrl, ) } + item { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + SegmentedButton( + label = { Text(text = stringResource(R.string.replace)) }, + onClick = { shouldAddChannelsState = false }, + selected = !shouldAddChannelsState, + shape = SegmentedButtonDefaults.itemShape(0, 2), + ) + SegmentedButton( + label = { Text(text = stringResource(R.string.add)) }, + onClick = { shouldAddChannelsState = true }, + selected = shouldAddChannelsState, + shape = SegmentedButtonDefaults.itemShape(1, 2), + ) + } + } item { ModemPresetInfo( modemPresetName = modemPresetName, @@ -401,9 +423,15 @@ private fun EditChannelUrl(enabled: Boolean, channelUrl: Uri, modifier: Modifier } @Composable -private fun QrCodeImage(enabled: Boolean, channelSet: ChannelSet, modifier: Modifier = Modifier) = Image( +private fun QrCodeImage( + enabled: Boolean, + channelSet: ChannelSet, + modifier: Modifier = Modifier, + shouldAddChannel: Boolean = false, +) = Image( painter = - channelSet.qrCode?.let { BitmapPainter(it.asImageBitmap()) } ?: painterResource(id = R.drawable.qrcode), + channelSet.qrCode(shouldAddChannel)?.let { BitmapPainter(it.asImageBitmap()) } + ?: painterResource(id = R.drawable.qrcode), contentDescription = stringResource(R.string.qr_code), modifier = modifier, contentScale = ContentScale.Inside, @@ -417,6 +445,7 @@ private fun ChannelListView( channelSet: ChannelSet, modemPresetName: String, channelSelections: SnapshotStateList, + shouldAddChannel: Boolean = false, onClick: () -> Unit = {}, ) { val selectedChannelSet = @@ -459,6 +488,7 @@ private fun ChannelListView( enabled = enabled, channelSet = selectedChannelSet, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + shouldAddChannel = shouldAddChannel, ) }, ) From 48679f443643aa0f5c7078a5081ec79b21942b59 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 3 Sep 2025 16:37:08 -0500 Subject: [PATCH 4/7] Fix Bluetooth reconnection logic (#2948) --- .../main/java/com/geeksville/mesh/service/SafeBluetooth.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index a582bc28c..05d65b956 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -557,8 +557,12 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun asyncConnect(autoConnect: Boolean = false, cb: (Result) -> Unit, lostConnectCb: () -> Unit) { logAssert(workQueue.isEmpty()) + + // If there's already connection work in progress, clear it before starting new connection + // This can happen during reconnection where previous connection work wasn't properly cleared if (currentWork != null) { - throw AssertionError("currentWork was not null: $currentWork") + warn("Found existing work during asyncConnect: $currentWork - clearing it") + synchronized(workQueue) { stopCurrentWork() } } lostConnectCallback = lostConnectCb From 39705ef30386439a936398e5f4aaa06550cab923 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:01:31 -0500 Subject: [PATCH 5/7] chore: Scheduled updates (Firmware, Hardware) (#2947) Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> --- app/src/main/assets/firmware_releases.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/assets/firmware_releases.json b/app/src/main/assets/firmware_releases.json index aa7128820..d8317b4bf 100644 --- a/app/src/main/assets/firmware_releases.json +++ b/app/src/main/assets/firmware_releases.json @@ -193,6 +193,12 @@ "title": "the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS", "page_url": "https://github.com/meshtastic/firmware/pull/7658", "zip_url": "https://github.com/meshtastic/firmware/actions/runs/17074730483" + }, + { + "id": "7583", + "title": "chore(deps): update meshtastic/web to v2.6.6", + "page_url": "https://github.com/meshtastic/firmware/pull/7583", + "zip_url": "https://github.com/meshtastic/firmware/actions/runs/17070663764" } ] } \ No newline at end of file From af4806c8c3240b013dec4b456dfad42d7d0129f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:01:47 -0500 Subject: [PATCH 6/7] chore(deps): update meshtastic protobufs to 34f0c81 (#2946) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- app/src/main/proto | 2 +- mesh_service_example/src/main/proto | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/proto b/app/src/main/proto index 4c4427c4a..34f0c8115 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit 4c4427c4a73c86fed7dc8632188bb8be95349d81 +Subproject commit 34f0c8115d95f9f4be6d600095428a03833ac98e diff --git a/mesh_service_example/src/main/proto b/mesh_service_example/src/main/proto index 4c4427c4a..34f0c8115 160000 --- a/mesh_service_example/src/main/proto +++ b/mesh_service_example/src/main/proto @@ -1 +1 @@ -Subproject commit 4c4427c4a73c86fed7dc8632188bb8be95349d81 +Subproject commit 34f0c8115d95f9f4be6d600095428a03833ac98e From de4ac5e3a1004a4611a98d2feba316132d037464 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:10:11 -0500 Subject: [PATCH 7/7] fix(ui): Prevent FAB from obscuring NodeScreen content (#2949) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt index e4c375138..ecaa69478 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt @@ -21,8 +21,10 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -157,6 +159,7 @@ fun NodeScreen( isConnected = connectionState.isConnected(), ) } + item { Spacer(modifier = Modifier.height(88.dp)) } } } }