Appearance
Getting started
FlakeClient
The entry point for using the library is the FlakeClient. It is responsible for one flake connection and gives the possibility to establish / close a connection or register for specific services.
The FlakeClient gets created with a FlakeConfig, a FlakeLogger and a CoroutineContext. The FlakeConfig can be used to adjust the flake environment to your needs such as timeouts, behaviours, etc. The FlakeLogger is used inside the library to log certain events. You can either write an implementation yourself or use one from the flake-logging module. The coroutine context is used for specific asynchronous tasks that the library has to execute independently from the user.
Example code:
kotlin
val flakeConfig = FlakeConfig(
DISCONNECT_AFTER_TIMEOUT = true,
REPLY_MESSAGE_TIMEOUT_MS = 3000
)
val flakeLogger = TimberFlakeLogger()
val flakeClient = FlakeClient(
flakeConfig,
flakeLogger,
Dispatchers.IO
)
Creating a connection
For opening a connection, the flakeClient needs a wire. There are implementations for a wire and wire providers in the flake-ble and the flake-net module but you can also implement them for your specific needs yourself.
You should not use the wire other than providing it to the flakeClient.
Example code:
kotlin
val wire = TcpWire(...)
flakeClient.openConnection(
wire = wire
)
Defining a Service
After a connection is established, you can get flakeObjects that are representing a specific service of the device. In order to have an easy access to the service properties, the services are s specified by a data class and some annotations the library comes with.
The class has to be annotated with @FlakeTypeUUID(<UUID>)
which defines the UUID of the service. Each variable of the class should be annotated with @FlakeProperty(<id>, <modifier>)
which defines the propertyId and the property modifier like readOnly, actionable, volatile or array. For more information see the flake protocol documentation.
In case of Int / UInt types, the corresponding additional annotation is needed: @ExplicitFlakeInt8
, @ExplicitFlakeInt16
, @ExplicitFlakeInt32
, @ExplicitFlakeUInt8
, @ExplicitFlakeUInt16
, @ExplicitFlakeUInt32
Example code:
kotlin
@FlakeTypeUUID("00000000-1111-2222-3333-aaaabbbbcccc")
data class FlakeService(
@FlakeProperty(0x0001, readOnly = true)
@ExplicitFlakeInt8
val int8: FLAKE_INT8 = 0,
@FlakeProperty(0x0002, readOnly = true)
@ExplicitFlakeInt16
val int16: FLAKE_INT16 = 0,
@FlakeProperty(0x0003, readOnly = true)
@ExplicitFlakeInt32
val int32: FLAKE_INT32 = 0,
@FlakeProperty(0x0004, readOnly = true)
@ExplicitFlakeUInt8
val uint8: FLAKE_UINT8 = 0,
@FlakeProperty(0x0005, readOnly = true)
@ExplicitFlakeUInt16
val uint16: FLAKE_UINT16 = 0,
@FlakeProperty(0x0006, readOnly = true)
@ExplicitFlakeUInt32
val uint32: FLAKE_UINT32 = 0,
@FlakeProperty(0x0007, actionable = true)
val bool: FLAKE_BOOL = false,
@FlakeProperty(0x0008, readOnly = true)
val float: FLAKE_FLOAT = 0f,
@FlakeProperty(0x0009, readOnly = true)
val datetime: FLAKE_DATETIME = Date(0),
@FlakeProperty(0x000A, readOnly = true)
val uuid: FLAKE_UUID = UUID.fromString("00000000-1111-2222-3333-aaaabbbbcccc"),
@FlakeProperty(0x000B, readOnly = true)
val binary: FLAKE_BIN = byteArrayOf(),
@FlakeProperty(0x000C, readOnly = true)
val string: FLAKE_STRING = "",
)
Register/Query for Services
After a connection is established and the service defined, you can register or query for flakeObjects with that service. queryFor
will return a list of services once, where registerFor
also will give updates whenever a service gets created or destroyed. If you have a static number of services, queryFor
should be enough. If you have a dynamic number of services, you should use registerFor
.
The queryFor
function takes the service class (see above), where registerFor
also takes an created & destroyed callback. These then are called whenever a service is created or destroyed. Initially the current services are propagated via the created callback.
Example code:
kotlin
flakeClient.registerFor(
objectType = FlakeService::class,
onNewFlakeObject = {
// Gets initially called for all current flakeObjects
// and then for each new created one
},
onFlakeObjectDestroyed = {
// Gets called for each destroyed flakeObject
}
)
Get service properties
There are two ways to receive the service properties. Either with the getUpdates
or the registerForUpdates
function. getUpdates
is getting the current values of all service properties once where registerForUpdates
calls an update listener everytime a value changes. The update listener also offers the functionality to call some code before and after the values got set. Both functions are working with the given service object and are returning it.
In case you need a more specific access to properties, there is also a getProperties
function that let you request only specific values.
Note: getProperties
does not call the update listener!
Example code:
kotlin
// Will return a new FlakeService class with the updated values
val updatedService = flakeObject.getUpdate(FlakeService())
// Will return a GenericProperty with the updated value in it.
val updatedProperty = flakeObject.getProperties(
listOf(
GenericProperty(
PropertyHeader(PropertyType.UINT8, id = 0x0001u),
1
)
)
)
Set service properties
You can set the service properties with the setValues
function. Its takes a new flakeService object but since its a data class you can simply call the copy function and change the values you want to set. After the values updated, the update listener is called with the new set values.
In case you need a more specific access to properties, there is also a setProperties
function that let you update only specific values.
Note: setProperties
does not call the update listener!
Example code:
kotlin
// Updates all values of the given service class that differ from the current state
flakeObject.setValues(oldService.copy(int8 = 2))
// Updates the given property with the given value
flakeObject.setProperties(
listOf(
GenericProperty(
PropertyHeader(PropertyType.UINT8, id = 0x0001u),
2
)
)
)
Receive / Send custom messages
The update listener has a function receivedCustomMessage(functionName: String, message: Message)
which gets called whenever the flakeObject receives a custom message.
To send a custom message simply use the sendCustomMessage
function of the flakeObject.
Example code:
kotlin
val updateListener = object: FlakeObject.UpdateListener<FlakeService> {
[...]
// Receiving custom messages
override fun receivedCustomMessage(functionName: String, message: Message): MessageResult {
// Only react to a certain type of custom messages
if(functionName != "MyCustomMessage")
return MessageResult.Ignored
// We are expecting a payload. If not presend, something went wrong
if(!message.hasPayload)
return MessageResult.Failed
// Deserialize the payload
val decodedPaylod = PayloadParser.deserialize(
message.payload!!,
flakeConfig
)
return MessageResult.Ok
}
})
// Sending custom messages
flakeObject.sendCustomMessage(
"MyCustomMessage",
listOf(
GenericProperty(
PropertyHeader(PropertyType.STRING, id = 0x0001u),
"This is a custom message"
)
)
)
Lifecycle
Since the library is running certain tasks in the background like maintaining the connection, listen for incoming messages, etc. it should be lifecycle aware.
The FlakeClient
class is inheriting from the `LifecycleObserver`` and therefore can be added to your lifecycle.
Example code:
kotlin
class MainActivity: AppCompatActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Add Lifecycle Observer
getLifecycle().addObserver(flakeClient)
}
override fun onDestroy() {
super.onDestroy()
// Remove Lifecycle Observer
getLifecycle().removeObserver(flakeClient)
}
}