-
Notifications
You must be signed in to change notification settings - Fork 5
Flic Duo Protocol Specification

The Flic Duo is backwards compatible with the Flic 2 Protocol Specification (the "base specification"). This means that an implementation using the base specification will be able to communicate with Flic Duo as well. However, only button events originating from the bigger of the two buttons will be sent in that case. In order to support more features, the Flic Duo protocol extends the Flic 2 protocol by adding new packet types as well as extending the content of some existing packet types. As specified by the base specification, packet structures may in the future be extended at the end and any extraneous unknown data should be ignored. Also, some bits previously reserved for future use are now in use.
This new extended protocol supports both Flic 2 and Flic Duo. As this is an extension, what is mentioned in the old specification applies unless overridden by this specification.
The FullVerifyRequest2 structure is extended as follows:
struct FullVerifyRequest2 {
uint8_t opcode;
uint8_t ecdh_public_key[32];
uint8_t random_bytes[8];
uint8_t rfu: 7;
uint8_t supports_duo: 1;
uint8_t verifier[16];
} PACKED;Implementations of this specification shall set supports_duo to 1. The base specification mentions that fullVerifySecret shall be created by hashing some data ending with a byte containing 0. Implementations of this specification shall instead use a byte containing 0x80, which corresponds to supports_duo being set to 1.
The FullVerifyResponse2 structure is extended as follows.
struct FullVerifyResponse2 {
uint8_t opcode;
uint8_t app_credentials_match: 1;
uint8_t cares_about_app_credentials: 1;
uint8_t is_duo: 1;
uint8_t rfu: 5;
uint8_t button_uuid[16];
uint8_t name_len;
char name[23];
uint32_t firmware_version;
uint16_t battery_level;
char serial_number[11];
char color[16];
} PACKED;An is_duo field has been added, which will be 1 for Flic Duo and 0 for other device types. If the request had supports_duo set to 0, the is_duo field will however be 0.
A null-terminated string containing the color of the device has been added. Currently we have devices using the colors "black" and "white".
The QuickVerifyRequest has been modified as follows:
struct QuickVerifyRequest {
uint8_t opcode;
uint8_t random_client_bytes[7];
uint8_t rfu0: 6
uint8_t supports_duo: 1;
uint8_t rfu1: 1;
uint32_t tmp_id;
uint32_t pairing_identifier;
} PACKED;Implementations of this specification shall set supports_duo to 1.
To create the session key, the base specification says "First a 16-byte message is produced by concatenating random_client_bytes, one byte containing 0 and random_button_bytes.". Implementations of this specification shall instead set that one byte to 0x40 (indicating that the supports_duo field was set to 1).
The response has been modified as follows:
struct QuickVerifyResponse {
uint8_t opcode;
uint8_t random_button_bytes[8];
uint32_t tmp_id;
uint8_t link_is_encrypted: 1;
uint8_t has_bond_info: 1;
uint8_t is_duo: 1;
uint8_t rfu: 5;
} PACKED;The is_duo field here is the same as in FullVerifyResponse2.
A new request has been added:
#define OPCODE_TO_FLIC_GET_COLOR_REQUEST 40
struct GetColorRequest {
uint8_t opcode;
};with a corresponding response:
#define OPCODE_FROM_FLIC_GET_COLOR_RESPONSE 34
struct GetColorResponse {
uint8_t opcode;
char color[16];
} PACKED;This request can only be sent signed on an established session. It will return the color in the same way as in FullVerifyResponse2.
The firmware update procedure for Flic Duo has been modified compared to the procedure in Flic 2. If attempting to update the firmware of a Flic Duo using the procedures in the base specification, the backend at the specified API endpoint will return null for a Flic Duo that has a new firmware. The response to a StartFirmwareUpdateRequest for Flic Duo will always be -1.
Implementations of this specification shall use the new API endpoint https://api.flic.io/api/v1/buttons/versions/firmware3 that otherwise behaves the same as the old endpoint. If is_duo returned earlier by the device (in FullVerifyResponse2 or QuickVerifyResponse) in the same session was 0, the firmware update procedure shall continue as in the old specification. If 1, the firmware update procedure shall continue as follows.
The firmware update file shall be split into two parts: the first 76 bytes make up the header and the remaining bytes make up the data.
#define OPCODE_TO_FLIC_START_FIRMWARE_UPDATE_DUO_REQUEST 38
struct StartFirmwareUpdateDuoRequest {
uint8_t opcode;
uint32_t len;
uint8_t header[76];
uint16_t status_interval;
} PACKED;The header field shall be set as instructed above and the len field shall be set to the number of bytes of data. The status_interval field should typically be set to 2 for optimal performance. The button will then send a response:
#define OPCODE_FROM_FLIC_START_FIRMWARE_UPDATE_RESPONSE 18
struct StartFirmwareUpdateResponse {
uint8_t opcode;
int start_pos;
} PACKED;If the request failed, start_pos will contain a negative number. The following values for negative start_pos are currently defined (all other negative numbers shall be treated as unknown errors):
- -1: Invalid request parameters.
- -2: A firmware update is already ongoing, potentially on a different session. Terminating the corresponding session will also cancel the firmware update.
- -3: A firmware update is already complete. Please disconnect and the button will reboot.
Otherwise, start_pos indicates the byte index the firmware update shall start from. If this index is not 0, a firmware update was interrupted and shall now be continued.
The data shall now be written using multiple chunks, split up arbitrarily, having the following packet format:
#define OPCODE_TO_FLIC_FIRMWARE_UPDATE_DATA_DUO_IND 39
struct FirmwareUpdateDataDuoInd {
uint8_t opcode;
uint8_t data[];
} PACKED;The length of the data array must be positive. It is suggested to use a maximum chunk size of 110 bytes.
If status_interval is nonzero, then when the number of data packets processed modulo status_interval is 0, the button will send back a notification:
#define OPCODE_FROM_FLIC_FIRMWARE_UPDATE_NOTIFICATION 19
struct FirmwareUpdateNotification {
uint8_t opcode;
int pos;
} PACKED;The pos fields indicates the number of bytes that have been processed. This notification will also be sent when all bytes have been written. In this case pos indicates the length of the firmware data in bytes which means success. If pos is instead 0, it indicates the firmware did not pass the signature verification test and was rejected.
Note that if there is a congestion of traffic from the Flic Duo to the client and there would be multiple queued such notifications, the older notifications may be dropped.
To achieve good performance, it is recommended to always send as many and as large FirmwareUpdateDataDuoInd packets as possible, but not having too much outstanding data that has not yet been acknowledged. It is suggested to not have more than 550 bytes outstanding data. After receiving a new FirmwareUpdateNotification, more packets may be sent.
After a firmware update has successfully been performed, the BLE connection to the button shall be disconnected. The button will reboot when the connection has been disconnected. If it's desired to automatically start advertising (in order to automatically reconnect), send a ForceBtDisconnectInd packet, with the restart field set to true.
The Flic Duo has a Push-Twist feature, enabling the client to get continuous updates of the rotation angle as long as at least one button is held. This feature shall only be used if is_duo is 1.
#define OPCODE_TO_FLIC_ENABLE_PUSH_TWIST_IND 37
struct EnablePushTwistInd {
uint8_t opcode;
uint8_t buttons: 2; // bitmask, one per button
uint8_t rfu: 6;
} PACKED;The feature can be enabled or disabled using the above packet. The first bit represents the setting for the big button and the second bit represents the setting for the small button.
The updates will be delivered as follows:
#define OPCODE_FROM_FLIC_PUSH_TWIST_DATA_NOTIFICATION 33
struct PushTwistDataNotification {
uint8_t opcode;
uint8_t buttons_pressed: 2; // one bit per button
uint8_t is_first_event: 2; // since button was pressed down, one bit per button
uint8_t buttons_pressed_for_at_least_half_a_second: 2; // since button was pressed down, one bit per button
uint8_t rfu: 2;
int angle_diff;
} PACKED;These updates will be sent approximately once per BLE connection interval.
The angle_diff parameter (signed integer) contains how much the Flic Duo has been rotated since the last event. The unit is such that 360 degrees equals 65536 or 0x10000. A positive diff indicates a twist to the right and a negative diff indicates a twist to the left. Each time both buttons are released and at least one button is pressed again, the internal current angle state will be reset. Hence, the first twist notification typically contains a small diff and not e.g. a diff corresponding to 180 degrees just because the Flic Duo has been rotated 180 degrees while both buttons were released.
This feature can only be used on an established session.
Compared to Flic 2, Flic Duo has two buttons as well as an accelerometer including built-in gesture recognition (swipe in four directions). The mechanism for sending button events has therefore been modified. When is_duo is 1, these packets shall be used instead of the old ones.
Flic Duo has a big button and a small button. Whenever the buttons are numbered or there is an array of two entries, one per button, the big button is number 0 and the small button is number 1.
#define OPCODE_TO_FLIC_INIT_BUTTON_EVENTS_DUO_LIGHT_REQUEST 35
struct InitButtonEventsDuoLightRequest {
uint8_t opcode;
uint32_t event_count[2];
uint32_t boot_id;
uint64_t auto_disconnect_time: 9;
uint64_t max_queued_packets: 5;
uint64_t max_queued_packets_age: 20;
uint64_t rfu: 6;
} PACKED;
#define OPCODE_FROM_FLIC_INIT_BUTTON_EVENTS_DUO_RESPONSE_WITH_BOOT_ID 30
struct InitButtonEventsDuoResponseWithoutBootId {
uint8_t opcode;
uint64_t has_queued_events: 1;
uint64_t timestamp: 47; // ms
uint32_t event_count[2];
} PACKED;
#define OPCODE_FROM_FLIC_INIT_BUTTON_EVENTS_DUO_RESPONSE_WITHOUT_BOOT_ID 31
struct InitButtonEventsDuoResponseWithBootId {
uint8_t opcode;
uint64_t has_queued_events: 1;
uint64_t timestamp: 47; // ms
uint32_t event_count[2];
uint32_t boot_id;
} PACKED;
#define OPCODE_FROM_FLIC_BUTTON_EVENT_DUO_NOTIFICATION 32
struct ButtonEventDuoNotification {
uint8_t opcode;
uint8_t events_data[];
} PACKED;
#define OPCODE_TO_FLIC_ACK_BUTTON_EVENTS_DUO_IND 36
struct AckButtonEventsDuoInd {
uint8_t opcode;
uint32_t event_count[2];
} PACKED;The differences compared to the old specification are:
- The
event_countis an array, one entry per button. - Timestamps now use milliseconds instead of 1/32768 of a second.
- The event notifications have a totally new, more efficient, encoding.
The client shall maintain the following variables for the session:
uint64_t last_timestamp;
uint32_t event_count[2];
bool end_of_queue_marker_received;The last_timestamp and end_of_queue_marker_received values shall be set to 0 and false, respectively, when the session is created. The event_count shall be set to the corresponding values in the init response packet. If the has_queued_events field is 0 in the init response packet, the end_of_queue_marker_received value shall be set to true.
Updates are delivered in ButtonEventDuoNotification packets. The events_data byte array should be seen as a bit array. As a reminder, little endian bit and byte order is used. This means that the first bit in the byte array is events_data[0] & 1. We will, per specific rules, extract the next x bits at a time. Let's call this operation extract_bits(x), which extracts x bits, converts it to a little endian integer, and then moves the bit position forward x bits. We will use the syntax {a, b, c, d} for describing an array. This can be followed by [extract_bits(2)]; it means we shall select a value based on a lookup table. When it is instructed that "if extract_bits(x) returns y, do ..." then that "otherwise, if extract_bits(x) returns y", it means that the extract_bits operation must be executed another time if the first condition did not hold. This kind of flow can therefore be directly translated to if-else statements calling this function. When the specification mentions a extract_bits(x) call, it is important to call this, even if the particular implementation does not make use of the value, to increase the bit position and stay in sync with the protocol.
The events_data array can contain multiple updates concatenated, so a bit reader should be implemented that continue as long as there are bits remaining. If there are less than one full byte remaining, the decoder shall however stop, since that byte cannot alone contain one update.
Each update shall be decoded as follows:
First, extract_bits(1) indicating button number.
Then, if this is the first update for this button in this packet, an event counter diff decoded as follows:
If extract_bits(1) returns 0, the diff is 0. Otherwise, if extract_bits(1) returns 0, the diff is 1. Otherwise, the diff is extract_bits({2, 4, 8, 32}[extract_bits(2)]). To this mentioned diff, 1 should be added and this value should then be written to event_count[button_number].
If this was not the first update for this button in this packet, event_count[button_number] shall be incremented by one.
Then, a timestamp delta decoded as extract_bits({8, 10, 13, 16, 24, 32, 40, 48}[extract_bits(3)]). The last_timestamp shall then be increased by this value. The resulting value indicates the absolute timestamp in milliseconds of the event.
Then, if end_of_queue_marker has not yet been received, we shall do the following:
If extract_bits(1) returns 0, this event does not mark the end of the event queue. Otherwise, this event marks the end of the queue, so end_of_queue_marker shall be set to true. In that case, if extract_bits(1) returns 0, this event is the actual last item in the event queue, otherwise, the last event queued had to be discarded; this event is the first non-queued event.
Then follows the actual event data.
First perform extract_bits(3) to get the event type. The following table shows a description:
Value (type) |
Description |
|---|---|
| 0 | up < 0.5 sec (can't decide yet whether this first click will result in a single or a double click) |
| 1 | up between 0.5 and 1 sec (no double click) |
| 2 | up >= 1 sec (no double click) |
| 3 | up < 0.5 sec (second click of a double click) |
| 4 | up >= 0.5 sec (second click of a double click) |
| 5 | down |
| 6 | single click timeout |
| 7 | hold |
In case the value is 4, if extract_bits(1) is 0, the button was released after less than 1 second, otherwise, after at least one second. Assign the returned bit to the newly created variable the_double_click_was_also_a_hold.
In case the value is 7, assign extract_bits(1) to the newly created variable next_up_will_be_double_click.
In case the value is <= 5 and event_count[button_number] is even, it means that an up or down event was not preceded by hold or single click timeout and therefore event_count[button_number] shall now be incremented by one.
The event_count[button_number] now contains the correct event count for this event.
For all "up" types and single click timeout:
If extract_bits(1) returns 0, it was determined that no gesture was performed. Otherwise, if extract_bits(1) returns 0, it was determined that a gesture was performed, but it was not recognized; otherwise, the gesture is determined based on the following table of extract_bits(2):
| Value | Direction |
|---|---|
| 0 | Left |
| 1 | Right |
| 2 | Up |
| 3 | Down |
Finally, for all events, x y z accelerometer vector values are also sent (call extract_bits(8) three times to extract x, y and z). Each value is to be interpreted as an 8-bit signed integer. The unit is such that after the value is divided by the floating point value 64.036875, a value of 1.0 represents 1 g.
Just as in the old specification, we have the same four different use cases. To emit the correct events we do the following:
First set these variables:
-
was_hold:type == 2 || (type == 4 && the_double_click_was_also_a_hold) -
single_click:type == 1 || type == 2 -
double_click:type == 3 || type == 4
We can now parse the event according to the four uses cases given the above variables:
- Button up:
type <= 4 - Button down:
type == 5
- Click:
type <= 4 && !was_hold - Hold:
type == 7
- Single click:
single_click - Double click:
double_click
- Single click:
(!was_hold && single_click) || (type == 6) - Double click:
double_click - Hold:
type == 7 && !next_up_will_be_double_click
If a condition above is true, an event should be emitted to the user if that use case is desired.
What otherwise applies to button events in the old specification shall also apply to button events in this specification. For button event acknowledgements sent to the Flic Duo, the event_count for both buttons shall be included, even if only one of the two values have actually been updated. An acknowledgement shall be sent when single_click || double_click || type == 6 is true in any of the updates.