diff --git a/App/driver/keyboard.c b/App/driver/keyboard.c index ed4e2e6a..4d0a2532 100644 --- a/App/driver/keyboard.c +++ b/App/driver/keyboard.c @@ -27,6 +27,27 @@ KEY_Code_t gKeyReading1 = KEY_INVALID; uint16_t gDebounceCounter = 0; bool gWasFKeyPressed = false; +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT +// Number of consecutive KEYBOARD_Poll() calls that return the injected key. +// Must exceed key_debounce_10ms (typically 2) to pass the debounce gate in app.c. +// After HOLD polls the key is cleared, which triggers a natural release cycle. +#define SERIAL_KEY_HOLD_POLLS 5 + +volatile KEY_Code_t gKeyFromSerial = KEY_INVALID; +static uint8_t gSerialKeyHoldCount = 0; + +// Inject a key received from serial (UART or VCP). +// keyCode is the raw packet byte; validated against KEY_Code_e range before use. +// KEY_PTT is explicitly blocked — PTT release cannot be guaranteed over serial. +void KEYBOARD_InjectKey(uint8_t keyCode) +{ + if (keyCode < KEY_INVALID && keyCode != KEY_PTT) { + gKeyFromSerial = (KEY_Code_t)keyCode; + gSerialKeyHoldCount = 0; + } +} +#endif + #define GPIOx GPIOB #define PIN_MASK_COLS (LL_GPIO_PIN_6 | LL_GPIO_PIN_5 | LL_GPIO_PIN_4 | LL_GPIO_PIN_3) #define PIN_COLS GPIO_MAKE_PIN(GPIOx, PIN_MASK_COLS) @@ -78,6 +99,21 @@ static const KEY_Code_t keyboard[5][4] = { KEY_Code_t KEYBOARD_Poll(void) { +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + // Serial-injected key: hold it for SERIAL_KEY_HOLD_POLLS consecutive calls + // so the debounce counter in app.c has time to reach key_debounce_10ms. + // Once the hold count is exhausted we clear it — the next call returns + // KEY_INVALID, which triggers the release path in app.c naturally. + if (gKeyFromSerial != KEY_INVALID) { + KEY_Code_t injected = gKeyFromSerial; + if (++gSerialKeyHoldCount >= SERIAL_KEY_HOLD_POLLS) { + gKeyFromSerial = KEY_INVALID; + gSerialKeyHoldCount = 0; + } + return injected; + } +#endif + KEY_Code_t Key = KEY_INVALID; // Scan all 5 columns - never break early to avoid GPIO state issues diff --git a/App/driver/keyboard.h b/App/driver/keyboard.h index 5a018445..3eef61af 100644 --- a/App/driver/keyboard.h +++ b/App/driver/keyboard.h @@ -50,7 +50,17 @@ extern KEY_Code_t gKeyReading1; extern uint16_t gDebounceCounter; extern bool gWasFKeyPressed; -KEY_Code_t KEYBOARD_Poll(void); +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT +// Serial-injected key (written by UART/VCP parser, consumed by KEYBOARD_Poll). +// Held for SERIAL_KEY_HOLD_POLLS calls to satisfy the debounce logic in app.c. +extern volatile KEY_Code_t gKeyFromSerial; +// Inject a key received from serial (UART or VCP), bypasses physical matrix. +// keyCode is the raw byte from the serial packet — validated against KEY_Code_e range. +// KEY_PTT is explicitly blocked — PTT release cannot be guaranteed over serial. +void KEYBOARD_InjectKey(uint8_t keyCode); #endif +KEY_Code_t KEYBOARD_Poll(void); + +#endif \ No newline at end of file diff --git a/App/driver/uart.c b/App/driver/uart.c index 33f4481f..d3e70079 100644 --- a/App/driver/uart.c +++ b/App/driver/uart.c @@ -22,7 +22,12 @@ #include "py32f071_ll_dma.h" #include "py32f071_ll_gpio.h" #include "py32f071_ll_usart.h" -#include "driver/uart.h" + +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT +#include "driver/keyboard.h" +// Packet type for serial key injection (K5Viewer → radio) +#define UART_TYPE_KEY 0x03 +#endif #define USARTx USART1 #define DMA_CHANNEL LL_DMA_CHANNEL_2 @@ -149,12 +154,40 @@ void UART_LogSend(const void *pBuffer, uint32_t Size) #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT bool UART_IsCableConnected(void) { + bool connected = false; + for (size_t i = 0; i < sizeof(UART_DMA_Buffer); i++) { - if (UART_DMA_Buffer[i] == 0x55) { - UART_DMA_Buffer[i] = 0x00; // Clear only the matched byte - return true; + uint8_t b = UART_DMA_Buffer[i]; + + if (b == 0x55) { + // Keepalive byte — viewer is alive + UART_DMA_Buffer[i] = 0x00; + connected = true; + } + else if (b == 0xAA) { + // Possible start of a key packet: 0xAA 0x55 0x03 + // Check if the next 3 bytes are available and match + size_t i1 = (i + 1) % sizeof(UART_DMA_Buffer); + size_t i2 = (i + 2) % sizeof(UART_DMA_Buffer); + size_t i3 = (i + 3) % sizeof(UART_DMA_Buffer); + + if (UART_DMA_Buffer[i1] == 0x55 && + UART_DMA_Buffer[i2] == UART_TYPE_KEY) + { + uint8_t keyCode = UART_DMA_Buffer[i3]; + + // Consume all 4 bytes + UART_DMA_Buffer[i] = 0x00; + UART_DMA_Buffer[i1] = 0x00; + UART_DMA_Buffer[i2] = 0x00; + UART_DMA_Buffer[i3] = 0x00; + + KEYBOARD_InjectKey(keyCode); + connected = true; + } } } - return false; + + return connected; } #endif \ No newline at end of file diff --git a/App/driver/vcp.c b/App/driver/vcp.c index 87cfc1d5..27d9613a 100644 --- a/App/driver/vcp.c +++ b/App/driver/vcp.c @@ -18,6 +18,12 @@ #include "usb_config.h" #include "py32f071_ll_bus.h" +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT +#include "driver/keyboard.h" +// Packet type for serial key injection (K5Viewer → radio) +#define VCP_TYPE_KEY 0x03 +#endif + uint8_t VCP_RxBuf[VCP_RX_BUF_SIZE]; volatile uint32_t VCP_RxBufPointer = 0; @@ -40,22 +46,96 @@ void VCP_Init() bool VCP_ScreenshotPing(void) { - static uint32_t read_ptr = 0; +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + // State machine for parsing incoming packets: + // Keepalive: 0x55 0xAA 0x00 0x00 → viewer alive + // Key packet: 0xAA 0x55 0x03 → inject key + // + // State transitions: + // IDLE → 0x55 → KA_1 + // KA_1 → 0xAA → KA_2 (else IDLE) + // KA_2 → 0x00 → KA_3 (else IDLE) + // KA_3 → 0x00 → keepalive OK, IDLE + // + // IDLE → 0xAA → KEY_1 + // KEY_1 → 0x55 → KEY_2 (else IDLE) + // KEY_2 → 0x03 → KEY_3 (else IDLE) + // KEY_3 → → inject key, IDLE + typedef enum { + STATE_IDLE = 0, + STATE_KA_1, + STATE_KA_2, + STATE_KA_3, + STATE_KEY_1, + STATE_KEY_2, + STATE_KEY_3, + } ParseState_t; + + static uint32_t read_ptr = 0; + static ParseState_t state = STATE_IDLE; + + bool connected = false; uint32_t write_ptr = VCP_RxBufPointer; while (read_ptr != write_ptr) { uint8_t b = VCP_RxBuf[read_ptr]; - read_ptr++; if (read_ptr >= VCP_RX_BUF_SIZE) read_ptr = 0; - if (b == 0x55) + switch (state) { - return true; + case STATE_IDLE: + if (b == 0x55) state = STATE_KA_1; + else if (b == 0xAA) state = STATE_KEY_1; + break; + + case STATE_KA_1: + state = (b == 0xAA) ? STATE_KA_2 : STATE_IDLE; + break; + + case STATE_KA_2: + state = (b == 0x00) ? STATE_KA_3 : STATE_IDLE; + break; + + case STATE_KA_3: + if (b == 0x00) connected = true; + state = STATE_IDLE; + break; + + case STATE_KEY_1: + state = (b == 0x55) ? STATE_KEY_2 : STATE_IDLE; + break; + + case STATE_KEY_2: + state = (b == VCP_TYPE_KEY) ? STATE_KEY_3 : STATE_IDLE; + break; + + case STATE_KEY_3: + KEYBOARD_InjectKey(b); + connected = true; + state = STATE_IDLE; + break; + + default: + state = STATE_IDLE; + break; } } + + return connected; + +#else + // Simple ping: just detect any incoming byte from the viewer + static uint32_t read_ptr = 0; + uint32_t write_ptr = VCP_RxBufPointer; + + if (read_ptr != write_ptr) { + read_ptr = write_ptr; + return true; + } return false; -} +#endif +} \ No newline at end of file