chore: enhance bluetooth and wifi connection logging (#3960)

This commit is contained in:
Mac DeCourcy 2025-12-13 12:44:23 -08:00 committed by GitHub
parent 3a74389eaa
commit f51b7fb0f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 279 additions and 37 deletions

View file

@ -95,6 +95,11 @@ constructor(
private val writeMutex = Mutex()
private var peripheral: Peripheral? = null
private var connectionStartTime: Long = 0
private var packetsReceived: Int = 0
private var packetsSent: Int = 0
private var bytesReceived: Long = 0
private var bytesSent: Long = 0
private var toRadioCharacteristic: RemoteCharacteristic? = null
private var fromNumCharacteristic: RemoteCharacteristic? = null
@ -121,7 +126,12 @@ constructor(
}
private fun dispatchPacket(packet: ByteArray) {
Timber.d("[$address] Dispatching packet to service.handleFromRadio()")
packetsReceived++
bytesReceived += packet.size
Timber.d(
"[$address] Dispatching packet to service.handleFromRadio() - " +
"Packet #$packetsReceived, ${packet.size} bytes (Total: $bytesReceived bytes)",
)
try {
service.handleFromRadio(p = packet)
} catch (t: Throwable) {
@ -157,14 +167,20 @@ constructor(
private fun connect() {
connectionScope.launch {
try {
connectionStartTime = System.currentTimeMillis()
Timber.i("[$address] BLE connection attempt started at $connectionStartTime")
peripheral = retryCall { findAndConnectPeripheral() }
peripheral?.let {
val connectionTime = System.currentTimeMillis() - connectionStartTime
Timber.i("[$address] BLE peripheral connected in ${connectionTime}ms")
onConnected()
observePeripheralChanges()
discoverServicesAndSetupCharacteristics(it)
}
} catch (e: Exception) {
Timber.e(e, "[$address] Failed to connect to peripheral")
val failureTime = System.currentTimeMillis() - connectionStartTime
Timber.e(e, "[$address] Failed to connect to peripheral after ${failureTime}ms")
service.onDisconnect(BleError.from(e))
}
}
@ -196,21 +212,35 @@ constructor(
private fun observePeripheralChanges() {
peripheral?.let { p ->
p.phy.onEach { phy -> Timber.d("[$address] PHY changed to $phy") }.launchIn(connectionScope)
p.phy.onEach { phy -> Timber.i("[$address] BLE PHY changed to $phy") }.launchIn(connectionScope)
p.connectionParameters
.onEach { Timber.d("[$address] Connection parameters changed to $it") }
.onEach { params -> Timber.i("[$address] BLE connection parameters changed to $params") }
.launchIn(connectionScope)
p.state
.onEach { state ->
Timber.d("[$address] State changed to $state")
Timber.i("[$address] BLE connection state changed to $state")
if (state is ConnectionState.Disconnected) {
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
} else {
0
}
Timber.w(
"[$address] BLE disconnected - Reason: ${state.reason}, " +
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
"Packets TX: $packetsSent ($bytesSent bytes)",
)
service.onDisconnect(BleError.Disconnected(reason = state.reason))
}
}
.launchIn(connectionScope)
}
centralManager.state
.onEach { state -> Timber.d("CentralManager state changed to $state") }
.onEach { state -> Timber.i("[$address] CentralManager state changed to $state") }
.launchIn(connectionScope)
}
@ -314,8 +344,15 @@ constructor(
throw e
} catch (e: Exception) {
currentAttempt++
if (currentAttempt >= RETRY_COUNT) throw e
Timber.w(e, "[$address] Operation failed, retrying ($currentAttempt/$RETRY_COUNT)...")
if (currentAttempt >= RETRY_COUNT) {
Timber.e(e, "[$address] BLE operation failed after $RETRY_COUNT attempts, giving up")
throw e
}
Timber.w(
e,
"[$address] BLE operation failed (attempt $currentAttempt/$RETRY_COUNT), " +
"retrying in ${RETRY_DELAY_MS}ms...",
)
delay(RETRY_DELAY_MS)
}
}
@ -330,7 +367,10 @@ constructor(
*/
override fun handleSendToRadio(p: ByteArray) {
toRadioCharacteristic?.let { characteristic ->
if (peripheral == null) return@let
if (peripheral == null) {
Timber.w("[$address] BLE peripheral is null, cannot send packet")
return@let
}
connectionScope.launch {
writeMutex.withLock {
try {
@ -341,22 +381,43 @@ constructor(
WriteType.WITH_RESPONSE
}
retryCall {
Timber.d("[$address] Writing packet to toRadioCharacteristic with $writeType")
packetsSent++
bytesSent += p.size
Timber.d(
"[$address] Writing packet #$packetsSent to toRadioCharacteristic with $writeType - " +
"${p.size} bytes (Total TX: $bytesSent bytes)",
)
characteristic.write(p, writeType = writeType)
}
drainPacketQueueAndDispatch()
} catch (e: Exception) {
Timber.e(e, "[$address] Failed to write packet to toRadioCharacteristic")
Timber.e(
e,
"[$address] Failed to write packet to toRadioCharacteristic after " +
"$packetsSent successful writes",
)
service.onDisconnect(BleError.from(e))
}
}
}
} ?: Timber.w("[$address] toRadio unavailable, can't send data")
} ?: Timber.w("[$address] toRadio characteristic unavailable, can't send data")
}
/** Closes the connection to the device. */
override fun close() {
runBlocking {
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
} else {
0
}
Timber.i(
"[$address] BLE close() called - " +
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
"Packets TX: $packetsSent ($bytesSent bytes)",
)
connectionScope.cancel()
peripheral?.disconnect()
service.onDisconnect(true)

View file

@ -48,16 +48,30 @@ constructor(
override fun connect() {
val device = serialInterfaceSpec.findSerial(address)
if (device == null) {
Timber.e("Can't find device")
Timber.e("[$address] Serial device not found at address")
} else {
Timber.i("Opening $device")
val onConnect: () -> Unit = { super.connect() }
val connectStart = System.currentTimeMillis()
Timber.i("[$address] Opening serial device: $device")
var packetsReceived = 0
var bytesReceived = 0L
var connectionStartTime = 0L
val onConnect: () -> Unit = {
connectionStartTime = System.currentTimeMillis()
val connectionTime = connectionStartTime - connectStart
Timber.i("[$address] Serial device connected in ${connectionTime}ms")
super.connect()
}
usbRepository
.createSerialConnection(
device,
object : SerialConnectionListener {
override fun onMissingPermission() {
Timber.e("Need permissions for port")
Timber.e(
"[$address] Serial connection failed - missing USB permissions for device: $device",
)
}
override fun onConnected() {
@ -65,13 +79,29 @@ constructor(
}
override fun onDataReceived(bytes: ByteArray) {
Timber.d("Received ${bytes.size} byte(s)")
packetsReceived++
bytesReceived += bytes.size
Timber.d(
"[$address] Serial received packet #$packetsReceived - " +
"${bytes.size} byte(s) (Total RX: $bytesReceived bytes)",
)
bytes.forEach(::readChar)
}
override fun onDisconnected(thrown: Exception?) {
thrown?.let { e -> Timber.e("Serial error: $e") }
Timber.d("$device disconnected")
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
} else {
0
}
thrown?.let { e -> Timber.e(e, "[$address] Serial error after ${uptime}ms: ${e.message}") }
Timber.w(
"[$address] Serial device disconnected - " +
"Device: $device, " +
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes)",
)
onDeviceDisconnect(false)
}
},
@ -84,6 +114,12 @@ constructor(
}
override fun sendBytes(p: ByteArray) {
connRef.get()?.sendBytes(p)
val conn = connRef.get()
if (conn != null) {
Timber.d("[$address] Serial sending ${p.size} bytes")
conn.sendBytes(p)
} else {
Timber.w("[$address] Serial connection not available, cannot send ${p.size} bytes")
}
}
}

View file

@ -44,6 +44,7 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
const val SOCKET_TIMEOUT = 5000
const val SOCKET_RETRIES = 18
const val SERVICE_PORT = NetworkRepository.SERVICE_PORT
const val TIMEOUT_LOG_INTERVAL = 5 // Log every Nth timeout
}
private var retryCount = 1
@ -52,22 +53,45 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
private var socket: Socket? = null
private lateinit var outStream: OutputStream
private var connectionStartTime: Long = 0
private var packetsReceived: Int = 0
private var packetsSent: Int = 0
private var bytesReceived: Long = 0
private var bytesSent: Long = 0
private var timeoutEvents: Int = 0
init {
connect()
}
override fun sendBytes(p: ByteArray) {
packetsSent++
bytesSent += p.size
Timber.d("[$address] TCP sending packet #$packetsSent - ${p.size} bytes (Total TX: $bytesSent bytes)")
outStream.write(p)
}
override fun flushBytes() {
Timber.d("[$address] TCP flushing output stream")
outStream.flush()
}
override fun onDeviceDisconnect(waitForStopped: Boolean) {
val s = socket
if (s != null) {
Timber.d("Closing TCP socket")
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
} else {
0
}
Timber.w(
"[$address] TCP disconnecting - " +
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
"Packets TX: $packetsSent ($bytesSent bytes), " +
"Timeout events: $timeoutEvents",
)
s.close()
socket = null
}
@ -80,37 +104,66 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
try {
startConnect()
} catch (ex: IOException) {
Timber.e("IOException in TCP reader: $ex")
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
} else {
0
}
Timber.e(ex, "[$address] TCP IOException after ${uptime}ms - ${ex.message}")
onDeviceDisconnect(false)
} catch (ex: Throwable) {
val uptime =
if (connectionStartTime > 0) {
System.currentTimeMillis() - connectionStartTime
} else {
0
}
Timber.e(ex, "[$address] TCP exception after ${uptime}ms - ${ex.message}")
Exceptions.report(ex, "Exception in TCP reader")
onDeviceDisconnect(false)
}
if (retryCount > MAX_RETRIES_ALLOWED) break
if (retryCount > MAX_RETRIES_ALLOWED) {
Timber.e("[$address] TCP max retries ($MAX_RETRIES_ALLOWED) exceeded, giving up")
break
}
Timber.d("Reconnect attempt $retryCount in ${backoffDelay / 1000}s")
Timber.i(
"[$address] TCP reconnect attempt #$retryCount in ${backoffDelay / 1000}s " +
"(backoff: ${backoffDelay}ms)",
)
delay(backoffDelay)
retryCount++
backoffDelay = minOf(backoffDelay * 2, MAX_BACKOFF_MILLIS)
}
Timber.d("Exiting TCP reader")
Timber.i("[$address] TCP reader exiting")
}
}
// Create a socket to make the connection with the server
private suspend fun startConnect() = withContext(Dispatchers.IO) {
Timber.d("TCP connecting to $address")
val attemptStart = System.currentTimeMillis()
Timber.i("[$address] TCP connection attempt starting...")
val (host, port) =
address.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: SERVICE_PORT) }
Timber.d("[$address] Resolving host '$host' and connecting to port $port...")
Socket(InetAddress.getByName(host), port).use { socket ->
socket.tcpNoDelay = true
socket.soTimeout = SOCKET_TIMEOUT
this@TCPInterface.socket = socket
val connectTime = System.currentTimeMillis() - attemptStart
connectionStartTime = System.currentTimeMillis()
Timber.i(
"[$address] TCP socket connected in ${connectTime}ms - " +
"Local: ${socket.localSocketAddress}, Remote: ${socket.remoteSocketAddress}",
)
BufferedOutputStream(socket.getOutputStream()).use { outputStream ->
outStream = outputStream
@ -125,17 +178,33 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @
try { // close after 90s of inactivity
val c = inputStream.read()
if (c == -1) {
Timber.w("Got EOF on TCP stream")
Timber.w("[$address] TCP got EOF on stream after $packetsReceived packets received")
break
} else {
timeoutCount = 0
packetsReceived++
bytesReceived++
readChar(c.toByte())
}
} catch (ex: SocketTimeoutException) {
timeoutCount++
timeoutEvents++
if (timeoutCount % TIMEOUT_LOG_INTERVAL == 0) {
Timber.d(
"[$address] TCP socket timeout count: $timeoutCount/$SOCKET_RETRIES " +
"(total timeouts: $timeoutEvents)",
)
}
// Ignore and start another read
}
}
if (timeoutCount >= SOCKET_RETRIES) {
val inactivityMs = SOCKET_RETRIES * SOCKET_TIMEOUT
Timber.w(
"[$address] TCP closing connection due to $SOCKET_RETRIES consecutive timeouts " +
"(${inactivityMs}ms of inactivity)",
)
}
}
}
onDeviceDisconnect(false)