For the Flipper Zero FAP's status page: hw status/CMD_STATUS replies
entirely via free-text Dbprintf packets the FAP's frame layer discards,
with nothing structured on the terminating ack. CMD_CEP_STATUS follows
the CMD_CAPABILITIES precedent instead - one binary cep_status_t reply
covering CEP handshake state, BWM battery telemetry, and a firmware
version string.
Always replies PM3_SUCCESS; fields for a feature this build lacks
(no WITH_CEP, no WITH_BWM_STATUS) read as zero rather than failing the
command, so any client can call it unconditionally. cep_attached isn't
included yet - its getter (cep_is_attached()) only exists on the
not-yet-merged WFI-skip responsiveness branch (#3687); can be appended
later following capabilities_t's own append-only convention.
Stacked on the pm5-bwm-battery-telemetry branch (CMD_PM5_BWM_GET_BATTERY),
which this command's battery field reuses via bwm_read_battery_info().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Flipper master only clocks the bus in bursts roughly every ~120ms
(its own idle poll cadence). CEP_SPI_BYTE_TIMEOUT (~3-10ms real time)
is ~20x shorter than that gap, so cep_spi_write_sync()'s first reply
byte - the one racing against the master's next poll - was giving up
long before the master actually showed up. Measured CEP reply success
at ~22% before this fix, ~100% after, across repeated LA-captured test
runs.
Fix: a separate, ~20x longer timeout (CEP_SPI_HEADER_BYTE_TIMEOUT) used
only for the first two length-header bytes of a reply - the ones that
can land in the gap. Every byte after that stays on the short timeout,
since once the master starts clocking it stays mid-CS and keeps going.
Also makes cep_spi_data_available() call cep_spi_resync() on its two
existing desync-evidence paths (sync-preamble discard overflow, an
implausible length header) instead of just returning false - those are
exactly the AT32 errata ES0003 corruption cep_spi_resync() already
exists to recover from; silently returning false left SPI1 in that bad
state until a later CRC/length check happened to catch it.
Stacked on fix-cep-spi-reliability (#3686, open) - this builds directly
on cep_spi_resync()/cep_spi_wait_tdbe()'s bounded-wait rewrite from that
PR and can't be tested standalone without it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bwm_charger.h is an armsrc-only header included after pm3_cmd.h in
appmain.c, so a struct embedding bwm_battery_info_t by value (e.g. a
future PM5 status command) couldn't see the type at the point it's
needed. bwm_ble_status_t and bwm_autooff_status_t already live in
pm3_cmd.h for the same reason - this just brings
bwm_battery_info_t in line with them. bwm_charger.c/.h keep their
function declarations unchanged; only the struct's home moves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hw status's battery/BWM section (bwm_print_battery_status()) only ever
formats its numbers into free-text Dbprintf lines, unusable by any
embedded/non-terminal client. Add a new command that returns the same
SoC/voltage/current/capacity/temp/health/fault numbers as one small
packed struct via reply_ng().
Refactor the shared reads into bwm_read_battery_info() so hw status
and the new command read the same values and can't drift apart -
hw status keeps all its existing charger-detail lines (MainCtl, input
limit, VIN_DPM, charge voltage/enable) unchanged, sourced from its own
reads as before; only the fuel-gauge numbers and the two small
charger-status reads the new struct needs are now shared.
Motivated by wanting a Flipper Zero FAP battery-status test screen
(separate app repo), but this command is generic and usable by any
client, not CEP-specific.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Problem
pm5_power_idle()'s WFI sleep adds ~40-50ms of latency before the main
loop notices a fresh byte on CEP's SPI1/USART1. At idle this is the
whole point of WFI - but it's also what made the original attach
handshake miss bytes (GH #3667): cep_attach_poll() runs every 50ms, and
WFI's own latency on top of that was enough to lose bytes of the
Flipper's one-shot handshake string often enough to require a physical
cable replug to recover.
Change
armsrc/appmain.c: skip pm5_power_idle() entirely once cep_is_attached()
is true, so the main loop polls CEP as fast as the hardware allows for
as long as a Flipper is physically present.
armsrc/pm5_cep.c/.h: new cep_is_attached(), true as soon as the CC
controller reports a physical attach - independent of whether the
handshake/SPI transport is actually up yet, so the loop stops sleeping
before the handshake even starts, not after. Paired with a
CEP_ACTIVITY_TIMEOUT_MS (3000ms) idle-detach: the FAP quitting doesn't
touch the CC controller's attach bit (the cable stays connected), so
losing its normal 20-120ms SPI poll cadence for this long is what
actually means it's gone - at that point cep_is_attached() still
reflects the physical attach correctly, but cep_is_active() (and
therefore this PR's WFI-skip, which gates on attachment rather than a
completed handshake) only backs off once the FAP itself has clearly quit
rather than just being between polls.
Built on top of the AT32 SPI1 errata fix (bounded waits + resync) from
the companion CEP-reliability PR - running the main loop flat-out
against an unbounded wait would have just hit that hang faster, which is
why this shipped after rather than alongside it.
Behaviour change
PM5 no longer downclocks to WFI between main-loop iterations while a
Flipper is attached over CEP (idle power draw increases for the duration
of the attach; unaffected when no Flipper is present, and unaffected on
RDV4/non-CEP builds).
Testing
- make clean && make PLATFORM=PM5 PLATFORM_EXTRAS=BWM and
make clean && make PLATFORM=PM3RDV4 - both zero warnings.
- Hardware: this flag was active throughout all of the companion CEP-
reliability PR's hardware testing (60s/110s/240s captures, 18x
`hw status`, 5x attach/detach + `hf search` cycles against a real
card) - all passed with WFI-skip engaged the whole time. Additionally
reflashed this exact branch standalone and reconfirmed a live
attach/detach cycle completes cleanly.
- Not tested: clang toolchain (firmware-only diff); idle power draw was
not independently measured (no direct ammeter in this session's
bench setup, only observed BWM telemetry prior to this change).
Checklist
- [x] Proper style of own code, no unrelated reformatting included
- [x] Builds warning-free with gcc (PM5 and RDV4)
- [ ] clang not tried (firmware-only diff)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Problem
With PR #3672/#3674 landed, CEP handshakes reliably but the SPI transport
hangs PM5 hard (needs a physical reset, no watchdog on this platform)
after some number of attach/reconnect cycles - sometimes the first
cycle, sometimes dozens of pings in. Logic-analyzer capture of a hang in
progress showed a ~1.2s burst of rapid CS-gated retries, and in one
capture, three periodic transactions after that burst got progressively
slower (2.1ms -> 3.9ms -> 3.2ms -> 5.2ms) instead of recovering.
Root causes (two, compounding)
1. cep_spi_write_sync() had seven bare `while (flag == RESET);` spins
waiting on SPI1's TDBE/BF flags, with no timeout. PM5 is the SPI
slave - TDBE only clears when the Flipper is actively clocking, on
its own ~20-120ms schedule, completely async to when PM5's reply is
ready. Queue a reply between two master polls and the wait never
returns - full main-loop death.
2. Artery's own errata sheet for the AT32F435/437 (ES0003) documents
that in SPI slave hardware-CS mode, the internal clock sync is not
guaranteed to reset on every CS falling edge - a bad sync can persist
across transactions with no self-correction. This is PM5's exact CEP
configuration (SPI_CS_HARDWARE_MODE, slave mode).
Change
armsrc/pm5_cep.c:
- cep_spi_write_sync() rewritten around a bounded cep_spi_wait_tdbe()
helper (same spin-iteration-count pattern already used for reads) -
returns PM3_EIO on timeout instead of hanging.
- New cep_spi_resync(): hardware-resets SPI1 via crm_periph_reset()
(a register re-init alone doesn't clear the errata's bad internal
state) and brings it back up. This is Artery's own documented recovery
for ES0003 ("reset SPI and restart communication"), triggered by our
existing NG-frame CRC/length checks instead of the SPI peripheral's
hardware CRC feature.
- cep_do_handshake()'s SPI-stall recovery path was itself unbounded
(retried forever as long as the CC controller reported "still
attached") and missing the StartTicks()/I2C_init(true) guard every
other I2C call site in this file has (same bug class as #3672) - now
bounded (CEP_HANDSHAKE_REPLY_MAX_RETRIES) with the guard added.
- cep_spi_data_available() now discards the Flipper's 0x55 idle-poll
filler bytes before reading the real length header, instead of ever
interpreting one as (part of) a frame length.
armsrc/cmd.c: wire cep_spi_resync() into every CEP-reachable error exit
in receive_ng_internal() (short/overflow/CRC-mismatch reads) and both
reply_old()/reply_ng_internal()'s write-failure paths, so any detected
corruption resets SPI1 before the next transaction rather than letting a
bad sync persist. Add a WITH_CEP-gated inline stub for cep_spi_resync()
so receive_ng_internal()'s call sites don't need per-site #ifdef guards
on non-CEP platforms.
Behaviour change
None for USB/FPC/BWM. CEP: a corrupted or stalled SPI1 transaction now
self-heals (one dropped reply, then normal operation) instead of
wedging the device.
Testing
- make clean && make PLATFORM=PM5 PLATFORM_EXTRAS=BWM and
make clean && make PLATFORM=PM3RDV4 - both zero warnings.
- Hardware: flashed to a real PM5 paired with a real Flipper Zero FAP.
- 60s continuous-connection capture with zero USB activity (isolates
CEP from any client-side interaction): clean, no anomalies.
- 110s capture with 18 real `hw status` calls interspersed: all 18
succeeded, CEP stayed attached throughout.
- 5 full attach/detach cycles with `hf search` against a real NTAG213
card before/during/after each attach (240s, logic-analyzer capture
throughout): all 15 reads returned the identical UID regardless of
CEP state, zero hangs, final liveness check succeeded with no reset
needed.
- Not tested: clang toolchain (firmware-only diff).
Checklist
- [x] Proper style of own code, no unrelated reformatting included
- [x] Builds warning-free with gcc (PM5 and RDV4)
- [ ] clang not tried (firmware-only diff)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two g_tx_completed spin loops in usb_write() had no timeout at all -
if the host ever stopped pulling an IN packet while the device still read
as enumerated, PM5 would spin forever with no recovery path. Add a
spin-iteration bound (not tick-based: g_tx_completed is set from the USB
core's own ISR, and the shared ticks timer is routinely stopped by LF/HF
code at arbitrary points), matching the existing bounded-wait pattern
used elsewhere in this codebase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
receive_ng_internal() zeroed rx->data (up to PM3_CMD_DATA_SIZE bytes)
between the preamble read and the payload read. CEP's SPI1 receiver has
no FIFO (single-byte holding register), so that stall lost bytes on every
frame - confirmed via logic analyzer (wire content byte-perfect) plus
Dbprintf instrumentation showing the payload read consistently short by
half. Reordered so preamble/payload/postamble/CRC are all read back-to-back
before any memset/memcpy happens. USB/FPC/BWM are unaffected: same total
work, just resequenced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds A1856618 to t55xx_default_pwds. This is the verified working login
password (as used with -p); the bit-reversed on-air form 1866A185 does not
authenticate.
cep_attach_poll() (and cep_init()) call I2C_BufferReadRaw()/I2C_init(true),
which spin on the shared ticks timer via WaitUS()/WaitMS(). That timer is
routinely stopped with StopTicks() by LF/HF protocol code once it's done
with precision timing (em4x50.c, legicrf.c, hitag_common.c, lfsampling.c,
etc). cep_attach_poll() runs unconditionally every 50ms in the main loop
with no idea whether the timer is currently running - if its poll lands in
a window where something just called StopTicks(), WaitTicks()'s
`while (GetTicks() < target)` spins forever on a frozen counter, hanging
the entire main loop irrecoverably. `hf search` cycles through many such
probes, giving this poll many chances to land in that window, matching the
reported "hf search hangs the PM5, needs a battery pull" regression - and
since the poll runs regardless of Flipper attachment, merely compiling in
WITH_CEP was enough to trigger it.
bwm_lowbatt_poll() (armsrc/bwm_charger.c) is the same shape of periodic
main-loop I2C poller and already guards against exactly this by calling
StartTicks(); I2C_init(true); itself immediately before every I2C access.
Mirror that convention in cep_attach_poll() and cep_init().
Re-enables WITH_CEP as the PM5 default (reverts f88880fc6's temporary
disable) now that the underlying bug is fixed. SKIP_CEP=1 remains available
for anyone who wants to opt out at build time regardless.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
doegox asked on PR #3668 whether CEP really needs to be guarded by a
compile flag, suggesting it ship on by default with a SKIP_CEP opt-out
instead - mirrors the existing NO_LOWBATT_SHUTDOWN/NO_LOWBATT_BEEP
opt-out pattern in common_arm/Makefile.hal. WITH_CEP is now baked into
PM5's base PLATFORM_DEFS; PLATFORM_EXTRAS=SKIP_CEP filters it back out.
No change to armsrc/Makefile (already keys off the WITH_CEP define, not
the old extras token) or to the CEP transport logic itself. Updated the
PM5-FLIPPER-USAGE doc, the CHANGELOG entry, and one stale comment in
include/pm3_cmd.h that referenced the old opt-in token.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pass the requested system code to firmware selection so default and system-specific reader modes share one polling path. Keep the RF field active across retries for the system-specific mode.
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Add --sys to the FeliCa reader so repeated polling can activate an Express Transit card while keeping the RF field on.
Co-Authored-By: Codex (GPT-6 Sol) <noreply@openai.com>
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>