This library provides the shared foundation for the Bluetooth modules: the common GATT attribute types (Service, Characteristic, Descriptor), GattResponse, and the BluetoothFormat (de)serialization framework. It is consumed by both bluetooth-client and bluetooth-server.
Installing
This library is available on Maven Central. You can import Kaluga Bluetooth Base as follows:
repositories {
// ...
mavenCentral()
}
// ...
dependencies {
// ...
implementation("com.splendo.kaluga:bluetooth-base:$kalugaVersion")
}
Serialization
The BluetoothFormat can be used to easily (De)Serialize Objects into ByteArrays usable by most Bluetooth protocols. By default this will serialize as follows:
Booleans will be added to the next available byte. Up to 8 booleans in a row will fit in a single byte, their bit position determined by their order. Numbers will be serialized as their byte length implies (e.g. an Int is 4 bytes, a Short 2) Strings and Collections will be prefixed with a single byte containing their length. If longer encoding will fail. Polymorphic and Enum classes will serialize their serialName as a string with no length prefix. When an element is nullable, a nullable flag will be added to the flags header. The position in the flags header is determined by order of the element within the object.
Multiple flags are available to change this behaviour:
- Use
@Prefixto always add bytes to the start of the structure. - Use
@Postfixto always add bytes to the end of the structure. - Use
@Checksumto add checksum bytes (of widthChecksum.width) between the body and thePostfix. Checksum will be calculated over body only. When decoding, ifvalidateChecksumistruethe checksum will be automatically compared to the checksum in the data and throw an exception if they don’t match. - Use
@FlagIndexto change the position of the header flag(s) to be used for storing headers. If applied to a Boolean, the boolean will be stored as a flag instead of within the body itself. - Use
@FlagWidthto change the width of the flags to be used by this element. If the desired width is bigger than this width, flag will be ignored. - Use
@ByteOrderto change the byte order in which this element is encoded. Nested structures must have the same byte order, though primary types can change. - Use
@LengthPrefixto change the length prefix used in Strings or Collections. - Use
@Encodedto change the encoding used in Strings. - Use
@NullTerminatedto change the end marking of a Collection of String to be determined by a null byte instead of a length prefix - Use
@Unsignedto encode numbers as unsigned. - Use
@Scalarto encode numbers as a scalar value. Length can be modified using@Size, defaults toLength.`16_BIT`for Floats andLength.`32_BIT`for Doubles - Use
@MedFloatto encode numbers as a MedFloat value. Can have its sizing determined by@Size. WhenLength.`16_BIT`encodes ascom.splendo.kaluga.base.utils.MedFloat16, whenLength.`32_BIT`encodes ascom.splendo.kaluga.base.utils.MedFloat32. Any other@Sizeis not allowed. - Use
@Sizeto change the length of the bytes used to encode a numeric value. When multiple are added, the smallestLengththat fits the entire number will be used and flags will be added to the header to indicate which size was picked. For Float/Double values, this can only beLength.`32_BIT`orLength.`64_BIT`, for@MedFloatit is restricted toLength.`16_BIT`andLength.`32_BIT`. - Use
@Unsizedto mark a String or Collection as Unsized, meaning all remaining bytes (with the exception of any@Checksumor@Postfix) belong to this object. Attempting to encode data after will lead to an exception. - Use
@NullIfEmptyto mark a Collection as nullable if it is empty. When null its size will not be encoded. - Use
@SerializedByteValueto change the byte identifier of an Enum or Polymorphic class. This replaces serializing its serial name as an unsized string.
Equivalent flags are available to encode items in a List (e.g. ItemSize) or key/values in a Map (e.g. KeyEncoded, ValueNullTerminated)
As an Example, this is what the Bluetooth Heart Rate Characteristic looks like:
@Serializable
@JvmInline
value class RRInterval private constructor(
@Size(Length.`16_BIT`)
@Scalar(binaryExponent = 10)
val seconds: Double,
) {
constructor(duration: Duration) : this(duration.toDouble(DurationUnit.SECONDS))
val duration: Duration get() = seconds.seconds
}
@Serializable
enum class SensorLocation {
@SerializedByteValue(0x00)
OTHER,
@SerializedByteValue(0x01)
CHEST,
@SerializedByteValue(0x02)
WRIST,
@SerializedByteValue(0x03)
FINGER,
@SerializedByteValue(0x04)
HAND,
@SerializedByteValue(0x05)
EAR_LOBE,
@SerializedByteValue(0x06)
FOOT,
}
@Serializable
data class HeartRate(
@Size(Length.`8_BIT`)
@Size(Length.`16_BIT`)
@Unsigned
val heartRate: Int,
@FlagIndex(1)
val contactSupported: Boolean,
@FlagIndex(2)
val contactDetected: Boolean = !contactSupported,
@Unsigned
@Size(Length.`16_BIT`)
val energyExpended: Int? = null,
@NullIfEmpty
@Unsized
val rrIntervals: List<RRInterval> = emptyList(),
)