Skip to content

Commit ead31e6

Browse files
committed
Merge branch 'main' of https://github.com/mdecourcy/Meshtastic-Android into poc/maplibre
2 parents 7e38aaa + 1c37842 commit ead31e6

File tree

32 files changed

+497
-183
lines changed

32 files changed

+497
-183
lines changed

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ body:
3636
- label: |
3737
I am able to reproduce the bug with the latest version.
3838
required: true
39+
- label: |
40+
I have updated to the latest *Alpha* firmware, and am able to reproduce the bug. Many issues are fixed quickly in alpha before the general beta release.
41+
required: true
3942
- label: |
4043
I made sure that there are no existing **OPEN or CLOSED issues** which I could contribute my information to.
4144
required: true
@@ -51,7 +54,9 @@ body:
5154
- label: |
5255
I agree to follow this project's Code of Conduct
5356
required: true
54-
57+
- label: |
58+
I actually read this list, and should be taken seriously.
59+
required: false
5560
- type: input
5661
id: app-version
5762
attributes:

.github/ISSUE_TEMPLATE/bug_repor_internal.yml renamed to .github/ISSUE_TEMPLATE/bug_report_internal.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ body:
3636
- label: |
3737
I am able to reproduce the bug with the latest version.
3838
required: true
39+
- label: |
40+
I have updated to the latest *Alpha* firmware, and am able to reproduce the bug. Many issues are fixed quickly in alpha before the general beta release.
41+
required: true
3942
- label: |
4043
I made sure that there are no existing **OPEN or CLOSED issues** which I could contribute my information to.
4144
required: true

.github/workflows/release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,16 @@ jobs:
241241
generate_release_notes: true
242242
files: ./artifacts/*/*
243243
draft: true
244+
prerelease: true
245+
246+
- name: Create or Update internal GitHub Release
247+
uses: softprops/action-gh-release@v2
248+
with:
249+
repository: ${{ secrets.INTERNAL_BUILDS_HOST }}
250+
token: ${{ secrets.INTERNAL_BUILDS_HOST_PAT }}
251+
tag_name: ${{ inputs.tag_name }}
252+
name: ${{ inputs.tag_name }} (${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }})
253+
generate_release_notes: true
254+
files: ./artifacts/*/*
255+
draft: false
244256
prerelease: true

