/**
 ******************************************************************************
 * @file           : trax_transport.c
 * @date           : Aug 18, 2026
 * @author         : bemre
 * @version        : 1.0
 *
 * @brief          : TraxProbe custom UART transport (fail-fast, non-blocking)
 ******************************************************************************
 * @attention
 * 	Implements the TraxProbe all-or-nothing transport contract
 * 	(.cursor/rules/trax-transport-transmission.mdc):
 *
 * 	  - write() pre-checks the TX ring free space. If the whole batch
 * 	    does not fit it returns 0 WITHOUT buffering anything — the core
 * 	    treats that as soft backpressure, keeps the batch queued in the
 * 	    TraxProbe ring, and retries on the next trax_process() pass.
 * 	  - If it fits, the whole batch is committed and size is returned.
 *
 * 	LINK: USART1 on the EmSparkerG0 Arduino header (PC4 = TX / D1,
 * 	PC5 = RX / D0) at 921600 8N1 — the port the EmPhasalis shield reads.
 * 	USART2 stays free for the debug terminal.
 *
 * 	This is the same design as the workshop transport
 * 	(Workshop_Live/Source/Workshop_11_Final/App/Transport), but this
 * 	project is plain CubeMX HAL with no BSP layer, so the TX/RX rings
 * 	that bsp_uart would own live at the bottom of this file. The
 * 	peripheral itself (clocks, GPIO, baud, DMA1 CH2) belongs to CubeMX;
 * 	trax_transport_init() only arms the RX path.
 *
 * 	TX is drained by DMA in UART_TX_DMA_CHUNK pieces: write() stages the
 * 	batch in the ring and kicks the engine, the TX-complete callback
 * 	re-kicks it until the ring is empty. Nothing here ever blocks, so a
 * 	stalled link is bounded by the TraxProbe ring filling up
 * 	(BUFFER_OVERFLOW stop) instead of wedging trax_process().
 *
 * 	NOTE: TRAX_CFG_TX_BATCH_MAX_BYTES must not exceed the TX ring's
 * 	usable capacity (UART_TX_RING_SIZE - 1), or a max-size batch could
 * 	never fit and the stream would stall until ring overflow. See
 * 	trax_config.h.
 ******************************************************************************
 */

#include "main.h"

#include <string.h>
#include <trax_transport.h>

/*=============================================================================
 ====================LOCAL MACRO FUNCTIONS=====================================
 ============================================================================*/

/*=============================================================================
 ====================LOCAL MACRO DEFINITIONS===================================
 ============================================================================*/
#define UART_HANDLE			huart1

/* TX ring: the burst absorber in front of the wire. Mirrors the EmSparkerG0
 * BSP sizing (bsp_conf_uart.h: 8 KB ring, 256 B DMA staging). */
#define UART_TX_RING_SIZE	8192U
#define UART_TX_DMA_CHUNK	256U

/* RX ring: commands from Traxcope only (CMD_START_STREAMING and friends),
 * so a small ring fed one byte per interrupt is plenty. */
#define UART_RX_RING_SIZE	256U

/*=============================================================================
 ====================LOCAL STUCTURES===========================================
 ============================================================================*/

/**
 * @brief Single-producer / single-consumer byte ring.
 *
 * One slot is always left empty so head == tail means empty, which keeps
 * count/space lock-free: the producer only advances head, the consumer only
 * advances tail, and both are 16-bit aligned (atomic loads/stores on M0+).
 */
struct uart_ring_t {
	uint8_t          *p_mem;
	uint16_t          size;
	volatile uint16_t head;
	volatile uint16_t tail;
};

/*=============================================================================
 ====================LOCAL VARIABLES===========================================
 ============================================================================*/

extern UART_HandleTypeDef UART_HANDLE;   /* CubeMX owns the peripheral */

static uint8_t tx_mem[UART_TX_RING_SIZE];
static uint8_t rx_mem[UART_RX_RING_SIZE];

/** Linear buffer the DMA reads from — a ring cannot be handed to DMA directly. */
static uint8_t tx_dma_staging[UART_TX_DMA_CHUNK];

/** Landing byte for the one-byte-at-a-time RX interrupt. */
static uint8_t rx_byte;

static struct uart_ring_t tx_ring;
static struct uart_ring_t rx_ring;

/*=============================================================================
 ====================LOCAL FUNCTION DECLARATION================================
 ============================================================================*/

static void     ring_init(struct uart_ring_t *p_ring, uint8_t *p_mem,
	uint16_t size);
static uint16_t ring_count(const struct uart_ring_t *p_ring);
static uint16_t ring_space(const struct uart_ring_t *p_ring);
static void     ring_write(struct uart_ring_t *p_ring, const uint8_t *p_src,
	uint16_t size);
