Add reliable Android BLE ride recorder

This commit is contained in:
Jay
2026-08-20 12:40:39 -04:00
parent 3bad2275cc
commit 6e408a3048
40 changed files with 2781 additions and 120 deletions
+34
View File
@@ -15,6 +15,12 @@ static uint32_t get_u32_le(const uint8_t *input)
((uint32_t)input[2] << 16) | ((uint32_t)input[3] << 24);
}
static uint64_t get_u64_le(const uint8_t *input)
{
return (uint64_t)get_u32_le(input) |
((uint64_t)get_u32_le(input + 4) << 32);
}
static void put_u16_le(uint8_t *output, uint16_t value)
{
output[0] = (uint8_t)value;
@@ -89,3 +95,31 @@ bool trikke_ble_decode_ack(
*packet_sequence = get_u32_le(ack + 4);
return true;
}
bool trikke_ble_decode_begin_session(
const uint8_t *command,
size_t command_size,
uint64_t *session_token)
{
if (command == NULL || session_token == NULL ||
command_size != TRIKKE_BLE_BEGIN_SESSION_SIZE ||
memcmp(command, "BGN1", 4) != 0) {
return false;
}
*session_token = get_u64_le(command + 4);
return true;
}
bool trikke_ble_update_subscription(
bool notify_enabled,
bool session_connection_ready,
bool *subscribed)
{
if (subscribed == NULL) {
return false;
}
const bool effective = notify_enabled && session_connection_ready;
const bool changed = effective != *subscribed;
*subscribed = effective;
return changed;
}
+15
View File
@@ -10,6 +10,7 @@ extern "C" {
#define TRIKKE_BLE_FRAGMENT_HEADER_SIZE 8
#define TRIKKE_BLE_ACK_SIZE 8
#define TRIKKE_BLE_BEGIN_SESSION_SIZE 12
// BLE data notifications carry a little-endian packet sequence, byte offset,
// total TRK1 frame size, then the frame bytes at that offset. The unchanged
@@ -29,6 +30,20 @@ bool trikke_ble_decode_ack(
size_t ack_size,
uint32_t *packet_sequence);
// A session begin is ASCII "BGN1" followed by a receiver-generated uint64
// token. Repeating the same token is idempotent across BLE reconnects.
bool trikke_ble_decode_begin_session(
const uint8_t *command,
size_t command_size,
uint64_t *session_token);
// Notification delivery is active only after both the CCCD and session
// authorization are ready. Returns true when the effective state changes.
bool trikke_ble_update_subscription(
bool notify_enabled,
bool session_connection_ready,
bool *subscribed);
#ifdef __cplusplus
}
#endif
+163 -6
View File
@@ -8,6 +8,8 @@
#include "host/ble_hs.h"
#include "host/ble_uuid.h"
#include "host/util/util.h"
#include "esp_attr.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
@@ -22,11 +24,24 @@
#define TRIKKE_BLE_MAX_ATT_PAYLOAD 253
#define TRIKKE_BLE_FRAGMENTS_PER_POLL 8
#define TRIKKE_BLE_ACK_TIMEOUT_US 1000000
#define TRIKKE_BLE_SESSION_RTC_MAGIC UINT32_C(0x54524b53)
#define TRIKKE_BLE_SESSION_RTC_XOR UINT32_C(0xa93cf17e)
#define TRIKKE_BLE_SESSION_RESET_DELAY_MS 50
static trikke_ble_transport_t *s_ble;
static uint8_t s_own_address_type;
static uint16_t s_data_value_handle;
static uint16_t s_ack_value_handle;
static uint16_t s_control_value_handle;
typedef struct {
uint32_t token_low;
uint32_t token_high;
uint32_t checksum;
uint32_t magic;
} trikke_ble_rtc_session_t;
RTC_NOINIT_ATTR static trikke_ble_rtc_session_t s_rtc_session;
// 7d2ea000-f75b-4a9b-8fbe-3d4c2a1e9c10 and adjacent characteristic UUIDs.
static const ble_uuid128_t TRIKKE_SERVICE_UUID =
@@ -38,6 +53,9 @@ static const ble_uuid128_t TRIKKE_DATA_UUID =
static const ble_uuid128_t TRIKKE_ACK_UUID =
BLE_UUID128_INIT(0x12, 0x9c, 0x1e, 0x2a, 0x4c, 0x3d, 0xbe, 0x8f,
0x9b, 0x4a, 0x5b, 0xf7, 0x00, 0xa0, 0x2e, 0x7d);
static const ble_uuid128_t TRIKKE_CONTROL_UUID =
BLE_UUID128_INIT(0x13, 0x9c, 0x1e, 0x2a, 0x4c, 0x3d, 0xbe, 0x8f,
0x9b, 0x4a, 0x5b, 0xf7, 0x00, 0xa0, 0x2e, 0x7d);
static uint32_t get_u32_le(const uint8_t *input)
{
@@ -45,6 +63,63 @@ static uint32_t get_u32_le(const uint8_t *input)
((uint32_t)input[2] << 16) | ((uint32_t)input[3] << 24);
}
// Caller must hold ble->lock. Keep the raw CCCD state independent from session
// authorization so either legal write order converges on the same effective
// subscription state.
static void update_subscribed_locked(trikke_ble_transport_t *ble)
{
if (trikke_ble_update_subscription(
ble->notify_enabled,
ble->session_connection_ready,
&ble->subscribed)) {
++ble->delivery_epoch;
}
}
static uint32_t rtc_session_checksum(uint32_t low, uint32_t high)
{
return TRIKKE_BLE_SESSION_RTC_MAGIC ^ low ^ high ^
TRIKKE_BLE_SESSION_RTC_XOR;
}
static void rtc_session_clear(void)
{
s_rtc_session.magic = 0;
s_rtc_session.token_low = 0;
s_rtc_session.token_high = 0;
s_rtc_session.checksum = 0;
}
static void rtc_session_store(uint64_t token)
{
const uint32_t low = (uint32_t)token;
const uint32_t high = (uint32_t)(token >> 32);
s_rtc_session.magic = 0;
s_rtc_session.token_low = low;
s_rtc_session.token_high = high;
s_rtc_session.checksum = rtc_session_checksum(low, high);
s_rtc_session.magic = TRIKKE_BLE_SESSION_RTC_MAGIC;
}
static bool rtc_session_load(uint64_t *token)
{
if (token == NULL || s_rtc_session.magic != TRIKKE_BLE_SESSION_RTC_MAGIC ||
s_rtc_session.checksum != rtc_session_checksum(
s_rtc_session.token_low, s_rtc_session.token_high)) {
return false;
}
*token = (uint64_t)s_rtc_session.token_low |
((uint64_t)s_rtc_session.token_high << 32);
return true;
}
static void session_restart_task(void *argument)
{
(void)argument;
vTaskDelay(pdMS_TO_TICKS(TRIKKE_BLE_SESSION_RESET_DELAY_MS));
esp_restart();
}
static int data_access(
uint16_t connection_handle,
uint16_t attribute_handle,
@@ -102,6 +177,69 @@ static int ack_access(
return accepted ? 0 : BLE_ATT_ERR_UNLIKELY;
}
static int session_access(
uint16_t connection_handle,
uint16_t attribute_handle,
struct ble_gatt_access_ctxt *context,
void *argument)
{
(void)attribute_handle;
(void)argument;
trikke_ble_transport_t *ble = s_ble;
if (ble == NULL || context->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
return BLE_ATT_ERR_UNLIKELY;
}
uint8_t command[TRIKKE_BLE_BEGIN_SESSION_SIZE] = {0};
uint16_t command_size = 0;
uint64_t requested_token = 0;
if (OS_MBUF_PKTLEN(context->om) != sizeof(command) ||
ble_hs_mbuf_to_flat(
context->om, command, sizeof(command), &command_size) != 0 ||
!trikke_ble_decode_begin_session(
command, command_size, &requested_token)) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
bool notify_session_ready = false;
bool restart = false;
portENTER_CRITICAL(&ble->lock);
if (!ble->connected || ble->connection_handle != connection_handle ||
ble->session_restart_pending) {
portEXIT_CRITICAL(&ble->lock);
return BLE_ATT_ERR_UNLIKELY;
}
if (ble->session_token_valid && ble->session_token == requested_token) {
ble->session_connection_ready = true;
update_subscribed_locked(ble);
if (!ble->session_started) {
ble->session_started = true;
notify_session_ready = true;
}
} else {
ble->session_connection_ready = false;
update_subscribed_locked(ble);
ble->session_restart_pending = true;
rtc_session_store(requested_token);
restart = true;
}
portEXIT_CRITICAL(&ble->lock);
if (notify_session_ready && ble->session_ready != NULL) {
ble->session_ready(ble->session_ready_context);
}
if (restart && xTaskCreate(
session_restart_task, "trikke_session_reset", 2048, NULL,
configMAX_PRIORITIES - 1, NULL) != pdPASS) {
rtc_session_clear();
portENTER_CRITICAL(&ble->lock);
ble->session_restart_pending = false;
portEXIT_CRITICAL(&ble->lock);
return BLE_ATT_ERR_UNLIKELY;
}
return 0;
}
static const struct ble_gatt_svc_def TRIKKE_GATT_SERVICES[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
@@ -119,6 +257,12 @@ static const struct ble_gatt_svc_def TRIKKE_GATT_SERVICES[] = {
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
.val_handle = &s_ack_value_handle,
},
{
.uuid = &TRIKKE_CONTROL_UUID.u,
.access_cb = session_access,
.flags = BLE_GATT_CHR_F_WRITE,
.val_handle = &s_control_value_handle,
},
{0},
},
},
@@ -169,7 +313,9 @@ static void on_reset(int reason)
++ble->counters.disconnect_count;
}
ble->connected = false;
ble->notify_enabled = false;
ble->subscribed = false;
ble->session_connection_ready = false;
ble->connection_handle = BLE_HS_CONN_HANDLE_NONE;
++ble->delivery_epoch;
portEXIT_CRITICAL(&ble->lock);
@@ -197,7 +343,9 @@ static int gap_event(struct ble_gap_event *event, void *argument)
if (event->connect.status == 0) {
portENTER_CRITICAL(&ble->lock);
ble->connected = true;
ble->notify_enabled = false;
ble->subscribed = false;
ble->session_connection_ready = false;
ble->connection_handle = event->connect.conn_handle;
++ble->delivery_epoch;
portEXIT_CRITICAL(&ble->lock);
@@ -212,7 +360,9 @@ static int gap_event(struct ble_gap_event *event, void *argument)
++ble->counters.disconnect_count;
}
ble->connected = false;
ble->notify_enabled = false;
ble->subscribed = false;
ble->session_connection_ready = false;
ble->connection_handle = BLE_HS_CONN_HANDLE_NONE;
++ble->delivery_epoch;
portEXIT_CRITICAL(&ble->lock);
@@ -222,11 +372,8 @@ static int gap_event(struct ble_gap_event *event, void *argument)
case BLE_GAP_EVENT_SUBSCRIBE:
if (event->subscribe.attr_handle == s_data_value_handle) {
portENTER_CRITICAL(&ble->lock);
const bool subscribed = event->subscribe.cur_notify != 0;
if (subscribed != ble->subscribed) {
ble->subscribed = subscribed;
++ble->delivery_epoch;
}
ble->notify_enabled = event->subscribe.cur_notify != 0;
update_subscribed_locked(ble);
portEXIT_CRITICAL(&ble->lock);
}
return 0;
@@ -431,7 +578,9 @@ static trikke_transport_status_t ble_poll_packet(void *context)
esp_err_t trikke_ble_transport_init(
trikke_ble_transport_t *ble,
trikke_transport_t *transport)
trikke_transport_t *transport,
trikke_ble_session_ready_fn session_ready,
void *session_ready_context)
{
if (ble == NULL || transport == NULL) {
return ESP_ERR_INVALID_ARG;
@@ -443,6 +592,14 @@ esp_err_t trikke_ble_transport_init(
memset(ble, 0, sizeof(*ble));
ble->lock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED;
ble->connection_handle = BLE_HS_CONN_HANDLE_NONE;
ble->session_ready = session_ready;
ble->session_ready_context = session_ready_context;
if (esp_reset_reason() == ESP_RST_SW &&
rtc_session_load(&ble->session_token)) {
ble->session_token_valid = true;
} else {
rtc_session_clear();
}
s_ble = ble;
esp_err_t error = nvs_flash_init();
+13 -1
View File
@@ -19,15 +19,23 @@ typedef struct {
uint32_t invalid_ack_count;
} trikke_ble_transport_counters_t;
typedef void (*trikke_ble_session_ready_fn)(void *context);
typedef struct {
portMUX_TYPE lock;
bool initialized;
bool connected;
bool notify_enabled;
bool subscribed;
bool frame_active;
bool frame_fully_sent_once;
bool ack_received;
bool session_token_valid;
bool session_connection_ready;
bool session_started;
bool session_restart_pending;
uint16_t connection_handle;
uint64_t session_token;
uint32_t delivery_epoch;
uint32_t frame_epoch;
uint32_t frame_sequence;
@@ -35,12 +43,16 @@ typedef struct {
size_t frame_size;
size_t next_offset;
int64_t ack_deadline_us;
trikke_ble_session_ready_fn session_ready;
void *session_ready_context;
trikke_ble_transport_counters_t counters;
} trikke_ble_transport_t;
esp_err_t trikke_ble_transport_init(
trikke_ble_transport_t *ble,
trikke_transport_t *transport);
trikke_transport_t *transport,
trikke_ble_session_ready_fn session_ready,
void *session_ready_context);
void trikke_ble_transport_get_counters(
trikke_ble_transport_t *ble,
+37 -16
View File
@@ -28,7 +28,7 @@
#define TRIKKE_I2C_FREQ_HZ 400000
#define TRIKKE_SAMPLE_RATE_HZ 100
#define TRIKKE_SAMPLE_TICKS pdMS_TO_TICKS(1000 / TRIKKE_SAMPLE_RATE_HZ)
#define TRIKKE_SAMPLE_QUEUE_DEPTH 1024
#define TRIKKE_SAMPLE_QUEUE_DEPTH 3072
#define TRIKKE_METADATA_INTERVAL_PACKETS 64
#define TRIKKE_STATUS_INTERVAL_PACKETS 64
#define TRIKKE_TRANSPORT_RETRY_DELAY_MS 10
@@ -59,9 +59,14 @@ typedef struct {
adxl345_t accelerometer;
l3g4200d_t gyroscope;
QueueHandle_t sample_queue;
StaticQueue_t sample_queue_control;
uint8_t sample_queue_storage[
TRIKKE_SAMPLE_QUEUE_DEPTH * sizeof(trikke_wire_sample_t)];
atomic_uint_least32_t sensor_read_failure_count;
atomic_uint_least32_t queue_overflow_count;
atomic_uint_least32_t loop_overrun_count;
TaskHandle_t output_task_handle;
TaskHandle_t acquisition_task_handle;
trikke_transport_t transport;
#if CONFIG_TRIKKE_TRANSPORT_BLE
trikke_ble_transport_t ble_transport;
@@ -147,6 +152,18 @@ static trikke_wire_status_t status_snapshot(
return status;
}
#if CONFIG_TRIKKE_TRANSPORT_BLE
static void session_ready(void *argument)
{
trikke_context_t *context = argument;
if (context == NULL) {
return;
}
xTaskNotifyGive(context->output_task_handle);
xTaskNotifyGive(context->acquisition_task_handle);
}
#endif
static void acquisition_task(void *argument)
{
trikke_context_t *context = argument;
@@ -407,8 +424,11 @@ void app_main(void)
usb_serial_jtag_vfs_set_tx_line_endings(ESP_LINE_ENDINGS_LF);
#endif
s_context.sample_queue =
xQueueCreate(TRIKKE_SAMPLE_QUEUE_DEPTH, sizeof(trikke_wire_sample_t));
s_context.sample_queue = xQueueCreateStatic(
TRIKKE_SAMPLE_QUEUE_DEPTH,
sizeof(trikke_wire_sample_t),
s_context.sample_queue_storage,
&s_context.sample_queue_control);
if (s_context.sample_queue == NULL) {
ESP_LOGE(TAG, "sample queue allocation failed");
l3g4200d_deinit(&s_context.gyroscope);
@@ -417,17 +437,15 @@ void app_main(void)
return;
}
TaskHandle_t output_task_handle = NULL;
TaskHandle_t acquisition_task_handle = NULL;
if (xTaskCreate(output_task, "trikke_output", 4096, &s_context, 5,
&output_task_handle) != pdPASS ||
&s_context.output_task_handle) != pdPASS ||
xTaskCreate(acquisition_task, "trikke_acquire", 4096, &s_context, 10,
&acquisition_task_handle) != pdPASS) {
if (output_task_handle != NULL) {
vTaskDelete(output_task_handle);
&s_context.acquisition_task_handle) != pdPASS) {
if (s_context.output_task_handle != NULL) {
vTaskDelete(s_context.output_task_handle);
}
if (acquisition_task_handle != NULL) {
vTaskDelete(acquisition_task_handle);
if (s_context.acquisition_task_handle != NULL) {
vTaskDelete(s_context.acquisition_task_handle);
}
vQueueDelete(s_context.sample_queue);
ESP_LOGE(TAG, "telemetry task creation failed");
@@ -439,14 +457,15 @@ void app_main(void)
#if CONFIG_TRIKKE_TRANSPORT_BLE
err = trikke_ble_transport_init(
&s_context.ble_transport, &s_context.transport);
&s_context.ble_transport, &s_context.transport,
session_ready, &s_context);
#else
err = trikke_usb_transport_init(
&s_context.usb_transport, &s_context.transport);
#endif
if (err != ESP_OK) {
vTaskDelete(output_task_handle);
vTaskDelete(acquisition_task_handle);
vTaskDelete(s_context.output_task_handle);
vTaskDelete(s_context.acquisition_task_handle);
vQueueDelete(s_context.sample_queue);
#if CONFIG_TRIKKE_TRANSPORT_BLE
ESP_LOGE(TAG, "BLE transport initialization failed: %s",
@@ -463,6 +482,8 @@ void app_main(void)
// No text may share the byte stream once framed binary output begins.
esp_log_level_set("*", ESP_LOG_NONE);
xTaskNotifyGive(output_task_handle);
xTaskNotifyGive(acquisition_task_handle);
#if !CONFIG_TRIKKE_TRANSPORT_BLE
xTaskNotifyGive(s_context.output_task_handle);
xTaskNotifyGive(s_context.acquisition_task_handle);
#endif
}