app/src/main/assets/device_hardware.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,28 @@
923923
"heltec_mesh_pocket.svg"
924924
],
925925
"requiresDfu": true,
926-
"hasInkHud": true
926+
"hasInkHud": true,
927+
"key": "HELTEC_MESH_POCKET",
928+
"variant": "10000mAh"
929+
},
930+
{
931+
"hwModel": 94,
932+
"hwModelSlug": "HELTEC_MESH_POCKET",
933+
"platformioTarget": "heltec-mesh-pocket-5000",
934+
"architecture": "nrf52840",
935+
"activelySupported": true,
936+
"supportLevel": 1,
937+
"displayName": "Heltec MeshPocket",
938+
"tags": [
939+
"Heltec"
940+
],
941+
"images": [
942+
"heltec_mesh_pocket.svg"
943+
],
944+
"requiresDfu": true,
945+
"hasInkHud": true,
946+
"key": "HELTEC_MESH_POCKET",
947+
"variant": "5000mAh"
927948
},
928949
{
929950
"hwModel": 95,

app/src/main/java/com/geeksville/mesh/service/MeshService.kt

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ class MeshService : Service() {
410410
}
411411
.launchIn(serviceScope)
412412

413-
loadSettings() // Load our last known node DB
413+
loadCachedNodeDB() // Load our last known node DB
414414

415415
// the rest of our init will happen once we are in radioConnection.onServiceConnected
416416
}
@@ -482,7 +482,7 @@ class MeshService : Service() {
482482
// BEGINNING OF MODEL - FIXME, move elsewhere
483483
//
484484

485-
private fun loadSettings() = serviceScope.handledLaunch {
485+
private fun loadCachedNodeDB() = serviceScope.handledLaunch {
486486
myNodeInfo = nodeRepository.myNodeInfo.value
487487
nodeDBbyNodeNum.putAll(nodeRepository.getNodeDBbyNum().first())
488488
// Note: we do not haveNodeDB = true because that means we've got a valid db from a real
@@ -721,6 +721,7 @@ class MeshService : Service() {
721721
rssi = packet.rxRssi,
722722
replyId = data.replyId,
723723
relayNode = packet.relayNode,
724+
viaMqtt = packet.viaMqtt,
724725
)
725726
}
726727

@@ -992,6 +993,17 @@ class MeshService : Service() {
992993
sessionPasskey = a.sessionPasskey
993994
}
994995

996+
/**
997+
* Check if a User is a default/placeholder from firmware (node was evicted and re-created) and whether we should
998+
* preserve existing user data instead of overwriting it.
999+
*/
1000+
private fun shouldPreserveExistingUser(existing: MeshProtos.User, incoming: MeshProtos.User): Boolean {
1001+
val isDefaultName = incoming.longName.matches(Regex("^Meshtastic [0-9a-fA-F]{4}$"))
1002+
val isDefaultHwModel = incoming.hwModel == MeshProtos.HardwareModel.UNSET
1003+
val hasExistingUser = existing.id.isNotEmpty() && existing.hwModel != MeshProtos.HardwareModel.UNSET
1004+
return hasExistingUser && isDefaultName && isDefaultHwModel
1005+
}
1006+
9951007
private fun handleSharedContactImport(contact: AdminProtos.SharedContact) {
9961008
handleReceivedUser(contact.nodeNum, contact.user, manuallyVerified = true)
9971009
}
@@ -1006,22 +1018,37 @@ class MeshService : Service() {
10061018
updateNodeInfo(fromNum) {
10071019
val newNode = (it.isUnknownUser && p.hwModel != MeshProtos.HardwareModel.UNSET)
10081020

1009-
val keyMatch = !it.hasPKC || it.user.publicKey == p.publicKey
1010-
it.user =
1011-
if (keyMatch) {
1012-
p
1013-
} else {
1014-
p.copy {
1015-
Timber.w("Public key mismatch from $longName ($shortName)")
1016-
publicKey = NodeEntity.ERROR_BYTE_STRING
1021+
// Check if this is a default/unknown user from firmware (node was evicted and re-created)
1022+
val shouldPreserve = shouldPreserveExistingUser(it.user, p)
1023+
1024+
if (shouldPreserve) {
1025+
// Firmware sent us a placeholder - keep all our existing user data
1026+
Timber.d(
1027+
"Preserving existing user data for node $fromNum: " +
1028+
"kept='${it.user.longName}' (hwModel=${it.user.hwModel}), " +
1029+
"skipped default='${p.longName}' (hwModel=UNSET)",
1030+
)
1031+
// Still update channel and verification status
1032+
it.channel = channel
1033+
it.manuallyVerified = manuallyVerified
1034+
} else {
1035+
val keyMatch = !it.hasPKC || it.user.publicKey == p.publicKey
1036+
it.user =
1037+
if (keyMatch) {
1038+
p
1039+
} else {
1040+
p.copy {
1041+
Timber.w("Public key mismatch from $longName ($shortName)")
1042+
publicKey = NodeEntity.ERROR_BYTE_STRING
1043+
}
10171044
}
1045+
it.longName = p.longName
1046+
it.shortName = p.shortName
1047+
it.channel = channel
1048+
it.manuallyVerified = manuallyVerified
1049+
if (newNode) {
1050+
serviceNotifications.showNewNodeSeenNotification(it)
10181051
}
1019-
it.longName = p.longName
1020-
it.shortName = p.shortName
1021-
it.channel = channel
1022-
it.manuallyVerified = manuallyVerified
1023-
if (newNode) {
1024-
serviceNotifications.showNewNodeSeenNotification(it)
10251052
}
10261053
}
10271054
}
@@ -1326,6 +1353,9 @@ class MeshService : Service() {
13261353
p.data.status = m
13271354
p.routingError = routingError
13281355
p.data.relayNode = relayNode
1356+
if (isAck) {
1357+
p.data.relays += 1
1358+
}
13291359
packetRepository.get().update(p)
13301360
}
13311361
serviceBroadcasts.broadcastMessageStatus(requestId, m)
@@ -1720,14 +1750,9 @@ class MeshService : Service() {
17201750
updateNodeInfo(info.num) {
17211751
if (info.hasUser()) {
17221752
// Check if this is a default/unknown user from firmware (node was evicted and re-created)
1723-
val isDefaultName = info.user.longName.matches(Regex("^Meshtastic [0-9a-fA-F]{4}$"))
1724-
val isDefaultHwModel = info.user.hwModel == MeshProtos.HardwareModel.UNSET
1725-
val hasExistingUser = it.user.id.isNotEmpty() && it.user.hwModel != MeshProtos.HardwareModel.UNSET
1726-
1727-
// If firmware sends a default user (evicted node), preserve our existing user data
1728-
val shouldPreserveExisting = hasExistingUser && isDefaultName && isDefaultHwModel
1753+
val shouldPreserve = shouldPreserveExistingUser(it.user, info.user)
17291754

1730-
if (shouldPreserveExisting) {
1755+
if (shouldPreserve) {
17311756
// Firmware sent us a placeholder - keep all our existing user data
17321757
Timber.d(
17331758
"Preserving existing user data for node ${info.num}: " +
@@ -2080,7 +2105,9 @@ class MeshService : Service() {
20802105
} else {
20812106
newNodes.forEach(::installNodeInfo)
20822107
newNodes.clear()
2083-
serviceScope.handledLaunch { nodeRepository.installConfig(myNodeInfo!!, nodeDBbyNodeNum.values.toList()) }
2108+
// Individual nodes are already upserted to DB via updateNodeInfo->nodeRepository.upsert
2109+
// Only call installConfig to persist myNodeInfo, not to overwrite all nodes
2110+
serviceScope.handledLaunch { myNodeInfo?.let { nodeRepository.installConfig(it, emptyList()) } }
20842111
haveNodeDB = true
20852112
flushEarlyReceivedPackets("node_info_complete")
20862113
sendAnalytics()
@@ -2286,6 +2313,8 @@ class MeshService : Service() {
22862313
historyLog { dbSummary }
22872314
// Do not clear packet DB here; messages are per-device and should persist
22882315
clearNotifications()
2316+
// Reload nodes from the newly switched database
2317+
loadCachedNodeDB()
22892318
}
22902319
} else {
22912320
Timber.d("SetDeviceAddress: Device address is unchanged, ignoring.")

app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package com.geeksville.mesh.ui.connections
2020
import android.net.InetAddresses
2121
import android.os.Build
2222
import android.util.Patterns
23-
import androidx.compose.animation.AnimatedVisibility
2423
import androidx.compose.foundation.layout.Arrangement
2524
import androidx.compose.foundation.layout.Box
2625
import androidx.compose.foundation.layout.Column
@@ -30,6 +29,7 @@ import androidx.compose.foundation.layout.Spacer
3029
import androidx.compose.foundation.layout.fillMaxSize
3130
import androidx.compose.foundation.layout.fillMaxWidth
3231
import androidx.compose.foundation.layout.height
32+
import androidx.compose.foundation.layout.heightIn
3333
import androidx.compose.foundation.layout.padding
3434
import androidx.compose.foundation.layout.size
3535
import androidx.compose.foundation.rememberScrollState
@@ -52,6 +52,8 @@ import androidx.compose.runtime.remember
5252
import androidx.compose.runtime.setValue
5353
import androidx.compose.ui.Alignment
5454
import androidx.compose.ui.Modifier
55+
import androidx.compose.ui.layout.onSizeChanged
56+
import androidx.compose.ui.platform.LocalDensity
5557
import androidx.compose.ui.text.font.FontFamily
5658
import androidx.compose.ui.text.style.TextAlign
5759
import androidx.compose.ui.unit.dp
@@ -123,6 +125,7 @@ fun ConnectionsScreen(
123125
val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
124126
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
125127
val bluetoothState by connectionsViewModel.bluetoothState.collectAsStateWithLifecycle()
128+
val density = LocalDensity.current
126129
val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET
127130

128131
val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
@@ -194,37 +197,49 @@ fun ConnectionsScreen(
194197
.padding(paddingValues)
195198
.padding(16.dp),
196199
) {
197-
AnimatedVisibility(visible = connectionState == ConnectionState.Connecting) {
198-
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
199-
CircularWavyProgressIndicator(modifier = Modifier.size(96.dp).padding(16.dp))
200+
var connectionSectionHeight by remember { mutableStateOf(0.dp) }
201+
val placeholderHeight = connectionSectionHeight.takeIf { it > 0.dp } ?: 0.dp
202+
Box(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp).heightIn(min = placeholderHeight)) {
203+
if (connectionState == ConnectionState.Connecting) {
204+
Row(
205+
modifier = Modifier.fillMaxWidth().align(Alignment.Center),
206+
horizontalArrangement = Arrangement.Center,
207+
) {
208+
CircularWavyProgressIndicator(modifier = Modifier.size(96.dp).padding(16.dp))
209+
}
200210
}
201-
}
202-
AnimatedVisibility(
203-
visible = connectionState.isConnected(),
204-
modifier = Modifier.padding(bottom = 16.dp),
205-
) {
206-
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
207-
ourNode?.let { node ->
208-
TitledCard(title = stringResource(Res.string.connected_device)) {
209-
CurrentlyConnectedInfo(
210-
node = node,
211-
bleDevice =
212-
bleDevices.firstOrNull { it.fullAddress == selectedDevice }
213-
as DeviceListEntry.Ble?,
214-
onNavigateToNodeDetails = onNavigateToNodeDetails,
215-
onClickDisconnect = { scanModel.disconnect() },
216-
)
211+
androidx.compose.animation.AnimatedVisibility(visible = connectionState.isConnected()) {
212+
Column(
213+
verticalArrangement = Arrangement.spacedBy(16.dp),
214+
modifier =
215+
Modifier.fillMaxWidth().onSizeChanged { size ->
216+
if (connectionState.isConnected()) {
217+
connectionSectionHeight = with(density) { size.height.toDp() }
218+
}
219+
},
220+
) {
221+
ourNode?.let { node ->
222+
TitledCard(title = stringResource(Res.string.connected_device)) {
223+
CurrentlyConnectedInfo(
224+
node = node,
225+
bleDevice =
226+
bleDevices.firstOrNull { it.fullAddress == selectedDevice }
227+
as DeviceListEntry.Ble?,
228+
onNavigateToNodeDetails = onNavigateToNodeDetails,
229+
onClickDisconnect = { scanModel.disconnect() },
230+
)
231+
}
217232
}
218-
}
219233

220-
if (regionUnset && selectedDevice != "m") {
221-
TitledCard(title = null) {
222-
ListItem(
223-
leadingIcon = Icons.Rounded.Language,
224-
text = stringResource(Res.string.set_your_region),
225-
) {
226-
isWaiting = true
227-
radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
234+
if (regionUnset && selectedDevice != "m") {
235+
TitledCard(title = null) {
236+
ListItem(
237+
leadingIcon = Icons.Rounded.Language,
238+
text = stringResource(Res.string.set_your_region),
239+
) {
240+
isWaiting = true
241+
radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
242+
}
228243
}
229244
}
230245
}

app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import kotlin.time.Duration.Companion.seconds
6464
private const val RSSI_DELAY = 10
6565
private const val RSSI_TIMEOUT = 5
6666

67+
@Suppress("LongMethod", "LoopWithTooManyJumpStatements")
6768
@Composable
6869
fun CurrentlyConnectedInfo(
6970
node: Node,

app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717

1818
package com.geeksville.mesh.ui.connections.components
1919

20-
import androidx.compose.foundation.clickable
20+
import androidx.compose.foundation.Indication
21+
import androidx.compose.foundation.LocalIndication
22+
import androidx.compose.foundation.gestures.detectTapGestures
23+
import androidx.compose.foundation.indication
24+
import androidx.compose.foundation.interaction.MutableInteractionSource
2125
import androidx.compose.foundation.layout.fillMaxWidth
2226
import androidx.compose.foundation.layout.size
2327
import androidx.compose.material.icons.Icons
@@ -35,8 +39,10 @@ import androidx.compose.material3.ListItemDefaults
3539
import androidx.compose.material3.RadioButton
3640
import androidx.compose.material3.Text
3741
import androidx.compose.runtime.Composable
42+
import androidx.compose.runtime.remember
3843
import androidx.compose.ui.Modifier
3944
import androidx.compose.ui.graphics.Color
45+
import androidx.compose.ui.input.pointer.pointerInput
4046
import androidx.compose.ui.unit.dp
4147
import com.geeksville.mesh.model.DeviceListEntry
4248
import org.jetbrains.compose.resources.stringResource
@@ -55,6 +61,7 @@ fun DeviceListItem(
5561
device: DeviceListEntry,
5662
onSelect: () -> Unit,
5763
modifier: Modifier = Modifier,
64+
onDelete: (() -> Unit)? = null,
5865
) {
5966
val icon =
6067
when (device) {
@@ -81,10 +88,19 @@ fun DeviceListItem(
8188
}
8289

8390
val useSelectable = modifier == Modifier
91+
val interactionSource = remember { MutableInteractionSource() }
92+
val indication: Indication = LocalIndication.current
93+
8494
ListItem(
8595
modifier =
86-
if (useSelectable) {
87-
modifier.fillMaxWidth().clickable(onClick = onSelect)
96+
if (useSelectable && onDelete != null) {
97+
modifier.fillMaxWidth().indication(interactionSource, indication).pointerInput(onDelete) {
98+
detectTapGestures(onTap = { onSelect() }, onLongPress = { onDelete() })
99+
}
100+
} else if (useSelectable) {
101+
modifier.fillMaxWidth().indication(interactionSource, indication).pointerInput(Unit) {
102+
detectTapGestures(onTap = { onSelect() })
103+
}
88104
} else {
89105
modifier.fillMaxWidth()
90106
},

0 commit comments

Comments
 (0)