static uint16_t ring_read(struct uart_ring_t *p_ring, uint8_t *p_dst,
	uint16_t size);
static void     uart_tx_kick(void);
static void     uart_rx_arm(void);

/*=============================================================================
 ====================GLOBAL FUNCTION IMPLEMANTATION============================
 ============================================================================*/

/**
 * @brief Initialize the transport binding (not the hardware itself). REQUIRED.
 *
 * Called once by trax_init(). The hardware is expected to be up already
 * (MX_USART1_UART_Init() owns clocks / GPIO / baud / DMA); here we reset the
 * rings and arm the RX path.
 *
 * @return 0 on success, non-zero on failure (aborts trax_init).
 */
int trax_transport_init(void)
{
	ring_init(&tx_ring, tx_mem, UART_TX_RING_SIZE);
	ring_init(&rx_ring, rx_mem, UART_RX_RING_SIZE);

	uart_rx_arm();
	return 0;
}

/**
 * @brief Send data to host — strictly all-or-nothing. REQUIRED.
 *
 * Fail-fast: if the batch does not fit in the TX ring, return 0 without
 * touching it. The core treats 0 as retryable backpressure — the frames
 * stay queued in the TraxProbe ring and are re-sent on the next
 * trax_process() pass. The free-space pre-check makes the copy below
 * unconditional: only the DMA callback drains the ring between the check
 * and the copy, so the space can only grow.
 *
 * @param[in] p_src  Data to send
 * @param[in] size   Number of bytes
 * @return @p size on success; 0 = nothing accepted (retry next pass)
 */
size_t trax_transport_write(const void *p_src, size_t size)
{
	if ((size_t)ring_space(&tx_ring) < size) {
		return 0U;   /* backpressure — core retries the batch */
	}

	ring_write(&tx_ring, (const uint8_t *)p_src, (uint16_t)size);

	/* Start the DMA if it is idle; otherwise the in-flight transfer's
	 * TX-complete callback picks the new bytes up. */
	uart_tx_kick();

	return size;
}

/**
 * @brief Read data from host — non-blocking. REQUIRED.
 *
 * @param[out] p_dst  Destination buffer
 * @param[in]  size   Maximum bytes to read
 * @return Bytes actually read
 */
size_t trax_transport_read(void *p_dst, size_t size)
{
	return (size_t)ring_read(&rx_ring, (uint8_t *)p_dst, (uint16_t)size);
}

/**
 * @brief Check bytes available from host — non-blocking. REQUIRED.
 *
 * @return Number of bytes waiting in the down-channel, or 0
 */
size_t trax_transport_has_data(void)
{
	return (size_t)ring_count(&rx_ring);
}

/**
 * @brief Up-channel TX free space. REQUIRED.
 *
 * The core caps each drain batch at this value BEFORE collecting frames,
 * so write() is guaranteed to accept the batch. Free space may only grow
 * between this call and write() (the DMA callback only drains).
 *
 * @return Bytes a write() is guaranteed to accept right now
 */
size_t trax_transport_tx_free(void)
{
	return (size_t)ring_space(&tx_ring);
}

/**
 * @brief Bytes buffered in the up-channel TX path. Diagnostics only. REQUIRED.
 *
 * @return Bytes pending in the transport's TX buffer
 */
size_t trax_transport_bytes_used(void)
{
	return (size_t)ring_count(&tx_ring);
}

/**
 * @brief Discard queued TX bytes on session restart. REQUIRED.
 *
 * Drops whatever is still queued in the ring so a new session does not start
 * behind stale frames from the previous one. Bytes already handed to the DMA
 * are on the wire and cannot be recalled — the host discards them as
 * pre-session noise.
 */
void trax_transport_clear_tx(void)
{
	uint32_t primask = __get_PRIMASK();

	__disable_irq();
	tx_ring.head = 0U;
	tx_ring.tail = 0U;
	__set_PRIMASK(primask);
}

/**
 * @brief HAL TX-complete callback — chain the next DMA chunk.
 *
 * The DMA can only be pointed at a linear buffer, so one transfer covers at
 * most UART_TX_DMA_CHUNK bytes. Re-kicking here is what drains a multi-chunk
 * batch without the writer ever blocking.
 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *p_huart)
{
	if (p_huart != &UART_HANDLE) {
		return;
	}

	uart_tx_kick();
}

/**
 * @brief HAL RX-complete callback — stash the byte and re-arm.
 *
 * Bytes that arrive with a full ring are dropped: the down-channel only
 * carries commands, and TraxProbe's protocol re-syncs on the next frame.
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *p_huart)
{
	if (p_huart != &UART_HANDLE) {
		return;
	}

	if (ring_space(&rx_ring) > 0U) {
		ring_write(&rx_ring, &rx_byte, 1U);
	}

	uart_rx_arm();
}

/**
 * @brief HAL error callback — re-arm RX and resume TX.
 *
 * An unplugged or noisy line raises ORE/FE/NE, which stops the RX interrupt.
 * Without re-arming here the probe would go deaf to commands after the first
 * glitch. The TX kick covers a DMA error that left bytes in the ring.
 */
