mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(ble): Replace custom BLE implementation with Nordic (#3595)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
6cbecdd25e
commit
9e8ffaa0ba
13 changed files with 466 additions and 1399 deletions
|
|
@ -108,7 +108,6 @@ import org.meshtastic.proto.position
|
|||
import org.meshtastic.proto.telemetry
|
||||
import org.meshtastic.proto.user
|
||||
import timber.log.Timber
|
||||
import java.util.Random
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
|
@ -206,7 +205,9 @@ class MeshService : Service() {
|
|||
val minDeviceVersion = DeviceVersion(BuildConfig.MIN_FW_VERSION)
|
||||
val absoluteMinDeviceVersion = DeviceVersion(BuildConfig.ABS_MIN_FW_VERSION)
|
||||
|
||||
private var configNonce = 1
|
||||
// Two-stage config flow nonces to avoid stale BLE packets, mirroring Meshtastic-Apple
|
||||
private const val DEFAULT_CONFIG_ONLY_NONCE = 69420
|
||||
private const val DEFAULT_NODE_INFO_NONCE = 69421
|
||||
}
|
||||
|
||||
private val serviceJob = Job()
|
||||
|
|
@ -472,7 +473,7 @@ class MeshService : Service() {
|
|||
NodeEntity(num = n, user = defaultUser, longName = defaultUser.longName, channel = channel)
|
||||
}
|
||||
|
||||
private val hexIdRegex = """\!([0-9A-Fa-f]+)""".toRegex()
|
||||
private val hexIdRegex = """!([0-9A-Fa-f]+)""".toRegex()
|
||||
|
||||
// Map a userid to a node/ node num, or throw an exception if not found
|
||||
// We prefer to find nodes based on their assigned IDs, but if no ID has been assigned to a
|
||||
|
|
@ -1252,7 +1253,7 @@ class MeshService : Service() {
|
|||
delay(timeout * 1000L)
|
||||
Timber.w("Device timeout out, setting disconnected")
|
||||
onConnectionChanged(ConnectionState.DISCONNECTED)
|
||||
} catch (ex: CancellationException) {
|
||||
} catch (_: CancellationException) {
|
||||
Timber.d("device sleep timeout cancelled")
|
||||
}
|
||||
}
|
||||
|
|
@ -1274,27 +1275,17 @@ class MeshService : Service() {
|
|||
}
|
||||
|
||||
fun startConnect() {
|
||||
// Do our startup init
|
||||
try {
|
||||
connectTimeMsec = System.currentTimeMillis()
|
||||
startConfig()
|
||||
startConfigOnly()
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
Timber.e(ex, "Invalid protocol buffer sent by device - update device software and try again")
|
||||
} catch (ex: RadioNotConnectedException) {
|
||||
// note: no need to call startDeviceSleep(), because this exception could only have
|
||||
// reached us if it was
|
||||
// already called
|
||||
Timber.e("Lost connection to radio during init - waiting for reconnect ${ex.message}")
|
||||
} catch (ex: RemoteException) {
|
||||
// It seems that when the ESP32 goes offline it can briefly come back for a 100ms
|
||||
// ish which
|
||||
// causes the phone to try and reconnect. If we fail downloading our initial radio
|
||||
// state we don't want
|
||||
// to
|
||||
// claim we have a valid connection still
|
||||
connectionStateHolder.setState(ConnectionState.DEVICE_SLEEP)
|
||||
startDeviceSleep()
|
||||
throw ex // Important to rethrow so that we don't tell the app all is well
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1351,6 +1342,7 @@ class MeshService : Service() {
|
|||
|
||||
private val packetHandlers: Map<PayloadVariantCase, ((MeshProtos.FromRadio) -> Unit)> by lazy {
|
||||
PayloadVariantCase.entries.associateWith { variant: PayloadVariantCase ->
|
||||
Timber.d("PacketHandler - handling $variant")
|
||||
when (variant) {
|
||||
PayloadVariantCase.PACKET -> { proto: MeshProtos.FromRadio -> handleReceivedMeshPacket(proto.packet) }
|
||||
|
||||
|
|
@ -1396,6 +1388,8 @@ class MeshService : Service() {
|
|||
// Explicitly handle default/unwanted cases to satisfy the exhaustive `when`
|
||||
PayloadVariantCase.PAYLOADVARIANT_NOT_SET -> { proto ->
|
||||
Timber.e("Unexpected or unrecognized FromRadio variant: ${proto.payloadVariantCase}")
|
||||
// Additional debug: log raw bytes if possible (can't access bytes here) and full proto
|
||||
Timber.d("Full FromRadio proto: $proto")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1408,6 +1402,13 @@ class MeshService : Service() {
|
|||
private fun onReceiveFromRadio(bytes: ByteArray) {
|
||||
try {
|
||||
val proto = MeshProtos.FromRadio.parseFrom(bytes)
|
||||
if (proto.payloadVariantCase == PayloadVariantCase.PAYLOADVARIANT_NOT_SET) {
|
||||
Timber.w(
|
||||
"Received FromRadio with PAYLOADVARIANT_NOT_SET. rawBytes=${bytes.joinToString(",") { b ->
|
||||
String.format("0x%02x", b)
|
||||
}} proto=$proto",
|
||||
)
|
||||
}
|
||||
proto.route()
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
Timber.e("Invalid Protobuf from radio, len=${bytes.size}", ex)
|
||||
|
|
@ -1420,6 +1421,10 @@ class MeshService : Service() {
|
|||
// provisional NodeInfos we will install if all goes well
|
||||
private val newNodes = mutableListOf<MeshProtos.NodeInfo>()
|
||||
|
||||
// Nonces for two-stage config flow (match Meshtastic-Apple)
|
||||
private var configOnlyNonce: Int = DEFAULT_CONFIG_ONLY_NONCE
|
||||
private var nodeInfoNonce: Int = DEFAULT_NODE_INFO_NONCE
|
||||
|
||||
private fun handleDeviceConfig(config: ConfigProtos.Config) {
|
||||
Timber.d("Received config ${config.toOneLineString()}")
|
||||
val packetToSave =
|
||||
|
|
@ -1734,7 +1739,7 @@ class MeshService : Service() {
|
|||
|
||||
private fun onHasSettings() {
|
||||
packetHandler.sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { setTimeOnly = currentSecond() })
|
||||
processQueuedPackets() // send any packets that were queued up
|
||||
processQueuedPackets()
|
||||
startMqttClientProxy()
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
sendAnalytics()
|
||||
|
|
@ -1742,53 +1747,88 @@ class MeshService : Service() {
|
|||
}
|
||||
|
||||
private fun handleConfigComplete(configCompleteId: Int) {
|
||||
if (configCompleteId == configNonce) {
|
||||
Timber.d("Received config complete for config-only nonce $configNonce")
|
||||
handleConfigComplete()
|
||||
Timber.d(
|
||||
"handleConfigComplete called with id=$configCompleteId, configOnly=$configOnlyNonce, nodeInfo=$nodeInfoNonce",
|
||||
)
|
||||
when (configCompleteId) {
|
||||
configOnlyNonce -> handleConfigOnlyComplete()
|
||||
nodeInfoNonce -> handleNodeInfoComplete()
|
||||
else ->
|
||||
Timber.w(
|
||||
"Config complete id mismatch: received=$configCompleteId expected one of [$configOnlyNonce,$nodeInfoNonce]",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConfigComplete() {
|
||||
Timber.d("Received config only complete for nonce $configNonce")
|
||||
private fun handleConfigOnlyComplete() {
|
||||
Timber.d("Config-only complete for nonce $configOnlyNonce")
|
||||
val packetToSave =
|
||||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
message_type = "ConfigComplete",
|
||||
message_type = "ConfigOnlyComplete",
|
||||
received_date = System.currentTimeMillis(),
|
||||
raw_message = configNonce.toString(),
|
||||
fromRadio = fromRadio { this.configCompleteId = configNonce },
|
||||
raw_message = configOnlyNonce.toString(),
|
||||
fromRadio = fromRadio { this.configCompleteId = configOnlyNonce },
|
||||
)
|
||||
insertMeshLog(packetToSave)
|
||||
|
||||
// This was our config request
|
||||
if (newMyNodeInfo == null) {
|
||||
Timber.e("Did not receive a valid config")
|
||||
} else {
|
||||
myNodeInfo = newMyNodeInfo
|
||||
}
|
||||
// This was our config request
|
||||
// Keep BLE awake and allow the firmware to settle before the node-info stage.
|
||||
serviceScope.handledLaunch {
|
||||
sendHeartbeat()
|
||||
startNodeInfoOnly()
|
||||
}
|
||||
}
|
||||
|
||||
/** Send a ToRadio heartbeat to keep the link alive without producing mesh traffic. */
|
||||
private fun sendHeartbeat() {
|
||||
try {
|
||||
packetHandler.sendToRadio(
|
||||
ToRadio.newBuilder().apply { heartbeat = MeshProtos.Heartbeat.getDefaultInstance() },
|
||||
)
|
||||
Timber.d("Heartbeat sent between nonce stages")
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Failed to send heartbeat; proceeding with node-info stage")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNodeInfoComplete() {
|
||||
Timber.d("NodeInfo complete for nonce $nodeInfoNonce")
|
||||
val packetToSave =
|
||||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
message_type = "NodeInfoComplete",
|
||||
received_date = System.currentTimeMillis(),
|
||||
raw_message = nodeInfoNonce.toString(),
|
||||
fromRadio = fromRadio { this.configCompleteId = nodeInfoNonce },
|
||||
)
|
||||
insertMeshLog(packetToSave)
|
||||
if (newNodes.isEmpty()) {
|
||||
Timber.e("Did not receive a valid node info")
|
||||
} else {
|
||||
newNodes.forEach(::installNodeInfo)
|
||||
newNodes.clear()
|
||||
serviceScope.handledLaunch { nodeRepository.installConfig(myNodeInfo!!, nodeDBbyNodeNum.values.toList()) }
|
||||
|
||||
haveNodeDB = true // we now have nodes from real hardware
|
||||
haveNodeDB = true
|
||||
sendAnalytics()
|
||||
onHasSettings()
|
||||
}
|
||||
}
|
||||
|
||||
/** Start the modern (REV2) API configuration flow */
|
||||
private fun startConfig() {
|
||||
configNonce += 1
|
||||
private fun startConfigOnly() {
|
||||
newMyNodeInfo = null
|
||||
Timber.d("Starting config-only nonce=$configOnlyNonce")
|
||||
packetHandler.sendToRadio(ToRadio.newBuilder().apply { this.wantConfigId = configOnlyNonce })
|
||||
}
|
||||
|
||||
private fun startNodeInfoOnly() {
|
||||
newNodes.clear()
|
||||
|
||||
Timber.d("Starting config only nonce=$configNonce")
|
||||
|
||||
packetHandler.sendToRadio(ToRadio.newBuilder().apply { this.wantConfigId = configNonce })
|
||||
Timber.d("Starting node-info nonce=$nodeInfoNonce")
|
||||
packetHandler.sendToRadio(ToRadio.newBuilder().apply { this.wantConfigId = nodeInfoNonce })
|
||||
}
|
||||
|
||||
/** Send a position (typically from our built in GPS) into the mesh. */
|
||||
|
|
@ -1799,20 +1839,14 @@ class MeshService : Service() {
|
|||
val idNum = destNum ?: mi.myNodeNum // when null we just send to the local node
|
||||
Timber.d("Sending our position/time to=$idNum ${Position(position)}")
|
||||
|
||||
// Also update our own map for our nodeNum, by handling the packet just like packets
|
||||
// from other users
|
||||
// Also update our own map for our nodeNum, by handling the packet just like packets from other users
|
||||
if (!localConfig.position.fixedPosition) {
|
||||
handleReceivedPosition(mi.myNodeNum, position)
|
||||
}
|
||||
|
||||
packetHandler.sendToRadio(
|
||||
newMeshPacketTo(idNum).buildMeshPacket(
|
||||
channel =
|
||||
if (destNum == null) {
|
||||
0
|
||||
} else {
|
||||
nodeDBbyNodeNum[destNum]?.channel ?: 0
|
||||
},
|
||||
channel = if (destNum == null) 0 else nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
priority = MeshPacket.Priority.BACKGROUND,
|
||||
) {
|
||||
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
||||
|
|
@ -1821,7 +1855,7 @@ class MeshService : Service() {
|
|||
},
|
||||
)
|
||||
}
|
||||
} catch (ex: BLEException) {
|
||||
} catch (_: BLEException) {
|
||||
Timber.w("Ignoring disconnected radio during gps location update")
|
||||
}
|
||||
}
|
||||
|
|
@ -1836,13 +1870,10 @@ class MeshService : Service() {
|
|||
Timber.d("Ignoring nop owner change")
|
||||
} else {
|
||||
Timber.d(
|
||||
"setOwner Id: $id longName: ${longName.anonymize}" +
|
||||
" shortName: $shortName isLicensed: $isLicensed" +
|
||||
" isUnmessagable: $isUnmessagable",
|
||||
"setOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed isUnmessagable: $isUnmessagable",
|
||||
)
|
||||
|
||||
// Also update our own map for our nodeNum, by handling the packet just like packets
|
||||
// from other users
|
||||
// Also update our own map for our nodeNum, by handling the packet just like packets from other users
|
||||
handleReceivedUser(dest.num, user)
|
||||
|
||||
// encapsulate our payload in the proper protobuf and fire it off
|
||||
|
|
@ -1851,18 +1882,14 @@ class MeshService : Service() {
|
|||
}
|
||||
|
||||
// Do not use directly, instead call generatePacketId()
|
||||
private var currentPacketId = Random(System.currentTimeMillis()).nextLong().absoluteValue
|
||||
private var currentPacketId = java.util.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) // A mask for only the valid packet ID bits, either 255 or maxint
|
||||
|
||||
val numPacketIds = ((1L shl 32) - 1)
|
||||
currentPacketId++
|
||||
|
||||
currentPacketId = currentPacketId and 0xffffffff // keep from exceeding 32 bits
|
||||
|
||||
// Use modulus and +1 to ensure we skip 0 on any values we return
|
||||
currentPacketId = currentPacketId and 0xffffffff
|
||||
return ((currentPacketId % numPacketIds) + 1L).toInt()
|
||||
}
|
||||
|
||||
|
|
@ -1941,7 +1968,6 @@ class MeshService : Service() {
|
|||
}
|
||||
},
|
||||
)
|
||||
updateNodeInfo(node.num) { it.isIgnored = !node.isIgnored }
|
||||
}
|
||||
|
||||
private fun sendReaction(reaction: ServiceAction.Reaction) = toRemoteExceptions {
|
||||
|
|
@ -1994,18 +2020,13 @@ class MeshService : Service() {
|
|||
radioInterfaceService.setDeviceAddress(deviceAddr)
|
||||
}
|
||||
|
||||
// Note: bound methods don't get properly exception caught/logged, so do that with a
|
||||
// wrapper
|
||||
// per https://blog.classycode.com/dealing-with-exceptions-in-aidl-9ba904c6d63
|
||||
override fun subscribeReceiver(packageName: String, receiverName: String) = toRemoteExceptions {
|
||||
serviceBroadcasts.subscribeReceiver(receiverName, packageName)
|
||||
}
|
||||
|
||||
override fun getUpdateStatus(): Int = -4 // ProgressNotStarted
|
||||
override fun getUpdateStatus(): Int = -4
|
||||
|
||||
override fun startFirmwareUpdate() = toRemoteExceptions {
|
||||
// TODO reimplement this after we have a new firmware update mechanism
|
||||
}
|
||||
override fun startFirmwareUpdate() = toRemoteExceptions {}
|
||||
|
||||
override fun getMyNodeInfo(): MyNodeInfo? = this@MeshService.myNodeInfo?.toMyNodeInfo()
|
||||
|
||||
|
|
@ -2039,24 +2060,17 @@ class MeshService : Service() {
|
|||
override fun send(p: DataPacket) {
|
||||
toRemoteExceptions {
|
||||
if (p.id == 0) p.id = generatePacketId()
|
||||
|
||||
val bytes = p.bytes!!
|
||||
Timber.i(
|
||||
"sendData dest=${p.to}, id=${p.id} <- ${bytes.size} bytes" +
|
||||
" (connectionState=${connectionStateHolder.getState()})",
|
||||
"sendData dest=${p.to}, id=${p.id} <- ${bytes.size} bytes (connectionState=${connectionStateHolder.getState()})",
|
||||
)
|
||||
|
||||
if (p.dataType == 0) {
|
||||
throw Exception("Port numbers must be non-zero!") // we are now more strict
|
||||
}
|
||||
|
||||
if (p.dataType == 0) throw Exception("Port numbers must be non-zero!")
|
||||
if (bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN.number) {
|
||||
p.status = MessageStatus.ERROR
|
||||
throw RemoteException("Message too long")
|
||||
} else {
|
||||
p.status = MessageStatus.QUEUED
|
||||
}
|
||||
|
||||
if (connectionStateHolder.getState() == ConnectionState.CONNECTED) {
|
||||
try {
|
||||
sendNow(p)
|
||||
|
|
@ -2068,10 +2082,7 @@ class MeshService : Service() {
|
|||
enqueueForSending(p)
|
||||
}
|
||||
serviceBroadcasts.broadcastMessageStatus(p)
|
||||
|
||||
// Keep a record of DataPackets, so GUIs can show proper chat history
|
||||
rememberDataPacket(p, false)
|
||||
|
||||
analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType))
|
||||
}
|
||||
}
|
||||
|
|
@ -2080,7 +2091,6 @@ class MeshService : Service() {
|
|||
this@MeshService.localConfig.toByteArray() ?: throw NoDeviceConfigException()
|
||||
}
|
||||
|
||||
/** Send our current radio config to the device */
|
||||
override fun setConfig(payload: ByteArray) = toRemoteExceptions {
|
||||
setRemoteConfig(generatePacketId(), myNodeNum, payload)
|
||||
}
|
||||
|
|
@ -2089,7 +2099,7 @@ class MeshService : Service() {
|
|||
Timber.d("Setting new radio config!")
|
||||
val config = ConfigProtos.Config.parseFrom(payload)
|
||||
packetHandler.sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setConfig = config })
|
||||
if (num == myNodeNum) setLocalConfig(config) // Update our local copy
|
||||
if (num == myNodeNum) setLocalConfig(config)
|
||||
}
|
||||
|
||||
override fun getRemoteConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
|
||||
|
|
@ -2104,12 +2114,11 @@ class MeshService : Service() {
|
|||
)
|
||||
}
|
||||
|
||||
/** Send our current module config to the device */
|
||||
override fun setModuleConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
|
||||
Timber.d("Setting new module config!")
|
||||
val config = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
|
||||
packetHandler.sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setModuleConfig = config })
|
||||
if (num == myNodeNum) setLocalModuleConfig(config) // Update our local copy
|
||||
if (num == myNodeNum) setLocalModuleConfig(config)
|
||||
}
|
||||
|
||||
override fun getModuleConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
|
||||
|
|
@ -2176,7 +2185,6 @@ class MeshService : Service() {
|
|||
override fun getNodes(): MutableList<NodeInfo> = toRemoteExceptions {
|
||||
val r = nodeDBbyNodeNum.values.map { it.toNodeInfo() }.toMutableList()
|
||||
Timber.i("in getOnline, count=${r.size}")
|
||||
// return arrayOf("+16508675309")
|
||||
r
|
||||
}
|
||||
|
||||
|
|
@ -2209,31 +2217,22 @@ class MeshService : Service() {
|
|||
|
||||
override fun requestPosition(destNum: Int, position: Position) = toRemoteExceptions {
|
||||
if (destNum != myNodeNum) {
|
||||
// Determine the best position to send based on user preferences and available
|
||||
// data
|
||||
val provideLocation = meshPrefs.shouldProvideNodeLocation(myNodeNum)
|
||||
val currentPosition =
|
||||
when {
|
||||
// Use provided position if valid and user allows phone location sharing
|
||||
provideLocation && position.isValid() -> position
|
||||
// Otherwise use the last valid position from nodeDB (node GPS or
|
||||
// static)
|
||||
else -> nodeDBbyNodeNum[myNodeNum]?.position?.let { Position(it) }?.takeIf { it.isValid() }
|
||||
}
|
||||
|
||||
if (currentPosition == null) {
|
||||
Timber.d("Position request skipped - no valid position available")
|
||||
return@toRemoteExceptions
|
||||
}
|
||||
|
||||
// Convert Position to MeshProtos.Position for the payload
|
||||
val meshPosition = position {
|
||||
latitudeI = Position.degI(currentPosition.latitude)
|
||||
longitudeI = Position.degI(currentPosition.longitude)
|
||||
altitude = currentPosition.altitude
|
||||
time = currentSecond()
|
||||
}
|
||||
|
||||
packetHandler.sendToRadio(
|
||||
newMeshPacketTo(destNum).buildMeshPacket(
|
||||
channel = nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue