#include "serial_link.h" #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/uart.h" #include "esp_log.h" static const char *TAG = "serial_link"; #define SYNC0 0xAA #define SYNC1 0x55 static serial_link_cam_tx_cb_t s_on_cam_tx; // ---- CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF, no reflect, no xorout) ---- // Bytewise (no table) - frames here are at most SERIAL_LINK_MAX_PAYLOAD + 3 bytes, so table // lookup isn't worth the flash/RAM tradeoff. MUST match the Kotlin-side implementation exactly // (see app SerialFrame.kt) or every frame will be silently rejected as corrupt. static uint16_t crc16_ccitt_false(const uint8_t *data, size_t len) { uint16_t crc = 0xFFFF; for (size_t i = 0; i < len; i++) { crc ^= (uint16_t)data[i] << 8; for (int b = 0; b < 8; b++) { crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1); } } return crc; } static bool send_frame(uint8_t type, const uint8_t *payload, int len) { if (len < 0 || len > SERIAL_LINK_MAX_PAYLOAD) { ESP_LOGW(TAG, "send_frame: payload too large (%d)", len); return false; } // type(1) + length(2) + payload(len) is what the CRC covers. uint8_t head[3]; head[0] = type; head[1] = (uint8_t)(len & 0xFF); head[2] = (uint8_t)((len >> 8) & 0xFF); uint16_t crc; { // Compute CRC over head+payload without a combined buffer copy: CRC is a running // state, so feed it in two calls worth of bytes by concatenating into a small stack // buffer (payload is capped at SERIAL_LINK_MAX_PAYLOAD, so head+payload comfortably // fits on the stack). uint8_t crc_buf[3 + SERIAL_LINK_MAX_PAYLOAD]; memcpy(crc_buf, head, 3); if (len > 0) memcpy(crc_buf + 3, payload, (size_t)len); crc = crc16_ccitt_false(crc_buf, (size_t)(3 + len)); } uint8_t sync[2] = {SYNC0, SYNC1}; uint8_t crc_bytes[2] = {(uint8_t)(crc & 0xFF), (uint8_t)((crc >> 8) & 0xFF)}; // Four separate writes rather than one assembled buffer - simplest given payload is // already wherever the caller has it (avoids a second copy of up to 160 bytes). int wrote = 0; wrote += uart_write_bytes(SERIAL_LINK_UART_NUM, sync, sizeof(sync)); wrote += uart_write_bytes(SERIAL_LINK_UART_NUM, head, sizeof(head)); if (len > 0) wrote += uart_write_bytes(SERIAL_LINK_UART_NUM, payload, (size_t)len); wrote += uart_write_bytes(SERIAL_LINK_UART_NUM, crc_bytes, sizeof(crc_bytes)); return wrote == (int)(sizeof(sync) + sizeof(head) + len + sizeof(crc_bytes)); } bool serial_link_send_cam_rx(int8_t rssi, const uint8_t *cam_uper, int cam_len) { if (cam_len < 0 || cam_len > SERIAL_LINK_MAX_PAYLOAD - 1) { ESP_LOGW(TAG, "send_cam_rx: cam_len too large (%d)", cam_len); return false; } uint8_t payload[SERIAL_LINK_MAX_PAYLOAD]; payload[0] = (uint8_t)rssi; memcpy(payload + 1, cam_uper, (size_t)cam_len); return send_frame(SERIAL_MSG_CAM_RX, payload, 1 + cam_len); } bool serial_link_send_status(uint8_t status) { return send_frame(SERIAL_MSG_STATUS, &status, 1); } // ---- RX framing state machine ---- // Runs in its own task, byte-at-a-time off the UART driver's RX ring buffer (via // uart_read_bytes with a short timeout, not raw ISR access - simplest correct option for a // link this slow/small; revisit if CAM traffic volume ever makes this a bottleneck). typedef enum { WAIT_SYNC0, WAIT_SYNC1, WAIT_TYPE, WAIT_LEN_LO, WAIT_LEN_HI, WAIT_PAYLOAD, WAIT_CRC_LO, WAIT_CRC_HI, } rx_state_t; static void rx_task(void *arg) { (void)arg; rx_state_t state = WAIT_SYNC0; uint8_t type = 0; uint16_t len = 0; uint16_t payload_idx = 0; uint8_t payload[SERIAL_LINK_MAX_PAYLOAD]; uint16_t crc_recv = 0; uint8_t byte; while (1) { int n = uart_read_bytes(SERIAL_LINK_UART_NUM, &byte, 1, pdMS_TO_TICKS(50)); if (n <= 0) continue; switch (state) { case WAIT_SYNC0: state = (byte == SYNC0) ? WAIT_SYNC1 : WAIT_SYNC0; break; case WAIT_SYNC1: state = (byte == SYNC1) ? WAIT_TYPE : (byte == SYNC0 ? WAIT_SYNC1 : WAIT_SYNC0); break; case WAIT_TYPE: type = byte; state = WAIT_LEN_LO; break; case WAIT_LEN_LO: len = byte; state = WAIT_LEN_HI; break; case WAIT_LEN_HI: len |= (uint16_t)byte << 8; if (len > SERIAL_LINK_MAX_PAYLOAD) { ESP_LOGW(TAG, "rx: length %u exceeds max, resyncing", len); state = WAIT_SYNC0; // can't trust this frame boundary at all - drop to resync } else if (len == 0) { payload_idx = 0; state = WAIT_CRC_LO; } else { payload_idx = 0; state = WAIT_PAYLOAD; } break; case WAIT_PAYLOAD: payload[payload_idx++] = byte; if (payload_idx >= len) state = WAIT_CRC_LO; break; case WAIT_CRC_LO: crc_recv = byte; state = WAIT_CRC_HI; break; case WAIT_CRC_HI: { crc_recv |= (uint16_t)byte << 8; uint8_t crc_buf[3 + SERIAL_LINK_MAX_PAYLOAD]; crc_buf[0] = type; crc_buf[1] = (uint8_t)(len & 0xFF); crc_buf[2] = (uint8_t)((len >> 8) & 0xFF); if (len > 0) memcpy(crc_buf + 3, payload, len); uint16_t crc_calc = crc16_ccitt_false(crc_buf, (size_t)(3 + len)); if (crc_calc == crc_recv) { if (type == SERIAL_MSG_CAM_TX && s_on_cam_tx) { s_on_cam_tx(payload, len); } else if (type != SERIAL_MSG_CAM_TX) { ESP_LOGW(TAG, "rx: unexpected frame type 0x%02x from phone, ignoring", type); } } else { ESP_LOGW(TAG, "rx: CRC mismatch (got %04x want %04x), dropping frame", crc_recv, crc_calc); } state = WAIT_SYNC0; break; } } } } void serial_link_init(serial_link_cam_tx_cb_t on_cam_tx) { s_on_cam_tx = on_cam_tx; uart_config_t cfg = { .baud_rate = SERIAL_LINK_BAUD, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .source_clk = UART_SCLK_DEFAULT, }; ESP_ERROR_CHECK(uart_driver_install(SERIAL_LINK_UART_NUM, 1024, 1024, 0, NULL, 0)); ESP_ERROR_CHECK(uart_param_config(SERIAL_LINK_UART_NUM, &cfg)); ESP_ERROR_CHECK(uart_set_pin(SERIAL_LINK_UART_NUM, SERIAL_LINK_TX_GPIO, SERIAL_LINK_RX_GPIO, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); xTaskCreate(rx_task, "serial_link_rx", 4096, NULL, 6, NULL); ESP_LOGI(TAG, "serial_link up on UART%d, TX=GPIO%d RX=GPIO%d @ %d baud", SERIAL_LINK_UART_NUM, SERIAL_LINK_TX_GPIO, SERIAL_LINK_RX_GPIO, SERIAL_LINK_BAUD); }