harden USB telemetry transport

This commit is contained in:
Jay
2026-08-17 14:51:12 -04:00
parent 1cf0a9ac77
commit 3c95f3d7be
17 changed files with 709 additions and 114 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
idf_component_register(
SRCS "trikke_sensor_main.c" "trikke_protocol.c"
SRCS "trikke_sensor_main.c" "trikke_protocol.c" "trikke_transport.c"
"trikke_usb_transport.c"
INCLUDE_DIRS "."
REQUIRES adxl345 l3g4200d esp_timer esp_driver_gpio esp_driver_i2c
esp_driver_usb_serial_jtag vfs
+39 -15
View File
@@ -14,6 +14,8 @@
#include "freertos/task.h"
#include "l3g4200d.h"
#include "trikke_protocol.h"
#include "trikke_transport.h"
#include "trikke_usb_transport.h"
// Seeed Studio XIAO ESP32-C3: D4/SDA = GPIO6, D5/SCL = GPIO7.
#define TRIKKE_I2C_PORT I2C_NUM_0
@@ -54,6 +56,8 @@ typedef struct {
QueueHandle_t sample_queue;
atomic_uint_least32_t dropped_sample_count;
atomic_uint_least32_t loop_overrun_count;
trikke_transport_t transport;
trikke_usb_transport_t usb_transport;
} trikke_context_t;
static trikke_context_t s_context;
@@ -157,22 +161,26 @@ static void acquisition_task(void *argument)
}
}
static bool write_binary_packet(const uint8_t *packet, size_t packet_size)
{
const size_t written = fwrite(packet, 1, packet_size, stdout);
const int flush_result = fflush(stdout);
const bool complete = written == packet_size && flush_result == 0;
if (!complete) {
clearerr(stdout);
}
return complete;
}
static void write_binary_packet_until_sent(
trikke_context_t *context,
trikke_transport_sender_t *sender,
const uint8_t *packet,
size_t packet_size)
{
while (!write_binary_packet(packet, packet_size)) {
while (true) {
const trikke_transport_status_t status =
trikke_transport_sender_step(
sender, &context->transport, packet, packet_size);
if (status == TRIKKE_TRANSPORT_COMPLETE) {
return;
}
if (status == TRIKKE_TRANSPORT_FATAL) {
// Preserve the in-flight packet and stop consuming the queue. A
// fatal backend invariant is not safely recoverable or retryable.
while (true) {
vTaskDelay(portMAX_DELAY);
}
}
vTaskDelay(pdMS_TO_TICKS(TRIKKE_TRANSPORT_RETRY_DELAY_MS));
}
}
@@ -185,12 +193,14 @@ static void output_task(void *argument)
uint8_t packet[TRIKKE_WIRE_MAX_PACKET_SIZE] = {0};
uint32_t packet_sequence = 0;
uint32_t sample_packet_count = 0;
trikke_transport_sender_t sender;
trikke_transport_sender_init(&sender);
size_t packet_size = trikke_encode_metadata_packet(
packet, sizeof(packet), packet_sequence++, esp_timer_get_time(),
atomic_load(&context->dropped_sample_count),
atomic_load(&context->loop_overrun_count), &TRIKKE_METADATA);
write_binary_packet_until_sent(packet, packet_size);
write_binary_packet_until_sent(context, &sender, packet, packet_size);
while (true) {
trikke_wire_sample_t samples[TRIKKE_WIRE_MAX_RECORDS] = {0};
@@ -227,14 +237,15 @@ static void output_task(void *argument)
packet, sizeof(packet), packet_sequence++, esp_timer_get_time(),
atomic_load(&context->dropped_sample_count),
atomic_load(&context->loop_overrun_count), &TRIKKE_METADATA);
write_binary_packet_until_sent(packet, packet_size);
write_binary_packet_until_sent(
context, &sender, packet, packet_size);
}
packet_size = trikke_encode_sample_packet(
packet, sizeof(packet), packet_sequence++,
atomic_load(&context->dropped_sample_count),
atomic_load(&context->loop_overrun_count), samples, sample_count);
write_binary_packet_until_sent(packet, packet_size);
write_binary_packet_until_sent(context, &sender, packet, packet_size);
++sample_packet_count;
}
}
@@ -324,6 +335,17 @@ void app_main(void)
// binary frames whenever a payload byte equals LF.
usb_serial_jtag_vfs_set_tx_line_endings(ESP_LINE_ENDINGS_LF);
err = trikke_usb_transport_init(
&s_context.usb_transport, &s_context.transport);
if (err != ESP_OK) {
ESP_LOGE(TAG, "USB transport initialization failed: %s",
esp_err_to_name(err));
l3g4200d_deinit(&s_context.gyroscope);
adxl345_deinit(&s_context.accelerometer);
i2c_del_master_bus(bus);
return;
}
s_context.sample_queue =
xQueueCreate(TRIKKE_SAMPLE_QUEUE_DEPTH, sizeof(trikke_wire_sample_t));
if (s_context.sample_queue == NULL) {
@@ -331,6 +353,7 @@ void app_main(void)
l3g4200d_deinit(&s_context.gyroscope);
adxl345_deinit(&s_context.accelerometer);
i2c_del_master_bus(bus);
trikke_usb_transport_deinit(&s_context.usb_transport);
return;
}
@@ -351,6 +374,7 @@ void app_main(void)
l3g4200d_deinit(&s_context.gyroscope);
adxl345_deinit(&s_context.accelerometer);
i2c_del_master_bus(bus);
trikke_usb_transport_deinit(&s_context.usb_transport);
return;
}
+43
View File
@@ -0,0 +1,43 @@
#include "trikke_transport.h"
static bool status_is_valid(trikke_transport_status_t status)
{
return status >= TRIKKE_TRANSPORT_COMPLETE &&
status <= TRIKKE_TRANSPORT_FATAL;
}
void trikke_transport_sender_init(trikke_transport_sender_t *sender)
{
if (sender != NULL) {
sender->pending = false;
}
}
trikke_transport_status_t trikke_transport_sender_step(
trikke_transport_sender_t *sender,
const trikke_transport_t *transport,
const uint8_t *packet,
size_t packet_size)
{
if (sender == NULL || transport == NULL || transport->begin == NULL ||
transport->poll == NULL || packet == NULL || packet_size == 0) {
return TRIKKE_TRANSPORT_FATAL;
}
const trikke_transport_status_t status = sender->pending
? transport->poll(transport->context)
: transport->begin(transport->context, packet, packet_size);
if (!status_is_valid(status)) {
sender->pending = false;
return TRIKKE_TRANSPORT_FATAL;
}
if (status == TRIKKE_TRANSPORT_PENDING) {
sender->pending = true;
} else {
// RETRY from poll is allowed only when the backend has discarded or
// otherwise resolved the old transfer and knows resubmission is safe.
sender->pending = false;
}
return status;
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
TRIKKE_TRANSPORT_COMPLETE = 0,
TRIKKE_TRANSPORT_RETRY,
TRIKKE_TRANSPORT_PENDING,
TRIKKE_TRANSPORT_FATAL,
} trikke_transport_status_t;
typedef trikke_transport_status_t (*trikke_transport_begin_fn)(
void *context,
const uint8_t *packet,
size_t packet_size);
typedef trikke_transport_status_t (*trikke_transport_poll_fn)(void *context);
typedef struct {
void *context;
trikke_transport_begin_fn begin;
trikke_transport_poll_fn poll;
} trikke_transport_t;
typedef struct {
bool pending;
} trikke_transport_sender_t;
void trikke_transport_sender_init(trikke_transport_sender_t *sender);
// Advances one bounded transport operation. The packet storage must remain valid
// and unchanged from the first PENDING result through COMPLETE. While pending,
// only poll is called: an ambiguous timeout can never duplicate a frame.
trikke_transport_status_t trikke_transport_sender_step(
trikke_transport_sender_t *sender,
const trikke_transport_t *transport,
const uint8_t *packet,
size_t packet_size);
#ifdef __cplusplus
}
#endif
+92
View File
@@ -0,0 +1,92 @@
#include "trikke_usb_transport.h"
#include "driver/usb_serial_jtag.h"
#include "driver/usb_serial_jtag_vfs.h"
#include "freertos/FreeRTOS.h"
#define TRIKKE_USB_TX_BUFFER_SIZE 512
#define TRIKKE_USB_RX_BUFFER_SIZE 128
#define TRIKKE_USB_OPERATION_TIMEOUT_MS 50
static trikke_transport_status_t usb_begin_packet(
void *context,
const uint8_t *packet,
size_t packet_size)
{
const trikke_usb_transport_t *usb = context;
if (usb == NULL || !usb->initialized || packet == NULL || packet_size == 0 ||
packet_size > TRIKKE_USB_TX_BUFFER_SIZE) {
return TRIKKE_TRANSPORT_FATAL;
}
const int written = usb_serial_jtag_write_bytes(
packet, packet_size,
pdMS_TO_TICKS(TRIKKE_USB_OPERATION_TIMEOUT_MS));
if (written == (int)packet_size) {
return TRIKKE_TRANSPORT_PENDING;
}
if (written == 0) {
return TRIKKE_TRANSPORT_RETRY;
}
return TRIKKE_TRANSPORT_FATAL;
}
static trikke_transport_status_t usb_poll_packet(void *context)
{
const trikke_usb_transport_t *usb = context;
if (usb == NULL || !usb->initialized) {
return TRIKKE_TRANSPORT_FATAL;
}
const esp_err_t err = usb_serial_jtag_wait_tx_done(
pdMS_TO_TICKS(TRIKKE_USB_OPERATION_TIMEOUT_MS));
if (err == ESP_OK) {
return TRIKKE_TRANSPORT_COMPLETE;
}
if (err == ESP_ERR_TIMEOUT) {
return TRIKKE_TRANSPORT_PENDING;
}
return TRIKKE_TRANSPORT_FATAL;
}
esp_err_t trikke_usb_transport_init(
trikke_usb_transport_t *usb,
trikke_transport_t *transport)
{
if (usb == NULL || transport == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (usb->initialized) {
return ESP_ERR_INVALID_STATE;
}
usb_serial_jtag_driver_config_t config = {
.tx_buffer_size = TRIKKE_USB_TX_BUFFER_SIZE,
.rx_buffer_size = TRIKKE_USB_RX_BUFFER_SIZE,
};
const esp_err_t err = usb_serial_jtag_driver_install(&config);
if (err != ESP_OK) {
return err;
}
usb->initialized = true;
transport->context = usb;
transport->begin = usb_begin_packet;
transport->poll = usb_poll_packet;
// Route any unexpected VFS output through the installed driver as well, so
// it cannot race the driver's ISR by touching the hardware FIFO directly.
usb_serial_jtag_vfs_use_driver();
return ESP_OK;
}
void trikke_usb_transport_deinit(trikke_usb_transport_t *usb)
{
if (usb == NULL || !usb->initialized) {
return;
}
usb_serial_jtag_vfs_use_nonblocking();
usb_serial_jtag_driver_uninstall();
usb->initialized = false;
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#include "trikke_transport.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
bool initialized;
} trikke_usb_transport_t;
esp_err_t trikke_usb_transport_init(
trikke_usb_transport_t *usb,
trikke_transport_t *transport);
void trikke_usb_transport_deinit(trikke_usb_transport_t *usb);
#ifdef __cplusplus
}
#endif