void HAL_UART_ErrorCallback(UART_HandleTypeDef *p_huart)
{
	if (p_huart != &UART_HANDLE) {
		return;
	}

	uart_rx_arm();
	uart_tx_kick();
}

/*=============================================================================
 ====================LOCAL FUNCTION IMPLEMANTATION=============================
 ============================================================================*/

static void ring_init(struct uart_ring_t *p_ring, uint8_t *p_mem, uint16_t size)
{
	p_ring->p_mem = p_mem;
	p_ring->size  = size;
	p_ring->head  = 0U;
	p_ring->tail  = 0U;
}

static uint16_t ring_count(const struct uart_ring_t *p_ring)
{
	uint16_t head = p_ring->head;
	uint16_t tail = p_ring->tail;

	if (head >= tail) {
		return (uint16_t)(head - tail);
	}

	return (uint16_t)((p_ring->size - tail) + head);
}

static uint16_t ring_space(const struct uart_ring_t *p_ring)
{
	return (uint16_t)(p_ring->size - 1U - ring_count(p_ring));
}

/**
 * @brief Copy @p size bytes in. Caller must have checked ring_space() first.
 */
static void ring_write(struct uart_ring_t *p_ring, const uint8_t *p_src,
	uint16_t size)
{
	uint16_t head  = p_ring->head;
	uint16_t first = (uint16_t)(p_ring->size - head);

	if (first > size) {
		first = size;
	}

	memcpy(&p_ring->p_mem[head], p_src, first);
	if (size > first) {
		memcpy(p_ring->p_mem, &p_src[first], (size_t)(size - first));
	}

	head = (uint16_t)(head + size);
	if (head >= p_ring->size) {
		head = (uint16_t)(head - p_ring->size);
	}

	p_ring->head = head;
}

/**
 * @brief Copy out up to @p size bytes.
 *
 * @return Bytes actually copied (0 when empty)
 */
static uint16_t ring_read(struct uart_ring_t *p_ring, uint8_t *p_dst,
	uint16_t size)
{
	uint16_t avail = ring_count(p_ring);
	uint16_t tail;
	uint16_t first;

	if (size > avail) {
		size = avail;
	}
	if (size == 0U) {
		return 0U;
	}

	tail  = p_ring->tail;
	first = (uint16_t)(p_ring->size - tail);
	if (first > size) {
		first = size;
	}

	memcpy(p_dst, &p_ring->p_mem[tail], first);
	if (size > first) {
		memcpy(&p_dst[first], p_ring->p_mem, (size_t)(size - first));
	}

	tail = (uint16_t)(tail + size);
	if (tail >= p_ring->size) {
		tail = (uint16_t)(tail - p_ring->size);
	}

	p_ring->tail = tail;
	return size;
}

/**
 * @brief Arm the one-byte RX interrupt.
 *
 * FIFO mode is disabled for USART1 in MX_USART1_UART_Init(), so a 1-byte
 * Receive_IT is the natural granularity for the low-rate command channel.
 */
static void uart_rx_arm(void)
{
	(void)HAL_UART_Receive_IT(&UART_HANDLE, &rx_byte, 1U);
}

/**
 * @brief Stage the next ring chunk and hand it to the TX DMA.
 *
 * Called from both the writer (task/main loop) and the TX-complete callback,
 * so the idle test and the DMA start must be one atomic step — otherwise both
 * could stage into tx_dma_staging at once and tear the stream. HAL's own
 * gState is the idle flag: it is set BUSY_TX by HAL_UART_Transmit_DMA() and
 * released only after the last byte clocks out, so it also self-heals after an
 * abort or error instead of latching a private "busy" bit forever.
 */
static void uart_tx_kick(void)
{
	uint32_t primask = __get_PRIMASK();
	uint16_t size;

	__disable_irq();

	if (UART_HANDLE.gState == HAL_UART_STATE_READY) {
		size = ring_read(&tx_ring, tx_dma_staging, UART_TX_DMA_CHUNK);
		if (size > 0U) {
			(void)HAL_UART_Transmit_DMA(&UART_HANDLE, tx_dma_staging,
				size);
		}
	}

	__set_PRIMASK(primask);
}
