From ba4016d7e26f19a42604a6c5bda1397e8dcf62d5 Mon Sep 17 00:00:00 2001 From: Tom Harkness Date: Sat, 3 Oct 2026 16:17:58 +1000 Subject: [PATCH] PM5 CEP: data_available() doesn't poll CEP, so CMD_BREAK_LOOP over a Flipper link can't interrupt a running reader loop Problem: triggering the Flipper FAP's built-in ReadHitag2 demo command turns the RF field on and it stays on even after backing out of the app - only an unrelated USB command clears it. data_available() (armsrc/util.c) is the standard idiom LF/HF reader loops use to notice an incoming CMD_BREAK_LOOP, but it only polled USB/BWM/FPC. CEP's own transport PR never wired it in, so a Flipper-sent break is invisible to any loop that was already running. Fix: add cep_spi_rx_pending() to armsrc/pm5_cep.c/.h - a genuinely non-blocking single SPI1 RDBF flag check, not a reuse of the existing (bounded-wait) cep_spi_data_available(), since this gets called every iteration of every reader loop in the codebase and needs to cost nothing when idle. Wire it into data_available()/data_available_fast() alongside the existing BWM/FPC branches, additive since CEP and BWM can coexist. Hardware-confirmed: field now drops promptly every cycle instead of sticking on, across repeated ReadHitag2 attach/attempt/cancel cycles. Co-Authored-By: Claude Sonnet 5 --- armsrc/pm5_cep.c | 11 +++++++++++ armsrc/pm5_cep.h | 7 +++++++ armsrc/util.c | 29 +++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/armsrc/pm5_cep.c b/armsrc/pm5_cep.c index cd5648fec..8ef5c9674 100644 --- a/armsrc/pm5_cep.c +++ b/armsrc/pm5_cep.c @@ -343,6 +343,17 @@ static bool cep_spi_read_byte(uint8_t *out) { return true; } +// Genuinely non-blocking - a single flag read, no spin-wait at all. For +// data_available()'s use (armsrc/util.c): a tight sampling loop calls that +// every pass purely to notice an incoming CMD_BREAK_LOOP promptly, so it +// needs to cost nothing when idle. cep_spi_data_available() isn't a fit +// here - it waits (bounded, but non-zero) for a first byte to actually +// arrive, which is fine for receive_ng()'s own cadence but would add that +// wait to every iteration of every LF/HF reader loop in the codebase. +bool cep_spi_rx_pending(void) { + return spi_i2s_flag_get(SPI1, SPI_I2S_RDBF_FLAG) != RESET; +} + bool cep_spi_data_available(void) { uint8_t len_header[2] = {0x00}; size_t discarded = 0; diff --git a/armsrc/pm5_cep.h b/armsrc/pm5_cep.h index 28d7bb0d9..7185be426 100644 --- a/armsrc/pm5_cep.h +++ b/armsrc/pm5_cep.h @@ -58,6 +58,13 @@ void cep_attach_poll(void); // simple non-blocking peek. bool cep_spi_data_available(void); +// True if SPI1's receive-data register currently holds an unread byte - a +// single flag check, zero wait. Gate for data_available() (armsrc/util.c), +// which many LF/HF reader loops call every pass to notice an incoming +// CMD_BREAK_LOOP - that call site needs to cost nothing when idle, unlike +// cep_spi_data_available() above which waits (bounded) for a first byte. +bool cep_spi_rx_pending(void); + // Read up to `len` raw NG bytes off SPI1. Returns the number of bytes // actually read before an internal per-byte timeout gave up early. uint32_t cep_spi_read_ng(uint8_t *data, size_t len); diff --git a/armsrc/util.c b/armsrc/util.c index d163f47b3..cc7eb6b5e 100644 --- a/armsrc/util.c +++ b/armsrc/util.c @@ -28,6 +28,9 @@ #ifdef WITH_BWM_FORWARD #include "bwm_forward.h" #endif +#ifdef WITH_CEP +#include "pm5_cep.h" +#endif #include "BigBuf.h" // trace_restart_timeline #ifdef WITH_SMARTCARD #include "i2c.h" // sc_log_trace_reset @@ -330,28 +333,42 @@ int BUTTON_HELD(int ms) { // This function returns false if no data is available or // the USB connection is invalid. bool data_available(void) { + bool avail; #if defined(WITH_BWM_FORWARD) // The BWM (BLE / WiFi) link is the host connection on a Proxmark5, so it // has to be polled here too or CMD_BREAK_LOOP is never seen over it. - return usb_poll_validate_length() || (bwm_fwd_rxdata_available() > 0); + avail = usb_poll_validate_length() || (bwm_fwd_rxdata_available() > 0); #elif defined(WITH_FPC_USART_HOST) - return usb_poll_validate_length() || (usart_rxdata_available() > 0); + avail = usb_poll_validate_length() || (usart_rxdata_available() > 0); #else - return usb_poll_validate_length(); + avail = usb_poll_validate_length(); #endif +#if defined(WITH_CEP) + // CEP is independent of BWM/FPC (both may be live at once) - a Flipper + // sending CMD_BREAK_LOOP into a running reader loop is otherwise + // invisible here, leaving the loop (and its RF field) running until an + // unrelated USB command happens to arrive. + avail = avail || cep_spi_rx_pending(); +#endif + return avail; } // This function doesn't check if the USB connection is valid. // In most of the cases, you should use data_available() unless // the timing is critical. bool data_available_fast(void) { + bool avail; #if defined(WITH_BWM_FORWARD) - return usb_available_length() || (bwm_fwd_rxdata_available() > 0); + avail = usb_available_length() || (bwm_fwd_rxdata_available() > 0); #elif defined(WITH_FPC_USART_HOST) - return usb_available_length() || (usart_rxdata_available() > 0); + avail = usb_available_length() || (usart_rxdata_available() > 0); #else - return usb_available_length(); + avail = usb_available_length(); #endif +#if defined(WITH_CEP) + avail = avail || cep_spi_rx_pending(); +#endif + return avail; } // Combined function to convert an unsigned int to an array of hex values corresponding to the last three bits of k1