Skip to content

Commit 52c5d56

Browse files
authored
Add logging support (#131)
1 parent cbb9b40 commit 52c5d56

50 files changed

Lines changed: 1407 additions & 112 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,31 @@ To scan for nearby peripherals, the [`Scanner`] provides an [`advertisements`] [
1717
[`Advertisement`] objects representing advertisements seen from nearby peripherals. [`Advertisement`] objects contain
1818
information such as the peripheral's name and RSSI (signal strength).
1919

20+
The [`Scanner`] may be configured via the following DSL (shown are defaults, when not specified):
21+
22+
```kotlin
23+
val scanner = Scanner {
24+
services = null
25+
logging {
26+
engine = SystemLogEngine
27+
level = Warnings
28+
format = Multiline
29+
}
30+
}
31+
```
32+
33+
To filter scan results at the system level (recommended), specify a list of services the remote peripheral is
34+
advertising, for example:
35+
36+
```kotlin
37+
val scanner = Scanner {
38+
services = listOf(
39+
uuidFrom("f000aa80-0451-4000-b000-000000000000"),
40+
uuidFrom("f000aa81-0451-4000-b000-000000000000"),
41+
)
42+
}
43+
```
44+
2045
Scanning begins when the [`advertisements`] [`Flow`] is collected and stops when the [`Flow`] collection is terminated.
2146
A [`Flow`] terminal operator (such as [`first`]) may be used to scan until an advertisement is found that matches a
2247
desired predicate.
@@ -40,6 +65,8 @@ connection handling and I/O operations.
4065
val peripheral = scope.peripheral(advertisement)
4166
```
4267

68+
### Configuration
69+
4370
To configure a `peripheral`, options may be set in the builder lambda:
4471

4572
```kotlin
@@ -48,6 +75,70 @@ val peripheral = scope.peripheral(advertisement) {
4875
}
4976
```
5077

78+
#### Logging
79+
80+
By default, Kable only logs a small number of warnings when unexpected failures occur. To aid in debugging, additional
81+
logging may be enabled and configured via the `logging` DSL, for example:
82+
83+
```kotlin
84+
val peripheral = scope.peripheral(advertisement) {
85+
logging {
86+
level = Events // or Data
87+
}
88+
}
89+
```
90+
91+
The available log levels are:
92+
93+
- `Warnings`: Logs warnings when unexpected failures occur _(default)_
94+
- `Events`: Same as `Warnings` plus logs all events (e.g. writing to a characteristic)
95+
- `Data`: Same as `Events` plus string representation of I/O data
96+
97+
Available logging settings are as follows (all settings are optional; shown are defaults, when not specified):
98+
99+
```kotlin
100+
val peripheral = scope.peripheral(advertisement) {
101+
logging {
102+
engine = SystemLogEngine
103+
level = Warnings
104+
format = Multiline
105+
data = Hex
106+
}
107+
}
108+
```
109+
110+
The format of the logs can be either `Compact` (on a single line per log) or `Multiline` (spanning multiple lines for
111+
details):
112+
113+
| `Compact` | `Multiline` _(default)_ |
114+
|-----------|-------------------------|
115+
| <pre>example message(detail1=value1, detail2=value2, ...)</pre> | <pre>example message<br/> detail1: value1<br/> detail2: value2<br/> ...</pre> |
116+
117+
Display format of I/O data may be customized, either by configuring the `Hex` representation, or by providing a
118+
`DataProcessor`, for example:
119+
120+
```kotlin
121+
val peripheral = scope.peripheral(advertisement) {
122+
logging {
123+
data = Hex {
124+
separator = " "
125+
lowerCase = false
126+
}
127+
128+
// or...
129+
130+
data = DataProcessor { bytes ->
131+
// todo: Convert `bytes` to desired String representation, for example:
132+
bytes.joinToString { byte -> byte.toString() } // Show data as integer representation of bytes.
133+
}
134+
}
135+
}
136+
```
137+
138+
_I/O data is only shown in logs when logging `level` is set to `Data`._
139+
140+
#### Service Discovery
141+
51142
All platforms support an `onServicesDiscovered` action (that is executed after service discovery but before observations
52143
are wired up):
53144

buildSrc/src/main/kotlin/Dependencies.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ object androidx {
2727
version: String = "1.0.0"
2828
) = "androidx.startup:startup-runtime:$version"
2929
}
30+
31+
fun tuulbox(
32+
module: String,
33+
version: String = "4.3.0"
34+
) = "com.juul.tuulbox:$module:$version"

core/api/core.api

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ public final class com/juul/kable/Peripheral$DefaultImpls {
182182
public final class com/juul/kable/PeripheralBuilder {
183183
public final fun getPhy ()Lcom/juul/kable/Phy;
184184
public final fun getTransport ()Lcom/juul/kable/Transport;
185+
public final fun logging (Lkotlin/jvm/functions/Function1;)V
185186
public final fun onServicesDiscovered (Lkotlin/jvm/functions/Function2;)V
186187
public final fun setPhy (Lcom/juul/kable/Phy;)V
187188
public final fun setTransport (Lcom/juul/kable/Transport;)V
@@ -224,9 +225,17 @@ public abstract interface class com/juul/kable/Scanner {
224225
public abstract fun getAdvertisements ()Lkotlinx/coroutines/flow/Flow;
225226
}
226227

228+
public final class com/juul/kable/ScannerBuilder {
229+
public fun <init> ()V
230+
public final fun getServices ()Ljava/util/List;
231+
public final fun logging (Lkotlin/jvm/functions/Function1;)V
232+
public final fun setServices (Ljava/util/List;)V
233+
}
234+
227235
public final class com/juul/kable/ScannerKt {
228236
public static final fun Scanner (Ljava/util/List;)Lcom/juul/kable/Scanner;
229-
public static synthetic fun Scanner$default (Ljava/util/List;ILjava/lang/Object;)Lcom/juul/kable/Scanner;
237+
public static final fun Scanner (Lkotlin/jvm/functions/Function1;)Lcom/juul/kable/Scanner;
238+
public static synthetic fun Scanner$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/juul/kable/Scanner;
230239
}
231240

232241
public abstract interface class com/juul/kable/Service {
@@ -273,6 +282,10 @@ public final class com/juul/kable/State$Disconnected$Status$Cancelled : com/juul
273282
public static final field INSTANCE Lcom/juul/kable/State$Disconnected$Status$Cancelled;
274283
}
275284

285+
public final class com/juul/kable/State$Disconnected$Status$CentralDisconnected : com/juul/kable/State$Disconnected$Status {
286+
public static final field INSTANCE Lcom/juul/kable/State$Disconnected$Status$CentralDisconnected;
287+
}
288+
276289
public final class com/juul/kable/State$Disconnected$Status$ConnectionLimitReached : com/juul/kable/State$Disconnected$Status {
277290
public static final field INSTANCE Lcom/juul/kable/State$Disconnected$Status$ConnectionLimitReached;
278291
}
@@ -285,6 +298,14 @@ public final class com/juul/kable/State$Disconnected$Status$Failed : com/juul/ka
285298
public static final field INSTANCE Lcom/juul/kable/State$Disconnected$Status$Failed;
286299
}
287300

301+
public final class com/juul/kable/State$Disconnected$Status$L2CapFailure : com/juul/kable/State$Disconnected$Status {
302+
public static final field INSTANCE Lcom/juul/kable/State$Disconnected$Status$L2CapFailure;
303+
}
304+
305+
public final class com/juul/kable/State$Disconnected$Status$LinkManagerProtocolTimeout : com/juul/kable/State$Disconnected$Status {
306+
public static final field INSTANCE Lcom/juul/kable/State$Disconnected$Status$LinkManagerProtocolTimeout;
307+
}
308+
288309
public final class com/juul/kable/State$Disconnected$Status$PeripheralDisconnected : com/juul/kable/State$Disconnected$Status {
289310
public static final field INSTANCE Lcom/juul/kable/State$Disconnected$Status$PeripheralDisconnected;
290311
}
@@ -335,3 +356,66 @@ public final class com/juul/kable/WriteType : java/lang/Enum {
335356
public static fun values ()[Lcom/juul/kable/WriteType;
336357
}
337358

359+
public final class com/juul/kable/logs/HexBuilder {
360+
public final fun getLowerCase ()Z
361+
public final fun getSeparator ()Ljava/lang/String;
362+
public final fun setLowerCase (Z)V
363+
public final fun setSeparator (Ljava/lang/String;)V
364+
}
365+
366+
public final class com/juul/kable/logs/HexKt {
367+
public static final fun Hex (Lkotlin/jvm/functions/Function1;)Lcom/juul/kable/logs/Logging$DataProcessor;
368+
public static synthetic fun Hex$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/juul/kable/logs/Logging$DataProcessor;
369+
public static final fun getHex ()Lcom/juul/kable/logs/Logging$DataProcessor;
370+
}
371+
372+
public abstract interface class com/juul/kable/logs/LogEngine {
373+
public abstract fun assert (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
374+
public abstract fun debug (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
375+
public abstract fun error (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
376+
public abstract fun info (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
377+
public abstract fun verbose (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
378+
public abstract fun warn (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
379+
}
380+
381+
public final class com/juul/kable/logs/Logging {
382+
public fun <init> ()V
383+
public final fun getData ()Lcom/juul/kable/logs/Logging$DataProcessor;
384+
public final fun getEngine ()Lcom/juul/kable/logs/LogEngine;
385+
public final fun getFormat ()Lcom/juul/kable/logs/Logging$Format;
386+
public final fun getLevel ()Lcom/juul/kable/logs/Logging$Level;
387+
public final fun setData (Lcom/juul/kable/logs/Logging$DataProcessor;)V
388+
public final fun setEngine (Lcom/juul/kable/logs/LogEngine;)V
389+
public final fun setFormat (Lcom/juul/kable/logs/Logging$Format;)V
390+
public final fun setLevel (Lcom/juul/kable/logs/Logging$Level;)V
391+
}
392+
393+
public abstract interface class com/juul/kable/logs/Logging$DataProcessor {
394+
public abstract fun process ([B)Ljava/lang/String;
395+
}
396+
397+
public final class com/juul/kable/logs/Logging$Format : java/lang/Enum {
398+
public static final field Compact Lcom/juul/kable/logs/Logging$Format;
399+
public static final field Multiline Lcom/juul/kable/logs/Logging$Format;
400+
public static fun valueOf (Ljava/lang/String;)Lcom/juul/kable/logs/Logging$Format;
401+
public static fun values ()[Lcom/juul/kable/logs/Logging$Format;
402+
}
403+
404+
public final class com/juul/kable/logs/Logging$Level : java/lang/Enum {
405+
public static final field Data Lcom/juul/kable/logs/Logging$Level;
406+
public static final field Events Lcom/juul/kable/logs/Logging$Level;
407+
public static final field Warnings Lcom/juul/kable/logs/Logging$Level;
408+
public static fun valueOf (Ljava/lang/String;)Lcom/juul/kable/logs/Logging$Level;
409+
public static fun values ()[Lcom/juul/kable/logs/Logging$Level;
410+
}
411+
412+
public final class com/juul/kable/logs/SystemLogEngine : com/juul/kable/logs/LogEngine {
413+
public static final field INSTANCE Lcom/juul/kable/logs/SystemLogEngine;
414+
public fun assert (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
415+
public fun debug (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
416+
public fun error (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
417+
public fun info (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
418+
public fun verbose (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
419+
public fun warn (Ljava/lang/Throwable;Ljava/lang/String;Ljava/lang/String;)V
420+
}
421+

core/build.gradle.kts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,6 @@ android {
8484
minSdkVersion(21)
8585
}
8686

87-
buildFeatures {
88-
buildConfig = false
89-
}
90-
9187
lintOptions {
9288
isAbortOnError = true
9389
isWarningsAsErrors = true

core/src/androidMain/kotlin/BluetoothDevice.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import android.os.Build
1313
import android.os.Handler
1414
import android.os.HandlerThread
1515
import com.juul.kable.gatt.Callback
16+
import com.juul.kable.logs.Logging
1617
import kotlinx.coroutines.android.asCoroutineDispatcher
1718
import kotlinx.coroutines.flow.MutableStateFlow
1819
import kotlinx.coroutines.newSingleThreadContext
@@ -27,12 +28,13 @@ internal fun BluetoothDevice.connect(
2728
phy: Phy,
2829
state: MutableStateFlow<State>,
2930
mtu: MutableStateFlow<Int?>,
31+
logging: Logging,
3032
invokeOnClose: () -> Unit,
3133
): Connection? =
3234
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
33-
connectApi26(context, transport, phy, state, mtu, invokeOnClose)
35+
connectApi26(context, transport, phy, state, mtu, logging, invokeOnClose)
3436
} else {
35-
connectApi21(context, transport, state, mtu, invokeOnClose)
37+
connectApi21(context, transport, state, mtu, logging, invokeOnClose)
3638
}
3739

3840
/**
@@ -44,9 +46,10 @@ private fun BluetoothDevice.connectApi21(
4446
transport: Transport,
4547
state: MutableStateFlow<State>,
4648
mtu: MutableStateFlow<Int?>,
49+
logging: Logging,
4750
invokeOnClose: () -> Unit,
4851
): Connection? {
49-
val callback = Callback(state, mtu)
52+
val callback = Callback(state, mtu, logging, address)
5053
val bluetoothGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
5154
connectGatt(context, false, callback, transport.intValue)
5255
} else {
@@ -76,13 +79,14 @@ private fun BluetoothDevice.connectApi26(
7679
phy: Phy,
7780
state: MutableStateFlow<State>,
7881
mtu: MutableStateFlow<Int?>,
82+
logging: Logging,
7983
invokeOnClose: () -> Unit,
8084
): Connection? {
8185
val thread = HandlerThread(threadName).apply { start() }
8286
try {
8387
val handler = Handler(thread.looper)
8488
val dispatcher = handler.asCoroutineDispatcher()
85-
val callback = Callback(state, mtu)
89+
val callback = Callback(state, mtu, logging, address)
8690

8791
val bluetoothGatt =
8892
connectGatt(context, false, callback, transport.intValue, phy.intValue, handler)

core/src/androidMain/kotlin/Observers.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.juul.kable
22

3-
import android.util.Log
3+
import com.juul.kable.logs.Logger
4+
import com.juul.kable.logs.Logging
45
import kotlinx.coroutines.flow.Flow
56
import kotlinx.coroutines.flow.MutableSharedFlow
67
import kotlinx.coroutines.flow.filter
@@ -49,8 +50,11 @@ internal sealed class AndroidObservationEvent {
4950
*/
5051
internal class Observers(
5152
private val peripheral: AndroidPeripheral,
53+
logging: Logging,
5254
) {
5355

56+
private val logger = Logger(logging, tag = "Kable/Observers")
57+
5458
val characteristicChanges = MutableSharedFlow<AndroidObservationEvent>()
5559
private val observations = Observations()
5660

@@ -82,7 +86,7 @@ internal class Observers(
8286
} catch (e: NotReadyException) {
8387
// Silently ignore as it is assumed that failure is due to connection drop, in which case Android
8488
// will clear the notifications.
85-
Log.d(TAG, "Stop notification failure ignored.")
89+
logger.debug { message = "Stop notification failure ignored." }
8690
}
8791
}
8892
}

0 commit comments

Comments
 (0)