From 622547351b49b38909422bad69cc1e82470d17bc Mon Sep 17 00:00:00 2001 From: Msprg <18015488+Msprg@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:52:23 +0200 Subject: [PATCH 1/7] doc: regenerate commands (hf ntag424 gettt / setconfig were missing) `make commands` on current master: the two hf ntag424 commands had not made it into the generated docs, and `hw bwm help` is gated by IfBwm since 339db44cf but the docs still list it as offline-capable. No other change. Kept separate so the commits that follow only carry their own doc changes. Co-Authored-By: Claude Fable 5.1 --- doc/commands.json | 61 +++++++++++++++++++++++++++++++++++++---------- doc/commands.md | 4 +++- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/doc/commands.json b/doc/commands.json index 364afb575..9649e0ae0 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -6994,7 +6994,9 @@ "hf mf sen", "hf mf sen --keep-nonces", "hf mf sen --no-oob", - "hf mf sen --reader" + "hf mf sen --reader", + "hf mf sen --suffix mycard", + "hf mf sen --ns" ], "offline": false, "options": [ @@ -7005,9 +7007,11 @@ "--offline-only stop after offline filtering, do not verify online", "--online-confirm only do online confirmation (skip generation if nonces loaded)", "--max-online-candidates abort online phase if total candidates exceed this limit", - "--parity-mask parity filter mask 1..F (default 1 = vetted staticnested_1nt behavior)" + "--parity-mask parity filter mask 1..F (default 1 = vetted staticnested_1nt behavior)", + "--suffix Add this suffix to generated files", + "--ns no save to file (--keep-nonces still writes its evidence file)" ], - "usage": "hf mf sen [-h] [--keep-nonces] [--no-oob] [--reader] [--offline-only] [--online-confirm] [--max-online-candidates ] [--parity-mask ]" + "usage": "hf mf sen [-h] [--keep-nonces] [--no-oob] [--reader] [--offline-only] [--online-confirm] [--max-online-candidates ] [--parity-mask ] [--suffix ] [--ns]" }, "hf mf setmod": { "command": "hf mf setmod", @@ -8699,7 +8703,7 @@ "--dfname Certificate application DF name (1-16 bytes)", "--fid Certificate file ID (1 byte)", "--no-auth Read certificate file without authentication", - "--ca CA input, or `skip` to skip certificate signature validation. Repeat --ca for multiple entries", + "--ca CA input, or `skip` to skip validation", "--keyaid Key application ID (default: cert app)", "--keyisoid Key application ISO DF ID (2 bytes)", "--keydfname Key application DF name (default: cert app)", @@ -8713,15 +8717,17 @@ "command": "hf mfdes view", "description": "Print a DESFire card dump file (json)", "notes": [ - "hf mfdes view -f hf-mfdes-01020304050607-dump.json" + "hf mfdes view -f hf-mfdes-01020304050607-dump.json", + "hf mfdes view --selftest" ], "offline": true, "options": [ "-h, --help This help", "-f, --file Filename of dump", - "-v, --verbose Verbose output" + "-v, --verbose Verbose output", + "--selftest Run the dump parsers self tests and exit" ], - "usage": "hf mfdes view [-hv] -f " + "usage": "hf mfdes view [-hv] [-f ] [--selftest]" }, "hf mfdes write": { "command": "hf mfdes write", @@ -9853,6 +9859,20 @@ ], "usage": "hf ntag424 getfs [-h] --fileno " }, + "hf ntag424 gettt": { + "command": "hf ntag424 gettt", + "description": "Read and print Tag Tamper status. Will authenticate if key information is provided.", + "notes": [ + "hf ntag424 gettt --keyno 0 -k 00000000000000000000000000000000" + ], + "offline": false, + "options": [ + "-h, --help This help", + "--keyno Key number", + "-k, --key Key for authentication (HEX 16 bytes)" + ], + "usage": "hf ntag424 gettt [-h] [--keyno ] [-k ]" + }, "hf ntag424 help": { "command": "hf ntag424 help", "description": "help This help view Display content from tag dump file --------------------------------------------------------------------------------------- hf ntag424 info available offline: no Get info about NXP NTAG424 DNA Family styled tag.", @@ -9885,6 +9905,21 @@ ], "usage": "hf ntag424 read [-h] --fileno <1|2|3> [--keyno ] [-k ] [-o ] -l [-m ]" }, + "hf ntag424 setconfig": { + "command": "hf ntag424 setconfig", + "description": "Change a PICC configuration option. Several options are one-way, see the datasheet before using this.", + "notes": [ + "hf ntag424 setconfig -k 00000000000000000000000000000000 --option 07 -d 0100" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-k, --key0 Authentication key (must be key 0, HEX 16 bytes)", + "--option Configuration option (HEX 1 byte)", + "-d, --data Option data (HEX, 1..47 bytes)" + ], + "usage": "hf ntag424 setconfig [-h] -k --option -d [-d ]..." + }, "hf ntag424 view": { "command": "hf ntag424 view", "description": "Print a NTAG 424 DNA dump file (bin/eml/json)", @@ -11010,12 +11045,12 @@ }, "hw bwm help": { "command": "hw bwm help", - "description": "help This help --------------------------------------------------------------------------------------- hw bwm autooff available offline: no Toggle automatic power-off when the PM5 is unplugged from USB (BWM only). Default is on. When on, the board powers itself down ~10s after USB is removed, so a BWM-equipped PM5 doesn't silently drain the battery. Button power-on is unaffected. Disable for standalone/BLE use on battery. Runtime only: resets to on at each boot.", + "description": "--------------------------------------------------------------------------------------- hw bwm autooff available offline: no Toggle automatic power-off when the PM5 is unplugged from USB (BWM only). Default is on. When on, the board powers itself down ~10s after USB is removed, so a BWM-equipped PM5 doesn't silently drain the battery. Button power-on is unaffected. Disable for standalone/BLE use on battery. Runtime only: resets to on at each boot.", "notes": [ "hw bwm autooff off -> disable auto power-off", "hw bwm autooff on -> re-enable auto power-off" ], - "offline": true, + "offline": false, "options": [ "-h, --help This help" ], @@ -12776,7 +12811,7 @@ "-1, --ht1 Card type Hitag 1", "-2, --ht2 Card type Hitag 2", "-s, --hts Card type Hitag S", - "-m, --htm Card type Hitag \u03bc" + "-m, --htm Card type Hitag \u00b5" ], "usage": "lf hitag eload [-h12sm] -f " }, @@ -12794,7 +12829,7 @@ "-1, --ht1 Card type Hitag 1", "-2, --ht2 Card type Hitag 2 (default)", "-s, --hts Card type Hitag S", - "-m, --htm Card type Hitag \u03bc" + "-m, --htm Card type Hitag \u00b5" ], "usage": "lf hitag eview [-hv12sm]" }, @@ -16616,8 +16651,8 @@ } }, "metadata": { - "commands_extracted": 924, + "commands_extracted": 926, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-16T22:14:12+00:00" + "extracted_on": "2026-09-24T08:29:19+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index 68718d6e8..1ec5acd84 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -855,6 +855,8 @@ Check column "offline" for their availability. |`hf ntag424 getfs `|N |`Get file settings` |`hf ntag424 changefs `|N |`Change file settings` |`hf ntag424 changekey `|N |`Change key` +|`hf ntag424 gettt `|N |`Get Tag Tamper status` +|`hf ntag424 setconfig `|N |`Set PICC configuration option` ### hf saflok @@ -1044,7 +1046,7 @@ Check column "offline" for their availability. |command |offline |description |------- |------- |----------- -|`hw bwm help `|Y |`This help` +|`hw bwm help `|N |`This help` |`hw bwm autooff `|N |`Toggle auto power-off on USB unplug` |`hw bwm charge `|N |`Enable/disable battery charging (one-shot)` |`hw bwm name `|N |`Get/set the BWM BLE advertising name` From 4f4acedbddc0fe0b889754b6b32f523a95420351 Mon Sep 17 00:00:00 2001 From: Msprg <18015488+Msprg@users.noreply.github.com> Date: Sat, 12 Sep 2026 03:03:57 +0200 Subject: [PATCH 2/7] pm5: reduce idle power (core clock scaling + WFI idle) Port the "reduce idle power" feature from the Fantasi firmware (soeinova/Fantasi @ 9671309, plus its later ACC-SOF fix) to the PM5 (AT32F435) target. Problem ------- The PM5 firmware ran the ARM core at 288 MHz with a 1.3 V LDO and a main loop that busy-polled at full speed even when the device was idle. The 288 MHz PLL and the 1.3 V LDO dominate idle draw, and the busy loop adds to it. Nothing scaled back when there was no work. Change ------ New armsrc/pm5_power.c drives a refcounted clock/voltage governor: - Idle (nothing pending, boost count 0, boot grace elapsed, FPGA off): take SCLK off the PLL onto HICK-48, power the PLL down, drop the LDO to 1.1 V, park the FPGA 24 MHz clock (PA8 low), and halt the core in WFI until the next IRQ or the 1 ms SysTick wake. - Any timing-critical work holds a 288 MHz boost: AppMain wraps PacketReceived (which on the PM5 also covers a standalone mode, since it is only entered through a command there); the BWM low-batt poll and the RGB indicator wrap their I2C. Boost is refcounted and IRQ-guarded. - USB (OS image) moves to the crystal-less HICK-48 clock, ACC-trimmed against the SOF, so powering the PLL down never touches USB. The bootrom keeps the HEXT/PLL USB path (AS_BOOTROM), so DFU flashing is unchanged. - bwm_uart_clock_update() re-inits UART4 through the SDK usart_init() on each clock switch (the divider set at 288 MHz is 6x off at 48 MHz). - FpgaIsOff() (fpga_core.c, PM5 only) tracks the last conf word so the governor never downclocks while a reader field / emulation is running. - Wake preamble on the BWM link: the companion module firmware (Proxmark5_BWM_esp32) light-sleeps 2 s after the last byte on the link and its UART wakes on RX edges but loses the bytes that carried them. bwm_uart_write() therefore leads with four 0x55 bytes and a 10 ms settle on its first write and after 1 s without a write of its own (RX is not tracked: the ring can be drained long after the bytes came in). The module only starts light-sleeping once it has seen a preamble, so the one sent while it was still booting is repeated after the boot-time link negotiation. A module firmware without light sleep drops the preamble as noise. New `hw powersave on|off` toggles the governor at runtime (default on); `hw status` reports the idle-clock and WFI-asleep fractions. Behaviour change ---------------- - PM5 idles at 48 MHz with the PLL off and the CPU halted; it boosts to 288 MHz for commands and BWM housekeeping. Command timing and throughput are unchanged (intra-command work is always boosted). - PM5 USB now runs off HICK-48 (ACC-trimmed) instead of the HEXT PLL. - New command id CMD_PM5_POWERSAVE (0x0180). Testing ------- Built warning-free (gcc) for PLATFORM=PM5 (with and without PLATFORM_EXTRAS=BWM), PM3RDV4 and PM3GENERIC, armsrc and bootrom. Client built via the default Makefile. Not built with clang or on Windows/macOS; nothing outside PM5/AT32 guards changes. Flashed to a PM5 and exercised against a MIFARE Classic 4K card: hw version/status/ping (4000 & 512 B), hw tune, hf search, hf 14a info/reader (x12 across idle gaps), hf mf info/chk/rdbl, lf search/read, hw powersave off/on, held-field (raw -sk) correctly blocks the downclock, hw reset + re-enumerate. All pass; USB stays up across downclocks. Idle reaches ~80-95% at 48 MHz and ~90%+ WFI-halted after a few seconds idle. Measured at the USB port: idle draw 283 mA -> 207 mA with the stock BWM firmware. Co-authored-by: Soei Nova Co-authored-by: noproto Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 1 + armsrc/CMakeLists.txt | 1 + armsrc/Makefile | 8 ++ armsrc/appmain.c | 32 ++++- armsrc/bwm_charger.c | 23 +++- armsrc/bwm_uart_at32.c | 41 +++++- armsrc/bwm_uart_at32.h | 10 ++ armsrc/pm5_power.c | 241 ++++++++++++++++++++++++++++++++++ armsrc/pm5_power.h | 46 +++++++ armsrc/rgb_indicator.c | 3 + client/src/cmdhw.c | 45 +++++++ common_arm/fpga/fpga_apis.h | 7 + common_arm/fpga/fpga_core.c | 13 ++ common_arm/usb/usb_cdc_at32.c | 21 +-- doc/commands.json | 17 ++- doc/commands.md | 1 + include/pm3_cmd.h | 2 + 17 files changed, 490 insertions(+), 22 deletions(-) create mode 100644 armsrc/pm5_power.c create mode 100644 armsrc/pm5_power.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 335a9f42c..5ecae15c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Added `hw powersave` - PM5 low-power idle, on by default: between commands the core drops to 48 MHz with the PLL off, the FPGA clock stopped and the CPU halted (WFI); USB now runs crystal-less off HICK. Ported from the Fantasi firmware (@Msprg) - Fixed `hf mf autopwn` on FM11RF08S - static encrypted nonce is now detected via the backdoor key (@iceman1001) - Added `hf mfu view -v` - prints the UL-C 3DES key or UL-AES key stored in the dump file (@iceman1001) - Added `hf mf sen --ns` - skip writing the key and dump files (@iceman1001) diff --git a/armsrc/CMakeLists.txt b/armsrc/CMakeLists.txt index 3960c5710..530fbba0a 100644 --- a/armsrc/CMakeLists.txt +++ b/armsrc/CMakeLists.txt @@ -313,6 +313,7 @@ if (PM5) list(APPEND THUMBSRC startup_at32f435_437.s) list(APPEND THUMBSRC i2c.c) list(APPEND THUMBSRC buzzer.c) # generic mainboard buzzer (QC self-test + BWM low-batt) + list(APPEND THUMBSRC pm5_power.c) # low-power idle (core clock scaling + WFI) list(APPEND THUMBSRC at32_unit_test.c) # TODO DXL: AT32单元测试代码 endif () diff --git a/armsrc/Makefile b/armsrc/Makefile index 931370c4c..4f68a5569 100644 --- a/armsrc/Makefile +++ b/armsrc/Makefile @@ -138,6 +138,13 @@ else SRC_BUZZER = endif +# PM5 low-power idle (core clock scaling + WFI) +ifeq ($(PLATFORM),PM5) + SRC_PM5_POWER = pm5_power.c +else + SRC_PM5_POWER = +endif + # PM5 antenna-RGB power/battery indicator ifneq (,$(findstring WITH_PM5_PWR_LED,$(APP_CFLAGS))) SRC_RGB_INDICATOR = rgb_indicator.c @@ -204,6 +211,7 @@ THUMBSRC = start.c \ $(SRC_BWM) \ $(SRC_BWM_CHARGER) \ $(SRC_BUZZER) \ + $(SRC_PM5_POWER) \ $(SRC_RGB_INDICATOR) \ $(SRC_HITAG) \ $(SRC_EM4x50) \ diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 3fba9bfec..76db985f5 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -89,6 +89,7 @@ #include "bwm_charger.h" // BWM charger / fuel-gauge + low-batt warning (WITH_BWM_*) #include "buzzer.h" // PM5 mainboard buzzer API #include "rgb_indicator.h" // PM5 antenna-RGB power/battery indicator (WITH_PM5_PWR_LED) +#include "pm5_power.h" // PM5 power-save idle (clock scaling + WFI) #ifdef WITH_PM5_AUTOOFF @@ -605,6 +606,9 @@ static void SendStatus(uint32_t wait) { Dbprintf(" BWM fw version...... " _YELLOW_("%s"), "unknown"); } } +#endif +#ifdef PM5 + pm5_power_print_status(); #endif printConnSpeed(wait); DbpString(_CYAN_("Various")); @@ -4291,8 +4295,15 @@ static void PacketReceived(PacketCommandNG *packet) { #endif break; } -#endif -#endif +#endif // WITH_BWM_STATUS + case CMD_PM5_POWERSAVE: { + // Payload: 1 byte, non-zero = enable (the default), zero = disable. + pm5_power_set_enabled((packet->length >= 1) ? (packet->data.asBytes[0] != 0) : true); + uint8_t state = pm5_power_get_enabled() ? 1 : 0; + reply_ng(CMD_PM5_POWERSAVE, PM3_SUCCESS, &state, 1); + break; + } +#endif // PM5 default: { Dbprintf("%s: 0x%04x", "unknown command:", packet->cmd); break; @@ -4377,12 +4388,19 @@ void __attribute__((noreturn)) AppMain(void) { // Negotiate the link up from the boot baud; harmless no-op if the ESP is // older or the target is unreachable (stays at BWM_UART_BAUD). bwm_fwd_negotiate_baud(BWM_UART_BAUD_TARGET); + // The ESP only light-sleeps once it has seen a preamble, and ours went out + // while it was still booting if both powered up together: send it again. + bwm_uart_wake_next(); #endif #ifdef WITH_BWM_CHARGERKICK bwm_charger_kick(); #endif +#ifdef PM5 + pm5_power_init(); +#endif + for (;;) { WDT_HIT(); @@ -4410,7 +4428,13 @@ void __attribute__((noreturn)) AppMain(void) { int ret = receive_ng(&rx); if (ret == PM3_SUCCESS) { +#ifdef PM5 + pm5_power_boost(); // commands assume the full 288 MHz clock +#endif PacketReceived(&rx); +#ifdef PM5 + pm5_power_unboost(); +#endif last_activity_label = GetTickCountLabel(); last_activity_tick = GetTickCount(); } else if (ret != PM3_ENODATA) { @@ -4499,5 +4523,9 @@ void __attribute__((noreturn)) AppMain(void) { #endif } + +#ifdef PM5 + pm5_power_idle(); +#endif } } diff --git a/armsrc/bwm_charger.c b/armsrc/bwm_charger.c index c592b3cf4..9fc5f9ce7 100644 --- a/armsrc/bwm_charger.c +++ b/armsrc/bwm_charger.c @@ -27,6 +27,7 @@ #include "i2c.h" #include "dbprint.h" #include "ticks_apis.h" // WaitMS / GetTickCount / GetTickCountDelta +#include "pm5_power.h" // pm5_power_boost / unboost around the low-batt poll #include "ansi.h" #ifdef WITH_BWM_LOWBATT_BEEP @@ -470,18 +471,12 @@ static void bwm_beep_low_batt(void) { // charging or if the BWM/gauge is not present. With WITH_PM5_LOWBATT_SHUTDOWN it // also powers the board off once the pack drops to the critical floor, confirmed // across several consecutive polls so a transient HF-field sag can't trip it. -void bwm_lowbatt_check(void) { - static uint32_t last_tick = 0; +static void bwm_lowbatt_poll(void) { #ifdef WITH_PM5_LOWBATT_SHUTDOWN static uint8_t crit = 0; static bool s_vusb_setup = false; #endif - if ((last_tick != 0) && (GetTickCountDelta(last_tick) < BWM_LOWBATT_PERIOD_MS)) { - return; - } - last_tick = GetTickCount(); - StartTicks(); I2C_init(true); @@ -539,4 +534,18 @@ void bwm_lowbatt_check(void) { bwm_beep_low_batt(); // BuzzerBeep() lazily sets the buzzer up on first use } + +void bwm_lowbatt_check(void) { + static uint32_t last_tick = 0; + + if ((last_tick != 0) && (GetTickCountDelta(last_tick) < BWM_LOWBATT_PERIOD_MS)) { + return; + } + last_tick = GetTickCount(); + + // Ticks timer, I2C and buzzer timing assume the full core clock. + pm5_power_boost(); + bwm_lowbatt_poll(); + pm5_power_unboost(); +} #endif // WITH_BWM_LOWBATT_BEEP diff --git a/armsrc/bwm_uart_at32.c b/armsrc/bwm_uart_at32.c index e796f1945..02088cfe2 100644 --- a/armsrc/bwm_uart_at32.c +++ b/armsrc/bwm_uart_at32.c @@ -21,6 +21,7 @@ #include "bwm_uart_at32.h" #include "pm3_cmd.h" +#include "ticks_apis.h" // GetTickCount / GetTickCountDelta / SpinDelay #include "at32f435_437.h" #include "at32f435_437_crm.h" #include "at32f435_437_gpio.h" @@ -52,6 +53,16 @@ static volatile uint16_t s_rx_tail = 0; // software read cursor; head comes fr static volatile bool s_inited = false; static volatile uint32_t s_cur_baud = BWM_UART_BAUD; +// The ESP light-sleeps BWM_ESP_AWAKE_MS after the last byte either way and loses +// the bytes whose edges wake it, so the first write, and any after +// BWM_ESP_WAKE_AFTER_MS of silence of ours (RX is not tracked, see bwm_uart_read), +// leads with a disposable preamble; ESP fw without light sleep resyncs past it. +#define BWM_ESP_AWAKE_MS 2000 // == UART_LINK_AWAKE_MS on the ESP +#define BWM_ESP_WAKE_AFTER_MS 1000 // preamble when quiet longer than this (margin for drift) +#define BWM_ESP_WAKE_SETTLE_MS 10 // light-sleep exit + UART driver back up +static volatile uint32_t s_last_traffic_tick = 0; +static volatile bool s_tx_ever = false; // nothing sent yet: the tick above means nothing + // Bytes the DMA controller has written so far, wrapped into the ring. // The channel's DTCNT counts DOWN from buffer_size and reloads to buffer_size // at wrap (loop mode), so head = size - remaining, always in [0, size-1]. @@ -138,7 +149,17 @@ uint32_t bwm_uart_get_baud(void) { return s_cur_baud; } -int bwm_uart_write(const uint8_t *data, size_t len) { +void bwm_uart_clock_update(void) { + if (s_inited == false) { + return; + } + // Recompute the baud divider for the new APB1 clock. usart_init() only + // rewrites the divider and the (unchanged) 8N1 frame bits; it does not + // touch the enable bits or the DMA request, so it is safe on a live port. + usart_init(BWM_UART, s_cur_baud, USART_DATA_8BITS, USART_STOP_1_BIT); +} + +static void bwm_uart_write_raw(const uint8_t *data, size_t len) { for (size_t i = 0; i < len; i++) { while (usart_flag_get(BWM_UART, USART_TDBE_FLAG) == RESET) { } @@ -146,9 +167,25 @@ int bwm_uart_write(const uint8_t *data, size_t len) { } while (usart_flag_get(BWM_UART, USART_TDC_FLAG) == RESET) { } +} + +int bwm_uart_write(const uint8_t *data, size_t len) { + if ((s_tx_ever == false) || (GetTickCountDelta(s_last_traffic_tick) > BWM_ESP_WAKE_AFTER_MS)) { + // 0x55 = five rising edges per byte; the ESP wakes after three. + static const uint8_t wake[4] = { 0x55, 0x55, 0x55, 0x55 }; + bwm_uart_write_raw(wake, sizeof(wake)); + SpinDelay(BWM_ESP_WAKE_SETTLE_MS); + } + bwm_uart_write_raw(data, len); + s_last_traffic_tick = GetTickCount(); + s_tx_ever = true; return PM3_SUCCESS; } +void bwm_uart_wake_next(void) { + s_tx_ever = false; +} + uint16_t bwm_uart_rx_available(void) { // An unhandled overrun (ROERR) latches on this USART and stops it feeding the // DMA - after one overrun every subsequent byte is lost until a re-init, which @@ -170,5 +207,7 @@ uint32_t bwm_uart_read(uint8_t *data, size_t len) { data[n++] = s_rx_ring[s_rx_tail]; s_rx_tail = (uint16_t)((s_rx_tail + 1) & (BWM_RX_RING_SZ - 1)); } + // RX does not refresh s_last_traffic_tick: the ring can be drained long after + // the ESP sent the bytes, so it says nothing about the module's awake window. return n; } diff --git a/armsrc/bwm_uart_at32.h b/armsrc/bwm_uart_at32.h index b5ead636d..080aac79e 100644 --- a/armsrc/bwm_uart_at32.h +++ b/armsrc/bwm_uart_at32.h @@ -36,8 +36,18 @@ void bwm_uart_set_baud(uint32_t baud); // Baud the link is currently running at (updated by bwm_uart_set_baud). uint32_t bwm_uart_get_baud(void); +// Re-derive the baud divider for the current APB1 clock. Called on every core +// clock switch (pm5_power.c); the divider set at 288 MHz is 6x off at 48 MHz. +void bwm_uart_clock_update(void); + +// Blocking write of one whole frame. On the first write, and after ~1 s without +// a write of our own, it first sends a wake preamble for the ESP's light sleep +// and waits ~10 ms (see the BWM_ESP_* defines in bwm_uart_at32.c). int bwm_uart_write(const uint8_t *data, size_t len); +// Lead the next write with the wake preamble, whatever the quiet time. +void bwm_uart_wake_next(void); + uint16_t bwm_uart_rx_available(void); uint32_t bwm_uart_read(uint8_t *data, size_t len); diff --git a/armsrc/pm5_power.c b/armsrc/pm5_power.c new file mode 100644 index 000000000..070d851b9 --- /dev/null +++ b/armsrc/pm5_power.c @@ -0,0 +1,241 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Proxmark5 (AT32F435) power-save idle - see pm5_power.h. +// +// Idle: SCLK PLL-288 -> HICK-48, PLL off, LDO 1.3 -> 1.1 V (good to 144 MHz), +// FPGA CLKOUT1 parked low, core in WFI until an IRQ or a 1 ms SysTick wake. +// USB is crystal-less off HICK-48 (ACC-trimmed on the SOF, usb_cdc_at32.c) and +// the 1 kHz tick counter is on the ERTC (HEXT/20), so neither notices the PLL +// going away. Everything else keeps its 288 MHz assumption via the boost. +// +// Ported from the Fantasi firmware. +//----------------------------------------------------------------------------- +#include "pm5_power.h" + +#include "proxmark3_arm.h" +#include "dbprint.h" +#include "ticks_apis.h" +#include "fpga_apis.h" +#include "config_gpio_proxmark5.h" +#ifdef WITH_BWM_FORWARD +#include "bwm_uart_at32.h" +#endif + +#include "at32f435_437.h" +#include "at32f435_437_crm.h" +#include "at32f435_437_pwc.h" +#include "at32f435_437_gpio.h" + +// Full speed through USB enumeration and the rest of the boot. +#define PM5_POWER_BOOT_GRACE_MS 3000 +// Quiet time after the last boost before downclocking: back-to-back commands +// (autopwn, scripts, BWM forwarded traffic) don't pay the switch each time. +#define PM5_POWER_IDLE_MS 200 +#define PM5_POWER_CLK_WAIT 500000u + +static volatile uint32_t s_boost; // >0 = something holds 288 MHz +static volatile bool s_clk_high = true; // boot runs at 288 MHz +static bool s_powersave = true; +static uint32_t s_last_busy_tick; + +// Stats for `hw status`. +static uint32_t s_downclocks; +static uint32_t s_low_ms; +static uint32_t s_low_since_tick; +static uint32_t s_sleeps; +static uint32_t s_asleep_ms; +static uint32_t s_asleep_us_resid; + +static void pm5_fpga_clk_park(void) { + // Static low, not floating, while the PLL behind CLKOUT1 is off. + gpio_init_type gpio_init_struct; + gpio_default_para_init(&gpio_init_struct); + gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER; + gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL; + gpio_init_struct.gpio_mode = GPIO_MODE_OUTPUT; + gpio_init_struct.gpio_pins = AT32_GPIO_FPGA_24M_CLK_PIN; + gpio_init_struct.gpio_pull = GPIO_PULL_NONE; + gpio_bits_reset(AT32_GPIO_FPGA_24M_CLK, AT32_GPIO_FPGA_24M_CLK_PIN); + gpio_init(AT32_GPIO_FPGA_24M_CLK, &gpio_init_struct); +} + +// Voltage-safe ordering: up = LDO before frequency, down = frequency before LDO. +// The LDO is only writable while SCLK is on HICK/HEXT, which holds on both paths. +// Runs with IRQs masked. +static void pm5_clk_apply(bool high) { + uint32_t i; + if (high) { + pwc_ldo_output_voltage_set(PWC_LDO_OUTPUT_1V3); + for (volatile uint32_t d = 0; d < 2000; d++) {} // LDO settle + + crm_clock_source_enable(CRM_CLOCK_SOURCE_PLL, TRUE); + for (i = 0; i < PM5_POWER_CLK_WAIT; i++) { + if (crm_flag_get(CRM_PLL_STABLE_FLAG) == SET) break; + } + crm_auto_step_mode_enable(TRUE); + crm_sysclk_switch(CRM_SCLK_PLL); + for (i = 0; i < PM5_POWER_CLK_WAIT; i++) { + if (crm_sysclk_switch_status_get() == CRM_SCLK_PLL) break; + } + crm_auto_step_mode_enable(FALSE); + system_core_clock_update(); + + FpgaSetup24MHzClk(); // CLKOUT1 back on PA8 + } else { + pm5_fpga_clk_park(); + + crm_clock_source_enable(CRM_CLOCK_SOURCE_HICK, TRUE); + for (i = 0; i < PM5_POWER_CLK_WAIT; i++) { + if (crm_flag_get(CRM_HICK_STABLE_FLAG) == SET) break; + } + crm_auto_step_mode_enable(TRUE); + crm_sysclk_switch(CRM_SCLK_HICK); + for (i = 0; i < PM5_POWER_CLK_WAIT; i++) { + if (crm_sysclk_switch_status_get() == CRM_SCLK_HICK) break; + } + crm_auto_step_mode_enable(FALSE); + crm_clock_source_enable(CRM_CLOCK_SOURCE_PLL, FALSE); + pwc_ldo_output_voltage_set(PWC_LDO_OUTPUT_1V1); + system_core_clock_update(); + } +#ifdef WITH_BWM_FORWARD + bwm_uart_clock_update(); // UART4 baud follows APB1 +#endif + s_clk_high = high; +} + +void pm5_power_init(void) { + // SCLK-from-HICK must be 48 MHz, not the 8 MHz reset default. usb_enable() + // already selects this for the crystal-less USB clock; make it explicit. + crm_hick_sclk_frequency_select(CRM_HICK_SCLK_48MHZ); + + // Free-running cycle counter: measures the time actually spent in WFI. + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CYCCNT = 0; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + + s_last_busy_tick = GetTickCount(); +} + +void pm5_power_boost(void) { + __disable_irq(); + if (s_boost++ == 0 && s_clk_high == false) { + pm5_clk_apply(true); + s_low_ms += GetTickCountDelta(s_low_since_tick); + } + __enable_irq(); +} + +void pm5_power_unboost(void) { + __disable_irq(); + if (s_boost > 0) { + s_boost--; + } + __enable_irq(); + // The downclock itself is left to pm5_power_idle() after PM5_POWER_IDLE_MS. + if (s_boost == 0) { + s_last_busy_tick = GetTickCount(); + } +} + +static bool pm5_power_may_downclock(void) { + if (s_boost != 0 || s_clk_high == false) { + return false; + } + if (GetTickCount() < PM5_POWER_BOOT_GRACE_MS) { + return false; + } + if (GetTickCountDelta(s_last_busy_tick) < PM5_POWER_IDLE_MS) { + return false; + } + // A reader field or emulation left running needs the FPGA clock. + return FpgaIsOff(); +} + +// Halt until the next IRQ, at most ~1 ms (one-shot SysTick, AHB/8 source like +// SpinDelayUs which reprograms SysTick anyway). IRQs stay masked across the +// WFI so the cycle delta measures only the sleep; the waking ISR runs after. +static void pm5_power_wfi(void) { + SysTick->CTRL = 0; + SysTick->LOAD = (system_core_clock / 8 / 1000) - 1; + SysTick->VAL = 0; + SysTick->CTRL = SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; + + __disable_irq(); + uint32_t t0 = DWT->CYCCNT; + __WFI(); + uint32_t cycles = DWT->CYCCNT - t0; + __enable_irq(); + + SysTick->CTRL = 0; + + s_sleeps++; + s_asleep_us_resid += cycles / (system_core_clock / 1000000); + if (s_asleep_us_resid >= 1000) { + s_asleep_ms += s_asleep_us_resid / 1000; + s_asleep_us_resid %= 1000; + } +} + +void pm5_power_idle(void) { + if (s_powersave == false) { + return; + } + if (pm5_power_may_downclock()) { + SpinDelayUs(10); // let a just-sent FPGA conf word land before its clock stops + __disable_irq(); + pm5_clk_apply(false); + __enable_irq(); + s_downclocks++; + s_low_since_tick = GetTickCount(); + } + pm5_power_wfi(); +} + +void pm5_power_set_enabled(bool on) { + s_powersave = on; + if (on == false && s_clk_high == false) { + __disable_irq(); + pm5_clk_apply(true); + __enable_irq(); + s_low_ms += GetTickCountDelta(s_low_since_tick); + } +} + +bool pm5_power_get_enabled(void) { + return s_powersave; +} + +// Per-mille of uptime, printed as a percentage with one decimal. +static uint32_t pm5_power_permille(uint32_t ms, uint32_t uptime_ms) { + if (uptime_ms == 0) { + return 0; + } + return (uint32_t)(((uint64_t)ms * 1000) / uptime_ms); +} + +void pm5_power_print_status(void) { + uint32_t uptime = GetTickCount(); + uint32_t low = pm5_power_permille(s_low_ms, uptime); + uint32_t asleep = pm5_power_permille(s_asleep_ms, uptime); + + DbpString(_CYAN_("Power")); + Dbprintf(" Power-save idle..... %s", s_powersave ? _GREEN_("enabled") : _YELLOW_("disabled")); + DbpString(" Core clock.......... 288 MHz active, 48 MHz idle ( PLL off, LDO 1.1 V )"); + Dbprintf(" Uptime.............. %u ms", uptime); + Dbprintf(" Idle at 48 MHz...... %u.%u %% ( %u downclocks )", low / 10, low % 10, s_downclocks); + Dbprintf(" Halted in WFI....... %u.%u %% ( %u sleeps )", asleep / 10, asleep % 10, s_sleeps); +} diff --git a/armsrc/pm5_power.h b/armsrc/pm5_power.h new file mode 100644 index 000000000..4221031c5 --- /dev/null +++ b/armsrc/pm5_power.h @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Proxmark5 power-save idle: core clock scaling + WFI - see pm5_power.c. +//----------------------------------------------------------------------------- +#ifndef PM5_POWER_H__ +#define PM5_POWER_H__ + +#include "common.h" + +#ifdef PM5 + +// Call once from AppMain after USB is up. +void pm5_power_init(void); + +// Refcounted hold on the full 288 MHz core clock. Wrap anything with timing +// that assumes the boot clock (commands, standalone mode, I2C, buzzer). +void pm5_power_boost(void); +void pm5_power_unboost(void); + +// Main-loop idle step: drop to the idle clock when allowed, then halt the core +// until the next IRQ or the 1 ms wake tick. +void pm5_power_idle(void); + +// Runtime toggle (default on). Off = stay at 288 MHz and spin. +void pm5_power_set_enabled(bool on); +bool pm5_power_get_enabled(void); + +// `hw status` section. +void pm5_power_print_status(void); + +#endif // PM5 + +#endif // PM5_POWER_H__ diff --git a/armsrc/rgb_indicator.c b/armsrc/rgb_indicator.c index 3a3cd91d2..ff1aff23c 100644 --- a/armsrc/rgb_indicator.c +++ b/armsrc/rgb_indicator.c @@ -21,6 +21,7 @@ #ifdef WITH_PM5_PWR_LED #include "rgb_apis.h" // RgbLedSet +#include "pm5_power.h" // pm5_power_boost / unboost #include "gpio_apis.h" // gpio_vusb_setup / Gpio_VUSB_Read (USB-present detection) #include "ticks_apis.h" // GetTickCount / GetTickCountDelta @@ -68,6 +69,7 @@ void rgb_indicator_update(void) { return; // edge-triggered: only write RGB when the state changes } + pm5_power_boost(); // I2C bit-bang timing assumes the full core clock // Commit last_state only if the controller actually ACKed. On a cold boot the // I2C RGB controller can still be powering up when the first write fires; if we // latched last_state regardless, a NAK here would leave the LED dark until the @@ -78,6 +80,7 @@ void rgb_indicator_update(void) { if (ok) { last_state = on_battery; } + pm5_power_unboost(); } #endif // WITH_PM5_PWR_LED diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 171965edc..386f3f556 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2033,6 +2033,50 @@ static int CmdPM5QCTest(const char *Cmd) { } +static int CmdPowerSave(const char *Cmd) { + // Positional sub-action (no dashes): hw powersave on | off + char verb[16] = {0}; + sscanf(Cmd, "%15s", verb); + bool on = (strcmp(verb, "on") == 0); + bool off = (strcmp(verb, "off") == 0); + + if (!on && !off) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw powersave", + "Toggle the PM5 power-save idle. Default is " _GREEN_("on") ": between commands the\n" + "core drops from 288 to 48 MHz with the PLL off, the FPGA clock stopped and the\n" + "CPU halted (WFI) until the next interrupt or the 1 ms wake tick. Commands run\n" + "at full speed; turn it off to keep the core at 288 MHz and spinning.\n" + _YELLOW_("Runtime only:") " resets to on at each boot. See `hw status` for the idle stats.", + "hw powersave off --> stay at 288 MHz, no idle sleep\n" + "hw powersave on --> re-enable the power-save idle"); + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + PrintAndLogEx(WARNING, "specify " _YELLOW_("on") " or " _YELLOW_("off")); + return PM3_EINVARG; + } + + uint8_t payload = off ? 0 : 1; + + clearCommandBuffer(); + SendCommandNG(CMD_PM5_POWERSAVE, &payload, sizeof(payload)); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_PM5_POWERSAVE, &resp, 2500) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5?)"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "failed to set power-save idle"); + return resp.status; + } + PrintAndLogEx(SUCCESS, "Power-save idle %s.", payload ? _GREEN_("enabled") : _YELLOW_("disabled")); + return PM3_SUCCESS; +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"}, @@ -2054,6 +2098,7 @@ static command_t CommandTable[] = { {"lcd", CmdLCD, IfPm3Lcd, "Send command/data to LCD"}, {"lcdreset", CmdLCDReset, IfPm3Lcd, "Hardware reset LCD"}, {"ping", CmdPing, IfPm3Present, "Test if the Proxmark3 is responsive"}, + {"powersave", CmdPowerSave, IfPm5, "Enable/disable the PM5 power-save idle"}, {"readmem", CmdReadmem, IfPm3Present, "Read from MCU flash"}, {"reset", CmdReset, IfPm3Present, "Reset the device"}, {"setlfdivisor", CmdSetDivisor, IfPm3Lf, "Drive LF antenna at 12MHz / (divisor + 1)"}, diff --git a/common_arm/fpga/fpga_apis.h b/common_arm/fpga/fpga_apis.h index 018f4086d..cebc1e940 100644 --- a/common_arm/fpga/fpga_apis.h +++ b/common_arm/fpga/fpga_apis.h @@ -327,6 +327,13 @@ void FpgaSendCommand(uint16_t cmd, uint16_t v); //----------------------------------------------------------------------------- void FpgaWriteConfWord(uint16_t v); +#ifdef PM5 +//----------------------------------------------------------------------------- +// True when the last configuration word selected FPGA_MAJOR_MODE_OFF. +//----------------------------------------------------------------------------- +bool FpgaIsOff(void); +#endif + //----------------------------------------------------------------------------- // enable FPGA internal tracing //----------------------------------------------------------------------------- diff --git a/common_arm/fpga/fpga_core.c b/common_arm/fpga/fpga_core.c index df7b312bc..040f0c442 100644 --- a/common_arm/fpga/fpga_core.c +++ b/common_arm/fpga/fpga_core.c @@ -15,8 +15,15 @@ bool FpgaIs16BitMsbMode(uint16_t fpga_mode) { return false; } +#ifdef PM5 +static uint16_t s_fpga_conf_word = FPGA_MAJOR_MODE_OFF; +#endif + void FpgaWriteConfWord(uint16_t v) { const int current = FpgaGetCurrent(); +#ifdef PM5 + s_fpga_conf_word = v; +#endif // Keep track of whether or not we should be monitoring the HF field timeout if (current == FPGA_BITSTREAM_HF || current == FPGA_BITSTREAM_HF_15 || current == FPGA_BITSTREAM_HF_FELICA) { @@ -44,6 +51,12 @@ void FpgaWriteConfWord(uint16_t v) { FpgaSendCommand(FPGA_CMD_SET_CONFREG, v); } +#ifdef PM5 +bool FpgaIsOff(void) { + return (s_fpga_conf_word & FPGA_MAJOR_MODE_MASK) == FPGA_MAJOR_MODE_OFF; +} +#endif + void FpgaEnableTracing(void) { FpgaSendCommand(FPGA_CMD_TRACE_ENABLE, 1); } diff --git a/common_arm/usb/usb_cdc_at32.c b/common_arm/usb/usb_cdc_at32.c index f2999160a..80c725b98 100644 --- a/common_arm/usb/usb_cdc_at32.c +++ b/common_arm/usb/usb_cdc_at32.c @@ -162,26 +162,23 @@ static usbd_desc_handler cdc_desc_handler = { */ static void usb_clock48m_select(usb_clk48_s clk_s) { if (clk_s == USB_CLK_HICK) { - /* UNUSED!!! - + // HICK-48 (also makes SCLK-from-HICK 48 MHz), trimmed by the ACC against + // the USB SOF to +/-0.25%. Independent of the PLL, so the PLL can be + // powered down at idle (armsrc/pm5_power.c) without touching USB. crm_usb_clock_source_select(CRM_USB_CLOCK_SOURCE_HICK); - // enable the acc calibration ready interrupt crm_periph_clock_enable(CRM_ACC_PERIPH_CLOCK, TRUE); - // update the c1\c2\c3 value + // compare window around the ideal 8000 HICK/6 counts per 1 ms SOF acc_write_c1(7980); acc_write_c2(8000); acc_write_c3(8020); - #if (USB_ID == 0) +#if (USB_ID == 0) acc_sof_select(ACC_SOF_OTG1); - #else +#else acc_sof_select(ACC_SOF_OTG2); - #endif - // open acc calibration +#endif acc_calibration_mode_enable(ACC_CAL_HICKTRIM, TRUE); - - */ } else { switch (system_core_clock) { /* 48MHz */ @@ -374,7 +371,11 @@ void usb_enable(void) { usb_gpio_config(); crm_periph_clock_enable(OTG_CLOCK, TRUE); // enable otgfs clock +#ifdef AS_BOOTROM usb_clock48m_select(USB_CLK_HEXT); // select usb 48m clcok source +#else + usb_clock48m_select(USB_CLK_HICK); // crystal-less, survives the idle PLL-off (pm5_power.c) +#endif nvic_irq_enable(OTG_IRQ, 0, 0); // enable otgfs irq usbd_init(&otg_core_struct, USB_FULL_SPEED_CORE_ID, USB_ID, &cdc_class_handler, &cdc_desc_handler); // init usb diff --git a/doc/commands.json b/doc/commands.json index 9649e0ae0..62c0d4381 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -11261,6 +11261,19 @@ ], "usage": "hw ping [-h] [-l ]" }, + "hw powersave": { + "command": "hw powersave", + "description": "Toggle the PM5 power-save idle. Default is on: between commands the core drops from 288 to 48 MHz with the PLL off, the FPGA clock stopped and the CPU halted (WFI) until the next interrupt or the 1 ms wake tick. Commands run at full speed; turn it off to keep the core at 288 MHz and spinning. Runtime only: resets to on at each boot. See `hw status` for the idle stats.", + "notes": [ + "hw powersave off -> stay at 288 MHz, no idle sleep", + "hw powersave on -> re-enable the power-save idle" + ], + "offline": false, + "options": [ + "-h, --help This help" + ], + "usage": "hw powersave [-h]" + }, "hw qc_pm5": { "command": "hw qc_pm5", "description": "QC Test for the PM5", @@ -16651,8 +16664,8 @@ } }, "metadata": { - "commands_extracted": 926, + "commands_extracted": 927, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-24T08:29:19+00:00" + "extracted_on": "2026-09-24T08:29:44+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index 1ec5acd84..1bd8490aa 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -1029,6 +1029,7 @@ Check column "offline" for their availability. |`hw lcd `|N |`Send command/data to LCD` |`hw lcdreset `|N |`Hardware reset LCD` |`hw ping `|N |`Test if the Proxmark3 is responsive` +|`hw powersave `|N |`Enable/disable the PM5 power-save idle` |`hw readmem `|N |`Read from MCU flash` |`hw reset `|N |`Reset the device` |`hw setlfdivisor `|N |`Drive LF antenna at 12MHz / (divisor + 1)` diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 86c82a17f..ed48c11e8 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -924,6 +924,8 @@ typedef struct { #define BWM_BLE_NAME_ACTION_GET 0x00 // resp: current BLE device name string #define BWM_BLE_NAME_ACTION_SET 0x01 // req: name bytes follow the action byte; BWM reboots to apply #define BWM_BLE_NAME_MAX_LEN 15 // usable chars; ESP name buffer is 16 incl NUL +// PM5, toggle the power-save idle (48 MHz + WFI when idle). Used by `hw powersave`. +#define CMD_PM5_POWERSAVE 0x0180 #define BWM_OTA_ACTION_BEGIN 0x00 #define BWM_OTA_ACTION_WRITE 0x01 #define BWM_OTA_ACTION_END 0x02 From 552276c75f032721e37cd0447ea37e014a73186d Mon Sep 17 00:00:00 2001 From: Msprg <18015488+Msprg@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:10:56 +0200 Subject: [PATCH 3/7] pm5: hw bwm powersave New `hw bwm powersave [on|off]` (CMD_PM5_BWM_POWERSAVE 0x0181) shows or sets the ESP's persisted power-save switch (companion Proxmark5_BWM_esp32 PR: DFS, light sleep and slow advertising after a 30 s fast phase, or the stock always-on behaviour), so the two can be A/B measured and the saving turned off when chasing a link problem. `hw status` prints the state next to the BWM firmware version. Both module queries in `hw status` are time-bounded (version 2 x 800 ms, power save 300 ms): a module that is silent, or a firmware without the command, cannot push the report past the client's timeout. Verified on hardware: state read/set/persist across an ESP reboot, `hw status` after 4, 15 and 40 s idle in power-save mode and after 6 s idle with it off. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 1 + armsrc/appmain.c | 40 +++++++++++++++++++++++++++++++-- armsrc/bwm_wifi.c | 25 +++++++++++++++++++-- armsrc/bwm_wifi.h | 8 ++++++- client/src/cmdbwm.c | 55 ++++++++++++++++++++++++++++++++++++++++++++- doc/commands.json | 22 ++++++++++++++---- doc/commands.md | 3 ++- include/pm3_cmd.h | 5 +++++ 8 files changed, 148 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ecae15c5..ff62b6015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Added `hw bwm powersave` - show/set the BWM (ESP32) power-save switch: DFS, light sleep and slow advertising after 30 s, or the stock always-on behaviour, persisted on the module (@Msprg) - Added `hw powersave` - PM5 low-power idle, on by default: between commands the core drops to 48 MHz with the PLL off, the FPGA clock stopped and the CPU halted (WFI); USB now runs crystal-less off HICK. Ported from the Fantasi firmware (@Msprg) - Fixed `hf mf autopwn` on FM11RF08S - static encrypted nonce is now detected via the backdoor key (@iceman1001) - Added `hf mfu view -v` - prints the UL-C 3DES key or UL-AES key stored in the dump file (@iceman1001) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 76db985f5..26fe723e9 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -597,11 +597,23 @@ static void SendStatus(uint32_t wait) { { // Read the ESP firmware version so hw status shows what the BWM runs // (and lets you confirm an OTA took: the string flips after a reflash). + // Keep the wait short: the client gives hw status about 2 s in total. One + // retry - a slow light-sleep wake eats the first frame but leaves the module up. uint8_t bwm_ver[64] = {0}; uint16_t bwm_ver_len = sizeof(bwm_ver) - 1; - if (bwm_esp_get_version(bwm_ver, &bwm_ver_len) == PM3_SUCCESS) { + int vres = bwm_esp_get_version(bwm_ver, &bwm_ver_len, 800); + if (vres == PM3_ETIMEOUT) { + bwm_ver_len = sizeof(bwm_ver) - 1; + vres = bwm_esp_get_version(bwm_ver, &bwm_ver_len, 800); + } + if (vres == PM3_SUCCESS) { bwm_ver[bwm_ver_len] = 0x00; Dbprintf(" BWM fw version...... " _YELLOW_("%s"), bwm_ver); + // Only once the ESP answered; older fw lacks these, so keep the wait short. + uint8_t ps = 0; + if (bwm_esp_get_power_save(&ps, 300) == PM3_SUCCESS) { + Dbprintf(" BWM power save...... " _YELLOW_("%s"), ps ? "on" : "off"); + } } else { Dbprintf(" BWM fw version...... " _YELLOW_("%s"), "unknown"); } @@ -4197,7 +4209,7 @@ static void PacketReceived(PacketCommandNG *packet) { // ack is lost). Replies here with a payload, unlike the others. uint8_t ver[64]; uint16_t vlen = sizeof(ver); - res = bwm_esp_get_version(ver, &vlen); + res = bwm_esp_get_version(ver, &vlen, 3000); reply_ng(CMD_PM5_BWM_ESP_OTA, res, ver, (res == PM3_SUCCESS) ? vlen : 0); replied = true; break; @@ -4281,6 +4293,30 @@ static void PacketReceived(PacketCommandNG *packet) { } #else reply_ng(CMD_PM5_BWM_BLE_NAME, PM3_ENOTIMPL, NULL, 0); +#endif + break; + } + case CMD_PM5_BWM_POWERSAVE: { +#ifdef WITH_BWM_FORWARD + // Payload / reply layout: pm3_cmd.h. The ESP persists it; nothing kept here. + if (packet->length < 1) { + reply_ng(CMD_PM5_BWM_POWERSAVE, PM3_EINVARG, NULL, 0); + break; + } + uint8_t action = packet->data.asBytes[0]; + uint8_t state = 0; + int res; + if (action == BWM_POWERSAVE_ACTION_GET) { + res = bwm_esp_get_power_save(&state, 3000); + } else if (action == BWM_POWERSAVE_ACTION_SET && packet->length >= 2) { + res = bwm_esp_set_power_save(packet->data.asBytes[1] != 0, &state); + } else { + reply_ng(CMD_PM5_BWM_POWERSAVE, PM3_EINVARG, NULL, 0); + break; + } + reply_ng(CMD_PM5_BWM_POWERSAVE, res, &state, (res == PM3_SUCCESS) ? 1 : 0); +#else + reply_ng(CMD_PM5_BWM_POWERSAVE, PM3_ENOTIMPL, NULL, 0); #endif break; } diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 81d74224f..3fa2b8686 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -313,8 +313,29 @@ int bwm_wifi_forward_down(void) { // boot slot, so those get generous timeouts. // --------------------------------------------------------------------------- // Read the ESP's running firmware version string (APP_CMD_GET_VERSION_INFO). -int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen) { - return bwm_cmd(BWM_CMD_GET_VERSION_INFO, NULL, 0, buf, buflen, 3000); +int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen, uint32_t timeout_ms) { + return bwm_cmd(BWM_CMD_GET_VERSION_INFO, NULL, 0, buf, buflen, timeout_ms); +} + +int bwm_esp_get_power_save(uint8_t *state, uint32_t timeout_ms) { + uint16_t len = 1; + int r = bwm_cmd(BWM_CMD_GET_SYS_POWER_SAVE, NULL, 0, state, &len, timeout_ms); + if (r == PM3_SUCCESS && len < 1) { + r = PM3_EFAILED; + } + return r; +} + +int bwm_esp_set_power_save(bool on, uint8_t *state) { + // Applied at once on the ESP (no reboot) and saved to its NVS; the reply + // carries the state the ESP ended up in. + uint8_t v = on ? 1 : 0; + uint16_t len = 1; + int r = bwm_cmd(BWM_CMD_SET_SYS_POWER_SAVE, &v, 1, state, &len, 3000); + if (r == PM3_SUCCESS && len < 1) { + r = PM3_EFAILED; + } + return r; } int bwm_esp_get_ble_name(uint8_t *buf, uint16_t *buflen) { diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 9d7b08a1c..934bb61dd 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -88,7 +88,13 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); #define BWM_CMD_START_BLE_SPP 4021 // no payload: restore BLE after OTA #define BWM_CMD_SET_BLE_DEVICE_NAME 4002 // req: name bytes #define BWM_CMD_GET_BLE_DEVICE_NAME 4003 // resp: current BLE device name string -int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen); +// ESP power-save switch (DFS + light sleep + low-duty advertising), persisted on +// the ESP. Both return the ESP's applied state in *state. +#define BWM_CMD_SET_SYS_POWER_SAVE 1019 // req: u8 0=off 1=on; resp: u8 applied state +#define BWM_CMD_GET_SYS_POWER_SAVE 1020 // resp: u8 state +int bwm_esp_get_power_save(uint8_t *state, uint32_t timeout_ms); +int bwm_esp_set_power_save(bool on, uint8_t *state); +int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen, uint32_t timeout_ms); int bwm_esp_get_ble_name(uint8_t *buf, uint16_t *buflen); int bwm_esp_set_ble_name(const uint8_t *name, uint16_t len); int bwm_esp_ota_begin(uint32_t total_size); diff --git a/client/src/cmdbwm.c b/client/src/cmdbwm.c index ebbcdfca5..07714cbfc 100644 --- a/client/src/cmdbwm.c +++ b/client/src/cmdbwm.c @@ -697,11 +697,64 @@ static int CmdBwmName(const char *Cmd) { return PM3_SUCCESS; } +static int CmdBwmPowerSave(const char *Cmd) { + // Positional sub-action (no dashes): hw bwm powersave [on | off]; no verb shows the state + char verb[16] = {0}; + sscanf(Cmd, "%15s", verb); + bool on = (strcmp(verb, "on") == 0); + bool off = (strcmp(verb, "off") == 0); + bool show = (verb[0] == 0); + + if (!on && !off && !show) { + // Not a recognised sub-action: render help (also serves -h), or error on + // a stray token, then stop. + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwm powersave", + "Show or set the BWM (ESP32) power-save switch, stored on the BWM in NVS.\n" + "Default is " _GREEN_("on") ": the ESP scales its clock down, light-sleeps between\n" + "link traffic, and after 30 s of fast advertising (boot, disconnect) advertises\n" + "once a second. " _YELLOW_("off") " pins it at full clock, no sleep, fast advertising\n" + "throughout. Applies at once and survives reboots. PM5 only.", + "hw bwm powersave --> show the current state\n" + "hw bwm powersave off --> stock always-on behaviour\n" + "hw bwm powersave on --> re-enable power saving"); + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + PrintAndLogEx(WARNING, "specify " _YELLOW_("on") ", " _YELLOW_("off") " or nothing to show the state"); + return PM3_EINVARG; + } + + uint8_t payload[2] = { show ? BWM_POWERSAVE_ACTION_GET : BWM_POWERSAVE_ACTION_SET, on ? 1 : 0 }; + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_POWERSAVE, payload, show ? 1 : 2); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_PM5_BWM_POWERSAVE, &resp, 5000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a responsive BWM?)"); + return PM3_ETIMEOUT; + } + if (resp.status == PM3_ENOTIMPL) { + PrintAndLogEx(WARNING, "firmware built without BWM link support"); + return resp.status; + } + if (resp.status != PM3_SUCCESS || resp.length < 1) { + PrintAndLogEx(FAILED, "BWM did not answer the power-save command (BWM firmware too old?)"); + return (resp.status != PM3_SUCCESS) ? resp.status : PM3_EFAILED; + } + bool state = (resp.data.asBytes[0] != 0); + PrintAndLogEx(SUCCESS, "BWM power save..... %s", state ? _GREEN_("on") : _YELLOW_("off")); + return PM3_SUCCESS; +} + static command_t BwmCommandTable[] = { - {"help", CmdHelpBwm, IfBwm, "This help"}, + {"help", CmdHelpBwm, AlwaysAvailable, "This help"}, {"autooff", CmdBwmAutoOff, IfBwm, "Toggle auto power-off on USB unplug"}, {"charge", CmdBwmCharge, IfBwm, "Enable/disable battery charging (one-shot)"}, {"name", CmdBwmName, IfBwm, "Get/set the BWM BLE advertising name"}, + {"powersave", CmdBwmPowerSave, IfBwm, "Show/set the BWM power-save switch (DFS, light sleep, slow adv)"}, {"setcap", CmdBwmSetCap, IfBwm, "Set fuel-gauge design capacity (run once after battery change)"}, {"upgrade", CmdBWMUpgrade, IfBwm, "Reflash BWM (ESP32) firmware over the BWM link, no header"}, {"vchg", CmdBwmVchg, IfBwm, "Set charger charge-voltage target (default 4100 mV)"}, diff --git a/doc/commands.json b/doc/commands.json index 62c0d4381..66197e779 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -11045,12 +11045,12 @@ }, "hw bwm help": { "command": "hw bwm help", - "description": "--------------------------------------------------------------------------------------- hw bwm autooff available offline: no Toggle automatic power-off when the PM5 is unplugged from USB (BWM only). Default is on. When on, the board powers itself down ~10s after USB is removed, so a BWM-equipped PM5 doesn't silently drain the battery. Button power-on is unaffected. Disable for standalone/BLE use on battery. Runtime only: resets to on at each boot.", + "description": "help This help --------------------------------------------------------------------------------------- hw bwm autooff available offline: no Toggle automatic power-off when the PM5 is unplugged from USB (BWM only). Default is on. When on, the board powers itself down ~10s after USB is removed, so a BWM-equipped PM5 doesn't silently drain the battery. Button power-on is unaffected. Disable for standalone/BLE use on battery. Runtime only: resets to on at each boot.", "notes": [ "hw bwm autooff off -> disable auto power-off", "hw bwm autooff on -> re-enable auto power-off" ], - "offline": false, + "offline": true, "options": [ "-h, --help This help" ], @@ -11070,6 +11070,20 @@ ], "usage": "hw bwm name [-h] [--set ]" }, + "hw bwm powersave": { + "command": "hw bwm powersave", + "description": "Show or set the BWM (ESP32) power-save switch, stored on the BWM in NVS. Default is on: the ESP scales its clock down, light-sleeps between link traffic, and after 30 s of fast advertising (boot, disconnect) advertises once a second. off pins it at full clock, no sleep, fast advertising throughout. Applies at once and survives reboots. PM5 only.", + "notes": [ + "hw bwm powersave -> show the current state", + "hw bwm powersave off -> stock always-on behaviour", + "hw bwm powersave on -> re-enable power saving" + ], + "offline": false, + "options": [ + "-h, --help This help" + ], + "usage": "hw bwm powersave [-h]" + }, "hw bwm setcap": { "command": "hw bwm setcap", "description": "Program the BWM fuel gauge (BQ27427) Design Capacity for the fitted cell. Run ONCE after fitting or replacing the battery. This triggers a gauge config-update; do not run it repeatedly, as that disrupts the Impedance Track learning cycle. PM5 only.", @@ -16664,8 +16678,8 @@ } }, "metadata": { - "commands_extracted": 927, + "commands_extracted": 928, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-24T08:29:44+00:00" + "extracted_on": "2026-09-24T08:30:13+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index 1bd8490aa..690ab782a 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -1047,10 +1047,11 @@ Check column "offline" for their availability. |command |offline |description |------- |------- |----------- -|`hw bwm help `|N |`This help` +|`hw bwm help `|Y |`This help` |`hw bwm autooff `|N |`Toggle auto power-off on USB unplug` |`hw bwm charge `|N |`Enable/disable battery charging (one-shot)` |`hw bwm name `|N |`Get/set the BWM BLE advertising name` +|`hw bwm powersave `|N |`Show/set the BWM power-save switch (DFS, light sleep, slow adv)` |`hw bwm setcap `|N |`Set fuel-gauge design capacity (run once after battery change)` |`hw bwm upgrade `|N |`Reflash BWM (ESP32) firmware over the BWM link, no header` |`hw bwm vchg `|N |`Set charger charge-voltage target (default 4100 mV)` diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index ed48c11e8..0de708f5f 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -926,6 +926,11 @@ typedef struct { #define BWM_BLE_NAME_MAX_LEN 15 // usable chars; ESP name buffer is 16 incl NUL // PM5, toggle the power-save idle (48 MHz + WFI when idle). Used by `hw powersave`. #define CMD_PM5_POWERSAVE 0x0180 +// PM5, BWM power-save switch on the ESP (DFS, light sleep, low-duty advertising; +// persisted on the BWM). Used by `hw bwm powersave`. +#define CMD_PM5_BWM_POWERSAVE 0x0181 // payload: [action:u8][state:u8 if SET]; resp: u8 applied state +#define BWM_POWERSAVE_ACTION_GET 0x00 +#define BWM_POWERSAVE_ACTION_SET 0x01 #define BWM_OTA_ACTION_BEGIN 0x00 #define BWM_OTA_ACTION_WRITE 0x01 #define BWM_OTA_ACTION_END 0x02 From 5e1b04def11dd43422ee81fa9f24771745db6536 Mon Sep 17 00:00:00 2001 From: Msprg <18015488+Msprg@users.noreply.github.com> Date: Tue, 15 Sep 2026 03:14:42 +0200 Subject: [PATCH 4/7] pm5: hw bwm wifipower, WiFi off or the modem power-save type New `hw bwm wifipower [off|none|min|max]`. `off` turns the BWM's WiFi fully off (BLE-only, persisted; the same path as `hw bwm wifi stop`, and the module's default state). The other three set the ESP's persisted WiFi modem power-save type (CMD_PM5_BWM_WIFI_PS 0x0182, module commands 2052/2053, esp_wifi_set_ps()) used while WiFi is up: min (the ESP-IDF default, the modem sleeps between DTIM beacons), max (sleeps for the whole listen interval, lowest current, slower to react), none (modem always on, lowest latency). With no argument it reports whether WiFi is up at all and the stored type. Independent of `hw bwm powersave`. The link helper's "cmd failed" diagnostic is now printed only at debug level: a status query while WiFi is off fails by design and was cluttering every call. Verified on hardware: default reads min with WiFi off; none, max and min each set and read back; off tears WiFi down; an unknown value is rejected; max survives a module reboot. Not measured with WiFi associated. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 1 + armsrc/appmain.c | 29 +++++++++++ armsrc/bwm_wifi.c | 31 ++++++++---- armsrc/bwm_wifi.h | 5 ++ client/src/cmdbwm.c | 120 ++++++++++++++++++++++++++++++++++++++------ doc/commands.json | 20 +++++++- doc/commands.md | 1 + include/pm3_cmd.h | 8 +++ 8 files changed, 190 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff62b6015..b42d632aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Added `hw bwm wifipower` - turn the BWM WiFi fully off, or set its modem power-save type (none/min/max), persisted on the module (@Msprg) - Added `hw bwm powersave` - show/set the BWM (ESP32) power-save switch: DFS, light sleep and slow advertising after 30 s, or the stock always-on behaviour, persisted on the module (@Msprg) - Added `hw powersave` - PM5 low-power idle, on by default: between commands the core drops to 48 MHz with the PLL off, the FPGA clock stopped and the CPU halted (WFI); USB now runs crystal-less off HICK. Ported from the Fantasi firmware (@Msprg) - Fixed `hf mf autopwn` on FM11RF08S - static encrypted nonce is now detected via the backdoor key (@iceman1001) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 26fe723e9..8f668d2b0 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -4317,6 +4317,35 @@ static void PacketReceived(PacketCommandNG *packet) { reply_ng(CMD_PM5_BWM_POWERSAVE, res, &state, (res == PM3_SUCCESS) ? 1 : 0); #else reply_ng(CMD_PM5_BWM_POWERSAVE, PM3_ENOTIMPL, NULL, 0); +#endif + break; + } + case CMD_PM5_BWM_WIFI_PS: { +#ifdef WITH_BWM_FORWARD + // Payload / reply layout: pm3_cmd.h. wifi_state rides along so the host + // can say the type is moot while WiFi is down. + if (packet->length < 1) { + reply_ng(CMD_PM5_BWM_WIFI_PS, PM3_EINVARG, NULL, 0); + break; + } + uint8_t action = packet->data.asBytes[0]; + uint8_t out[2] = { 0, BWM_WIFI_STATE_OFF }; + int res; + if (action == BWM_WIFI_PS_ACTION_GET) { + res = bwm_esp_get_wifi_ps(&out[0]); + } else if (action == BWM_WIFI_PS_ACTION_SET && packet->length >= 2 && packet->data.asBytes[1] <= BWM_WIFI_PS_MAX) { + res = bwm_esp_set_wifi_ps(packet->data.asBytes[1], &out[0]); + } else { + reply_ng(CMD_PM5_BWM_WIFI_PS, PM3_EINVARG, NULL, 0); + break; + } + if (res == PM3_SUCCESS) { + uint32_t ip = 0; + (void)bwm_wifi_forward_status(&out[1], &ip); // 0xFF when the WiFi stack is down + } + reply_ng(CMD_PM5_BWM_WIFI_PS, res, out, (res == PM3_SUCCESS) ? sizeof(out) : 0); +#else + reply_ng(CMD_PM5_BWM_WIFI_PS, PM3_ENOTIMPL, NULL, 0); #endif break; } diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 3fa2b8686..122bf4c88 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -160,7 +160,11 @@ int bwm_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len, esp_err = (int32_t)((uint32_t)pbuf[2] | ((uint32_t)pbuf[3] << 8) | ((uint32_t)pbuf[4] << 16) | ((uint32_t)pbuf[5] << 24)); } - Dbprintf("[bwm-wifi] cmd 0x%04x failed, esp_err=0x%08x", (unsigned)cmd, (unsigned)esp_err); + // Expected in places (e.g. a status query while WiFi is + // off): only worth seeing when debugging the link. + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf("[bwm-wifi] cmd 0x%04x failed, esp_err=0x%08x", (unsigned)cmd, (unsigned)esp_err); + } return PM3_EFAILED; } } @@ -317,25 +321,34 @@ int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen, uint32_t timeout_ms) { return bwm_cmd(BWM_CMD_GET_VERSION_INFO, NULL, 0, buf, buflen, timeout_ms); } -int bwm_esp_get_power_save(uint8_t *state, uint32_t timeout_ms) { +// One u8-in / u8-out ESP setting round trip: send req (NULL/0 for a GET) and +// expect a one-byte reply in *out. PM3_EFAILED if the ESP acked without a payload. +static int bwm_esp_u8_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len, uint8_t *out, uint32_t timeout_ms) { uint16_t len = 1; - int r = bwm_cmd(BWM_CMD_GET_SYS_POWER_SAVE, NULL, 0, state, &len, timeout_ms); + int r = bwm_cmd(cmd, req, req_len, out, &len, timeout_ms); if (r == PM3_SUCCESS && len < 1) { r = PM3_EFAILED; } return r; } +int bwm_esp_get_power_save(uint8_t *state, uint32_t timeout_ms) { + return bwm_esp_u8_cmd(BWM_CMD_GET_SYS_POWER_SAVE, NULL, 0, state, timeout_ms); +} + int bwm_esp_set_power_save(bool on, uint8_t *state) { // Applied at once on the ESP (no reboot) and saved to its NVS; the reply // carries the state the ESP ended up in. uint8_t v = on ? 1 : 0; - uint16_t len = 1; - int r = bwm_cmd(BWM_CMD_SET_SYS_POWER_SAVE, &v, 1, state, &len, 3000); - if (r == PM3_SUCCESS && len < 1) { - r = PM3_EFAILED; - } - return r; + return bwm_esp_u8_cmd(BWM_CMD_SET_SYS_POWER_SAVE, &v, 1, state, 3000); +} + +int bwm_esp_get_wifi_ps(uint8_t *mode) { + return bwm_esp_u8_cmd(BWM_CMD_GET_WIFI_CFG_PS_MODE, NULL, 0, mode, 3000); +} + +int bwm_esp_set_wifi_ps(uint8_t mode, uint8_t *applied) { + return bwm_esp_u8_cmd(BWM_CMD_SET_WIFI_CFG_PS_MODE, &mode, 1, applied, 3000); } int bwm_esp_get_ble_name(uint8_t *buf, uint16_t *buflen) { diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 934bb61dd..63422b996 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -93,6 +93,11 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); #define BWM_CMD_SET_SYS_POWER_SAVE 1019 // req: u8 0=off 1=on; resp: u8 applied state #define BWM_CMD_GET_SYS_POWER_SAVE 1020 // resp: u8 state int bwm_esp_get_power_save(uint8_t *state, uint32_t timeout_ms); +// ESP WiFi modem power-save type (0 none, 1 min, 2 max), persisted on the ESP. +#define BWM_CMD_SET_WIFI_CFG_PS_MODE 2052 // req: u8 mode; resp: u8 applied mode +#define BWM_CMD_GET_WIFI_CFG_PS_MODE 2053 // resp: u8 mode +int bwm_esp_get_wifi_ps(uint8_t *mode); +int bwm_esp_set_wifi_ps(uint8_t mode, uint8_t *applied); int bwm_esp_set_power_save(bool on, uint8_t *state); int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen, uint32_t timeout_ms); int bwm_esp_get_ble_name(uint8_t *buf, uint16_t *buflen); diff --git a/client/src/cmdbwm.c b/client/src/cmdbwm.c index 07714cbfc..5ebb5df5f 100644 --- a/client/src/cmdbwm.c +++ b/client/src/cmdbwm.c @@ -697,6 +697,40 @@ static int CmdBwmName(const char *Cmd) { return PM3_SUCCESS; } +// Round trip for the BWM u8 settings (`hw bwm powersave`, `hw bwm wifipower`): +// send [action][value] (value only when has_value) and wait for the ESP's answer. +// Prints the generic failure messages; on PM3_SUCCESS resp holds >= 1 byte. +static int bwm_setting_txn(uint16_t cmd, uint8_t action, uint8_t value, bool has_value, + PacketResponseNG *resp, const char *what) { + uint8_t payload[2] = { action, value }; + clearCommandBuffer(); + SendCommandNG(cmd, payload, has_value ? 2 : 1); + if (WaitForResponseTimeout(cmd, resp, 8000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a responsive BWM?)"); + return PM3_ETIMEOUT; + } + if (resp->status == PM3_ENOTIMPL) { + PrintAndLogEx(WARNING, "firmware built without BWM link support"); + return resp->status; + } + if (resp->status != PM3_SUCCESS || resp->length < 1) { + PrintAndLogEx(FAILED, "BWM did not answer the %s command (BWM firmware too old?)", what); + return (resp->status != PM3_SUCCESS) ? resp->status : PM3_EFAILED; + } + return PM3_SUCCESS; +} + +static const char *bwm_wifi_state_name(uint8_t state) { + switch (state) { + case 0: return "disconnected"; + case 1: return "connecting"; + case 2: return "connected"; + case 3: return "reconnecting"; + case 4: return "connect task stopped"; + default: return "unknown state"; + } +} + static int CmdBwmPowerSave(const char *Cmd) { // Positional sub-action (no dashes): hw bwm powersave [on | off]; no verb shows the state char verb[16] = {0}; @@ -728,27 +762,84 @@ static int CmdBwmPowerSave(const char *Cmd) { return PM3_EINVARG; } - uint8_t payload[2] = { show ? BWM_POWERSAVE_ACTION_GET : BWM_POWERSAVE_ACTION_SET, on ? 1 : 0 }; - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_POWERSAVE, payload, show ? 1 : 2); PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_PM5_BWM_POWERSAVE, &resp, 5000) == false) { - PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a responsive BWM?)"); - return PM3_ETIMEOUT; - } - if (resp.status == PM3_ENOTIMPL) { - PrintAndLogEx(WARNING, "firmware built without BWM link support"); - return resp.status; - } - if (resp.status != PM3_SUCCESS || resp.length < 1) { - PrintAndLogEx(FAILED, "BWM did not answer the power-save command (BWM firmware too old?)"); - return (resp.status != PM3_SUCCESS) ? resp.status : PM3_EFAILED; + int res = bwm_setting_txn(CMD_PM5_BWM_POWERSAVE, show ? BWM_POWERSAVE_ACTION_GET : BWM_POWERSAVE_ACTION_SET, + on ? 1 : 0, !show, &resp, "power-save"); + if (res != PM3_SUCCESS) { + return res; } bool state = (resp.data.asBytes[0] != 0); PrintAndLogEx(SUCCESS, "BWM power save..... %s", state ? _GREEN_("on") : _YELLOW_("off")); return PM3_SUCCESS; } +static const char *bwm_wifi_ps_name(uint8_t mode) { + switch (mode) { + case BWM_WIFI_PS_NONE: return "none"; + case BWM_WIFI_PS_MIN: return "min"; + case BWM_WIFI_PS_MAX: return "max"; + default: return "?"; + } +} + +static int CmdBwmWifiPower(const char *Cmd) { + // Positional sub-action (no dashes): hw bwm wifipower [off | none | min | max]; no verb shows the state + char verb[16] = {0}; + sscanf(Cmd, "%15s", verb); + int mode = -1; + if (verb[0] == 0) { + mode = -1; + } else if (strcmp(verb, "off") == 0) { + // WiFi fully off: same as `hw bwm wifi stop`. + return CmdBWMWifi("stop"); + } else if (strcmp(verb, "none") == 0) { + mode = BWM_WIFI_PS_NONE; + } else if (strcmp(verb, "min") == 0) { + mode = BWM_WIFI_PS_MIN; + } else if (strcmp(verb, "max") == 0) { + mode = BWM_WIFI_PS_MAX; + } else { + // Not a recognised sub-action: render help (also serves -h), or error on + // a stray token, then stop. + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwm wifipower", + "Show or set how much power the BWM (ESP32) spends on WiFi.\n" + _YELLOW_("off") " turns WiFi fully off (BLE-only, persisted; the default state, same as\n" + "`hw bwm wifi stop`). Bring it back with " _YELLOW_("hw bwm wifi --ssid --pwd ") ".\n" + "The other three set the modem power-save type while WiFi is up, stored on the\n" + "BWM: " _GREEN_("min") " (default) sleeps between DTIM beacons, " _YELLOW_("max") " for the whole\n" + "listen interval, " _YELLOW_("none") " never. Independent of " _YELLOW_("hw bwm powersave") ". PM5 only.", + "hw bwm wifipower --> show whether WiFi is up and the power-save type\n" + "hw bwm wifipower off --> WiFi fully off, BLE-only\n" + "hw bwm wifipower none --> modem always on while WiFi is up\n" + "hw bwm wifipower min --> ESP-IDF default (DTIM sleep)\n" + "hw bwm wifipower max --> deepest modem sleep"); + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + PrintAndLogEx(WARNING, "specify " _YELLOW_("off") ", " _YELLOW_("none") ", " _YELLOW_("min") ", " _YELLOW_("max") " or nothing to show the state"); + return PM3_EINVARG; + } + + PacketResponseNG resp; + int res = bwm_setting_txn(CMD_PM5_BWM_WIFI_PS, (mode < 0) ? BWM_WIFI_PS_ACTION_GET : BWM_WIFI_PS_ACTION_SET, + (mode < 0) ? 0 : (uint8_t)mode, mode >= 0, &resp, "WiFi power"); + if (res != PM3_SUCCESS) { + return res; + } + uint8_t wifi_state = (resp.length >= 2) ? resp.data.asBytes[1] : BWM_WIFI_STATE_OFF; + if (wifi_state == BWM_WIFI_STATE_OFF) { + PrintAndLogEx(SUCCESS, "BWM WiFi................ " _GREEN_("off") " (BLE-only)"); + } else { + PrintAndLogEx(SUCCESS, "BWM WiFi................ " _YELLOW_("on") " (%s)", bwm_wifi_state_name(wifi_state)); + } + PrintAndLogEx(SUCCESS, "BWM WiFi power save..... " _YELLOW_("%s"), bwm_wifi_ps_name(resp.data.asBytes[0])); + return PM3_SUCCESS; +} + static command_t BwmCommandTable[] = { {"help", CmdHelpBwm, AlwaysAvailable, "This help"}, {"autooff", CmdBwmAutoOff, IfBwm, "Toggle auto power-off on USB unplug"}, @@ -759,6 +850,7 @@ static command_t BwmCommandTable[] = { {"upgrade", CmdBWMUpgrade, IfBwm, "Reflash BWM (ESP32) firmware over the BWM link, no header"}, {"vchg", CmdBwmVchg, IfBwm, "Set charger charge-voltage target (default 4100 mV)"}, {"wifi", CmdBWMWifi, IfBwm, "Bring up WiFi (STA + TCP server) for a tcp: connection"}, + {"wifipower", CmdBwmWifiPower, IfBwm, "WiFi fully off, or the modem power-save type (none/min/max)"}, {NULL, NULL, NULL, NULL} }; diff --git a/doc/commands.json b/doc/commands.json index 66197e779..b3a21fc71 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -11145,6 +11145,22 @@ ], "usage": "hw bwm wifi [-h] [--ssid ] [--pwd ] [--port ] [--hostname ]" }, + "hw bwm wifipower": { + "command": "hw bwm wifipower", + "description": "Show or set how much power the BWM (ESP32) spends on WiFi. off turns WiFi fully off (BLE-only, persisted; the default state, same as `hw bwm wifi stop`). Bring it back with hw bwm wifi --ssid --pwd . The other three set the modem power-save type while WiFi is up, stored on the BWM: min (default) sleeps between DTIM beacons, max for the whole listen interval, none never. Independent of hw bwm powersave. PM5 only.", + "notes": [ + "hw bwm wifipower -> show whether WiFi is up and the power-save type", + "hw bwm wifipower off -> WiFi fully off, BLE-only", + "hw bwm wifipower none -> modem always on while WiFi is up", + "hw bwm wifipower min -> ESP-IDF default (DTIM sleep)", + "hw bwm wifipower max -> deepest modem sleep" + ], + "offline": false, + "options": [ + "-h, --help This help" + ], + "usage": "hw bwm wifipower [-h]" + }, "hw connect": { "command": "hw connect", "description": "Connects to a Proxmark3 device via specified serial port. Baudrate here is only for physical UART or UART-BT, NOT for USB-CDC or blue shark add-on", @@ -16678,8 +16694,8 @@ } }, "metadata": { - "commands_extracted": 928, + "commands_extracted": 929, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-24T08:30:13+00:00" + "extracted_on": "2026-09-24T08:30:49+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index 690ab782a..a58fe479f 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -1056,6 +1056,7 @@ Check column "offline" for their availability. |`hw bwm upgrade `|N |`Reflash BWM (ESP32) firmware over the BWM link, no header` |`hw bwm vchg `|N |`Set charger charge-voltage target (default 4100 mV)` |`hw bwm wifi `|N |`Bring up WiFi (STA + TCP server) for a tcp: connection` +|`hw bwm wifipower `|N |`WiFi fully off, or the modem power-save type (none/min/max)` ### lf diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 0de708f5f..66adbe114 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -931,6 +931,14 @@ typedef struct { #define CMD_PM5_BWM_POWERSAVE 0x0181 // payload: [action:u8][state:u8 if SET]; resp: u8 applied state #define BWM_POWERSAVE_ACTION_GET 0x00 #define BWM_POWERSAVE_ACTION_SET 0x01 +// PM5, BWM WiFi modem power-save type (persisted on the ESP). Used by `hw bwm wifipower`. +#define CMD_PM5_BWM_WIFI_PS 0x0182 // payload: [action:u8][mode:u8 if SET]; resp: [mode:u8][wifi_state:u8, 0xFF = WiFi off] +#define BWM_WIFI_PS_ACTION_GET 0x00 +#define BWM_WIFI_PS_ACTION_SET 0x01 +#define BWM_WIFI_PS_NONE 0 // modem never sleeps +#define BWM_WIFI_PS_MIN 1 // sleeps between DTIM beacons (ESP-IDF default) +#define BWM_WIFI_PS_MAX 2 // sleeps for the listen interval +#define BWM_WIFI_STATE_OFF 0xFF // wifi_state byte: WiFi stack down (BLE-only) #define BWM_OTA_ACTION_BEGIN 0x00 #define BWM_OTA_ACTION_WRITE 0x01 #define BWM_OTA_ACTION_END 0x02 From 0cd1fada0e2af09b4adffe214fc664d4251d1ddb Mon Sep 17 00:00:00 2001 From: Msprg <18015488+Msprg@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:38:43 +0200 Subject: [PATCH 5/7] pm5: auto power-off: idle timeout, unplug switch, stored on the BWM The USB-unplug power-off left two ways to drain the battery: a PM5 woken by the button and forgotten, and one left idle after the phone disconnected. Idle trigger (opt-in, PM5_AUTOOFF_IDLE_MS 0 by default so out of the box only the unplug trigger exists, as before): on battery the board powers off after the idle timeout with no interaction and no wireless client. Interaction is a received command over any transport, any button press, or a client connecting or leaving; a standalone mode or a long-running command holds the main loop, so neither can be cut short. The BWM reports its clients with the LINK_STATE broadcast (8092, companion Proxmark5_BWM_esp32 PR) which bwm_forward.c tracks; at boot the BLE half is seeded with a status query since the module only reports changes. When the timer is due but a client is still tracked, the module is asked for its BLE state (at most every 10 s), so a lost "client left" broadcast cannot pin the board on. And right before going off the module is asked once more: older module firmware never sends the broadcast and a "connected" one can get lost, and neither may power the board off under a silent BLE client. No answer means off. --unplug off: the unplug trigger always won, so "survive the unplug, go off after N idle seconds" was not reachable. It is its own setting (default on = unchanged). With it off an unplug only restarts the idle clock - from the unplug, not from the last command, or a board that sat idle on USB for an hour would still die with the cable. Unplug detection is edge-based (VUSB present at the previous poll, absent at this one) instead of "absent and seen since boot", which would fire on every poll after a tolerated unplug. VUSB is followed while the feature is switched off too, so switching it on again on battery is not taken for an unplug. Stored on the BWM: runtime-only settings reset at every boot, which is exactly when the idle trigger matters. The PM5 has no settings store, so the switch, the idle timeout and the unplug switch live in the module's host value slots (APP_CMD_SET/GET_SYS_HOST_VALUE 1021/1022, companion PR; slot 0 switch, 1 idle seconds, 2 unplug), loaded at the first auto-off poll and saved by CMD_PM5_BWM_AUTOOFF with 1 s per write, so a silent or older module still leaves the reply inside the client's 5 s wait. Without a module, or with an older one, the defaults apply and the status says "not stored". hw bwm autooff [on|off] [--idle ] [--unplug ]; no argument shows the state, one fact per line: switch, idle timeout, unplug power-off, stored on module, USB power, USB seen since boot, unplug trigger armed, tracked client, the module's BLE state, idle time. Payload [action][enabled][idle_s u32][unplug, optional] with keep sentinels; both actions are non-zero so an old firmware, which reads byte 0 as the enable flag, can only be left enabled. hw status prints the module's live BLE state and the auto power-off setting; at debug level also the tracked client, which should agree with the live state. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 1 + armsrc/appmain.c | 233 +++++++++++++++++++++++++++++++++++++------ armsrc/bwm_forward.c | 15 +++ armsrc/bwm_forward.h | 7 ++ armsrc/bwm_wifi.c | 24 +++++ armsrc/bwm_wifi.h | 12 +++ client/src/cmdbwm.c | 116 ++++++++++++++++----- doc/commands.json | 18 ++-- doc/commands.md | 2 +- include/pm3_cmd.h | 24 ++++- 10 files changed, 385 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b42d632aa..253e69430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Changed `hw bwm autooff` - `--idle ` adds an opt-in power-off after that long idle on battery (no command, button press or BLE/WiFi client), off by default; `--unplug off` makes an unplug only restart that idle clock instead of powering off at once; all stored on the BWM, `hw status` shows it (@Msprg) - Added `hw bwm wifipower` - turn the BWM WiFi fully off, or set its modem power-save type (none/min/max), persisted on the module (@Msprg) - Added `hw bwm powersave` - show/set the BWM (ESP32) power-save switch: DFS, light sleep and slow advertising after 30 s, or the stock always-on behaviour, persisted on the module (@Msprg) - Added `hw powersave` - PM5 low-power idle, on by default: between commands the core drops to 48 MHz with the PLL off, the FPGA clock stopped and the CPU halted (WFI); USB now runs crystal-less off HICK. Ported from the Fantasi firmware (@Msprg) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 8f668d2b0..e546330c3 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -93,35 +93,91 @@ #ifdef WITH_PM5_AUTOOFF -// Automatic power-off on USB unplug. When the BWM keeps the PM5 alive on battery, -// users leave it draining. This powers the board down after USB has been absent -// continuously for a grace period, using the SAME latch release as the long-press -// shutdown (Gpio_ARM_Power_ON_Low). Button power-ON is a hardware function and is -// unaffected - once powered off there is no firmware running to interfere with it. -// -// Runtime toggle (default ON) via CMD_PM5_BWM_AUTOOFF; resets to default each boot. -// Standalone / BLE-relay users who run unplugged on purpose can disable it. +// Automatic power-off, through the same latch release as the long-press shutdown +// (Gpio_ARM_Power_ON_Low); button power-ON is hardware and unaffected. Two +// triggers: a USB unplug (g_autooff_unplug) and, opt-in, g_autooff_idle_ms idle +// on battery. Idle resets on a command, a button press or a client coming/going. +// The switch and both trigger settings are set over CMD_PM5_BWM_AUTOOFF, persisted +// on the BWM and reloaded at boot; without a module the defaults hold: on, unplug +// on, idle off. #ifndef PM5_AUTOOFF_POLL_MS -#define PM5_AUTOOFF_POLL_MS 250 // how often to sample VUSB +#define PM5_AUTOOFF_POLL_MS 250 // how often to sample VUSB and the link state +#endif +#ifndef PM5_AUTOOFF_IDLE_MS +#define PM5_AUTOOFF_IDLE_MS 0 // e.g. (5 * 60 * 1000) for 5 min #endif -bool g_autooff_enabled = true; // default on; toggled by CMD_PM5_BWM_AUTOOFF +bool g_autooff_enabled = true; // default on; CMD_PM5_BWM_AUTOOFF +uint32_t g_autooff_idle_ms = PM5_AUTOOFF_IDLE_MS; // 0 = no idle power-off +bool g_autooff_unplug = true; // power off when USB is pulled static bool s_autooff_setup = false; +static uint32_t s_autooff_activity_tick = 0; // last interaction, see above +static bool s_autooff_usb_seen = false; +static bool s_autooff_link = false; +static bool s_autooff_persisted = false; // setting loaded from / saved to the module -// Auto power-off on USB unplug. CRITICAL: only powers off on a USB-present -> absent -// TRANSITION - i.e. the board was running on USB and the cable was pulled. A board that -// booted on battery (button press, no USB) must NOT auto-off, or it could never be used -// unplugged at all (and the hw bwm autooff toggle would be unreachable, since setting it -// needs a client/USB). So we require having seen USB present at least once this session -// before an absent reading triggers shutdown. +void pm5_autooff_touch(void) { + s_autooff_activity_tick = GetTickCount(); +} + +void pm5_autooff_get_status(bwm_autooff_status_t *st) { + st->enabled = g_autooff_enabled ? 1 : 0; + st->idle_s = g_autooff_idle_ms / 1000; + st->idle_now_s = GetTickCountDelta(s_autooff_activity_tick) / 1000; + st->link = s_autooff_link ? 1 : 0; + st->ble_live = 0xFF; +#ifdef WITH_BWM_FORWARD + (void)bwm_esp_get_ble_state(&st->ble_live, 800); +#endif + st->usb = Gpio_VUSB_Read() ? 1 : 0; + st->usb_seen = s_autooff_usb_seen ? 1 : 0; + st->persisted = s_autooff_persisted ? 1 : 0; + st->unplug = g_autooff_unplug ? 1 : 0; +} + +// The setting lives on the BWM host value slots: the PM5 has no store of its own. +static void pm5_autooff_save(void) { +#ifdef WITH_BWM_FORWARD + // 1 s each: an NVS write is tens of ms, and three must still fit the client's wait. + s_autooff_persisted = (bwm_esp_host_value_set(BWM_HOSTVAL_AUTOOFF_ENABLED, g_autooff_enabled ? 1 : 0, 1000) == PM3_SUCCESS) && + (bwm_esp_host_value_set(BWM_HOSTVAL_AUTOOFF_IDLE_S, g_autooff_idle_ms / 1000, 1000) == PM3_SUCCESS) && + (bwm_esp_host_value_set(BWM_HOSTVAL_AUTOOFF_UNPLUG, g_autooff_unplug ? 1 : 0, 1000) == PM3_SUCCESS); +#endif +} + +static void pm5_autooff_load(void) { +#ifdef WITH_BWM_FORWARD + uint32_t v = 0; + int r = bwm_esp_host_value_get(BWM_HOSTVAL_AUTOOFF_ENABLED, &v, 300); + if (r == PM3_SUCCESS) { + g_autooff_enabled = (v != 0); + } + if (r == PM3_SUCCESS || r == PM3_ENODATA) { + s_autooff_persisted = true; // the module keeps the setting, stored or still default + if (bwm_esp_host_value_get(BWM_HOSTVAL_AUTOOFF_IDLE_S, &v, 300) == PM3_SUCCESS) { + g_autooff_idle_ms = MIN(v, BWM_AUTOOFF_IDLE_MAX_S) * 1000; + } + if (bwm_esp_host_value_get(BWM_HOSTVAL_AUTOOFF_UNPLUG, &v, 300) == PM3_SUCCESS) { + g_autooff_unplug = (v != 0); + } + } +#endif +} + +static void pm5_power_off(void) { + LEDsoff(); + Gpio_ARM_Power_ON_Low(); + while (1); // wait for hardware power-off (button press powers back on, in hardware) +} + +// CRITICAL: the unplug trigger fires only on a USB present -> absent TRANSITION. +// A board that booted on battery must not go off at once, or it could never be +// used unplugged (and the toggle would be unreachable): it gets the idle timer. static void bwm_autooff_check(void) { static uint32_t last_tick = 0; - static bool usb_was_present = false; // have we seen USB present since boot? + static bool usb_prev = false; // VUSB at the previous poll - if (g_autooff_enabled == false) { - return; - } if ((last_tick != 0) && (GetTickCountDelta(last_tick) < PM5_AUTOOFF_POLL_MS)) { return; } @@ -129,24 +185,94 @@ static void bwm_autooff_check(void) { if (s_autooff_setup == false) { gpio_vusb_setup(); + pm5_autooff_load(); +#ifdef WITH_BWM_FORWARD + // The ESP only broadcasts link changes: ask once for the state at boot + // (a BLE client can outlive an AT32 reset). Silence = no ESP or old fw. + uint8_t st = 0; + if (bwm_esp_get_ble_state(&st, 300) == PM3_SUCCESS) { + bwm_fwd_link_seed_ble(st == BWM_BLE_STATE_CONNECTED); + } +#endif + pm5_autooff_touch(); // the boot itself counts s_autooff_setup = true; } + if (g_autooff_enabled == false) { + usb_prev = Gpio_VUSB_Read(); // keep following VUSB: a re-enable on battery is not an unplug + return; + } - // Gpio_VUSB_Read() == true means USB power present. + bool link_up = false; +#ifdef WITH_BWM_FORWARD + link_up = bwm_fwd_link_connected(); +#endif + if (link_up != s_autooff_link) { + s_autooff_link = link_up; + pm5_autooff_touch(); // a client came or went: the idle clock restarts + } + + // Gpio_VUSB_Read() == true means USB power present: never power off. if (Gpio_VUSB_Read()) { - usb_was_present = true; // latch: USB has been present this session + s_autooff_usb_seen = true; // latch: USB has been present this session + usb_prev = true; return; } - // USB absent. Only power off if USB had previously been present (a real unplug). - // If it booted on battery and never saw USB, leave it running. - if (usb_was_present == false) { - return; + // USB absent after having been present: a real unplug. Off at once, or with + // the unplug trigger off, just restart the idle clock. + if (usb_prev) { + usb_prev = false; + if (g_autooff_unplug) { + pm5_power_off(); + } + pm5_autooff_touch(); } - LEDsoff(); - Gpio_ARM_Power_ON_Low(); - while (1); // wait for hardware power-off (button press powers back on, in hardware) + // On battery: off only after g_autooff_idle_ms with no interaction and no client. + if (g_autooff_idle_ms == 0) { + return; + } + if (GetTickCountDelta(s_autooff_activity_tick) < g_autooff_idle_ms) { + return; + } + if (link_up) { +#ifdef WITH_BWM_FORWARD + static uint32_t last_verify_tick = 0; + // The timer would fire but a client is (still) tracked: the broadcast + // that reported it leaving could have been lost, so ask the module, + // at most every 10 s. Only BLE can be asked; a WiFi client is trusted. + if ((last_verify_tick == 0) || (GetTickCountDelta(last_verify_tick) >= 10000)) { + last_verify_tick = GetTickCount(); + uint8_t st = 0; + if (bwm_esp_get_ble_state(&st, 300) == PM3_SUCCESS) { + bwm_fwd_link_seed_ble(st == BWM_BLE_STATE_CONNECTED); + } + } +#endif + return; // (a corrected flag is picked up on the next pass) + } +#ifdef WITH_BWM_FORWARD + // A client the module never reported (module firmware without LINK_STATE, or + // a lost broadcast): ask once before going off; no answer means off. + uint8_t ble = 0; + if ((bwm_esp_get_ble_state(&ble, 300) == PM3_SUCCESS) && (ble == BWM_BLE_STATE_CONNECTED)) { + bwm_fwd_link_seed_ble(true); + return; + } +#endif + pm5_power_off(); +} + +static void pm5_autooff_print_status(void) { + if (g_autooff_enabled == false) { + Dbprintf(" Auto power-off...... " _YELLOW_("off")); + } else if (g_autooff_idle_ms == 0) { + Dbprintf(" Auto power-off...... " _GREEN_("on") " (%s)", g_autooff_unplug ? "USB unplug only" : "no trigger set"); + } else if (g_autooff_unplug) { + Dbprintf(" Auto power-off...... " _GREEN_("on") " (USB unplug, or %u s idle on battery)", g_autooff_idle_ms / 1000); + } else { + Dbprintf(" Auto power-off...... " _GREEN_("on") " (%u s idle on battery, also after an unplug)", g_autooff_idle_ms / 1000); + } } #endif // WITH_PM5_AUTOOFF @@ -614,6 +740,15 @@ static void SendStatus(uint32_t wait) { if (bwm_esp_get_power_save(&ps, 300) == PM3_SUCCESS) { Dbprintf(" BWM power save...... " _YELLOW_("%s"), ps ? "on" : "off"); } + uint8_t bs = 0; + if (bwm_esp_get_ble_state(&bs, 300) == PM3_SUCCESS) { + Dbprintf(" BWM BLE............. " _YELLOW_("%s"), (bs == BWM_BLE_STATE_CONNECTED) ? "client connected" : (bs == 1) ? "advertising" : "off"); + } + // Our cached copy of the ESP's LINK_STATE broadcasts (BLE or WiFi), which holds + // off the idle power-off. Debug only: it should agree with the live line above. + if (g_dbglevel >= DBG_DEBUG) { + Dbprintf(" BWM tracked client.. " _YELLOW_("%s"), bwm_fwd_link_connected() ? "connected (BLE or WiFi)" : "none"); + } } else { Dbprintf(" BWM fw version...... " _YELLOW_("%s"), "unknown"); } @@ -621,6 +756,9 @@ static void SendStatus(uint32_t wait) { #endif #ifdef PM5 pm5_power_print_status(); +#endif +#ifdef WITH_PM5_AUTOOFF + pm5_autooff_print_status(); #endif printConnSpeed(wait); DbpString(_CYAN_("Various")); @@ -4350,11 +4488,32 @@ static void PacketReceived(PacketCommandNG *packet) { break; } case CMD_PM5_BWM_AUTOOFF: { - // Toggle automatic power-off on USB unplug (runtime, default on). - // Payload: 1 byte, non-zero = enable (default), zero = disable. + // Automatic power-off switch + idle timeout. Layouts: pm3_cmd.h. #ifdef WITH_PM5_AUTOOFF - g_autooff_enabled = (packet->length >= 1) ? (packet->data.asBytes[0] != 0) : true; - reply_ng(CMD_PM5_BWM_AUTOOFF, PM3_SUCCESS, (uint8_t *)&g_autooff_enabled, 1); + if (packet->length == 1) { + // old client: enable flag only + g_autooff_enabled = (packet->data.asBytes[0] != 0); + pm5_autooff_touch(); + pm5_autooff_save(); + } else if ((packet->length >= 6) && (packet->data.asBytes[0] == BWM_AUTOOFF_ACTION_SET)) { + uint8_t en = packet->data.asBytes[1]; + uint32_t idle_s; + memcpy(&idle_s, &packet->data.asBytes[2], sizeof(idle_s)); + if (en != BWM_AUTOOFF_KEEP_U8) { + g_autooff_enabled = (en != 0); + } + if (idle_s != BWM_AUTOOFF_KEEP_U32) { + g_autooff_idle_ms = MIN(idle_s, BWM_AUTOOFF_IDLE_MAX_S) * 1000; + } + if ((packet->length >= 7) && (packet->data.asBytes[6] != BWM_AUTOOFF_KEEP_U8)) { + g_autooff_unplug = (packet->data.asBytes[6] != 0); + } + pm5_autooff_touch(); // a new timeout counts from now + pm5_autooff_save(); + } + bwm_autooff_status_t reply; + pm5_autooff_get_status(&reply); + reply_ng(CMD_PM5_BWM_AUTOOFF, PM3_SUCCESS, (uint8_t *)&reply, sizeof(reply)); #else reply_ng(CMD_PM5_BWM_AUTOOFF, PM3_ENOTIMPL, NULL, 0); #endif @@ -4499,6 +4658,9 @@ void __attribute__((noreturn)) AppMain(void) { PacketReceived(&rx); #ifdef PM5 pm5_power_unboost(); +#endif +#ifdef WITH_PM5_AUTOOFF + pm5_autooff_touch(); // a command, over any transport #endif last_activity_label = GetTickCountLabel(); last_activity_tick = GetTickCount(); @@ -4520,6 +4682,11 @@ void __attribute__((noreturn)) AppMain(void) { // Press button for one second to enter a possible standalone mode button_status = BUTTON_HELD(1000); +#ifdef WITH_PM5_AUTOOFF + if (button_status != BUTTON_NO_CLICK) { + pm5_autooff_touch(); // any press is an interaction + } +#endif if (button_status == BUTTON_HOLD) { /* * So this is the trigger to execute a standalone mod. Generic entrypoint by following the standalone/standalone.h headerfile diff --git a/armsrc/bwm_forward.c b/armsrc/bwm_forward.c index b7eaf6445..36291109e 100644 --- a/armsrc/bwm_forward.c +++ b/armsrc/bwm_forward.c @@ -63,6 +63,18 @@ static volatile bool s_baud_ack = false; // ESP is alive and responding at the baud we just switched to (verify step). static volatile bool s_getbaud_ack = false; +// Last LINK_STATE broadcast from the ESP: a client on BLE / on the TCP server. +static volatile uint8_t s_link_ble = 0; +static volatile uint8_t s_link_wifi = 0; + +bool bwm_fwd_link_connected(void) { + return (s_link_ble != 0) || (s_link_wifi != 0); +} + +void bwm_fwd_link_seed_ble(bool connected) { + s_link_ble = connected ? 1 : 0; +} + int bwm_fwd_writebuffer_sync(const uint8_t *data, size_t len) { static uint8_t frame[BWM_TX_BUFSZ]; // single-threaded bare-metal: static OK @@ -251,6 +263,9 @@ static void bwm_feed_byte(bwm_parser_t *p, uint8_t byte) { } else if ((p->is_bcast == false) && p->cmd == BWM_CMD_GET_UART_BAUD) { // SLAVE_RESP for our GET_BAUD verify probe s_getbaud_ack = true; + } else if (p->is_bcast && p->cmd == BWM_CMD_LINK_STATE && p->len >= 2) { + s_link_ble = p->payload[0]; + s_link_wifi = p->payload[1]; } } // valid non-DATA_FORWARD frames and CRC failures alike: just resync diff --git a/armsrc/bwm_forward.h b/armsrc/bwm_forward.h index 8d8e0515c..4623368d5 100644 --- a/armsrc/bwm_forward.h +++ b/armsrc/bwm_forward.h @@ -47,6 +47,7 @@ #define BWM_CMD_SEND_FORWARD_DATA 5000 // host cmd: payload -> BLE/WiFi endpoint #define BWM_CMD_DATA_FORWARD 8089 // slave bcast: payload came from endpoint +#define BWM_CMD_LINK_STATE 8092 // slave bcast: [ble u8][wifi u8], 1 = a client is connected // System command: set the ESP<->AT32 UART baud (app_com_defs.h, enum @1000). // SET is a HOST_CMD carrying u32 LE baud; the ESP replies with a SLAVE_RESP // echoing this cmd (len 0) at the OLD baud, then commits to the new baud. @@ -92,6 +93,12 @@ uint32_t bwm_read_ng(uint8_t *data, size_t len); // >0 when raw bytes are waiting on the FPC USART (gate for receive_ng()). uint16_t bwm_fwd_rxdata_available(void); +// True while the ESP reports a client on BLE or on its WiFi TCP server (the +// LINK_STATE broadcast, sent on change only). ESP firmware without it: never true. +bool bwm_fwd_link_connected(void); +// Seed the BLE half from a status query at boot (a client can outlive an AT32 reset). +void bwm_fwd_link_seed_ble(bool connected); + // Bring the ESP<->AT32 UART to `target` baud: adopt it if the ESP is already // there (its baud survives an AT32-only reset), else negotiate up via app_com // cmd 1011 and re-init UART4 to match. Returns true if the link runs at diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 122bf4c88..8b5b405d2 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -336,6 +336,30 @@ int bwm_esp_get_power_save(uint8_t *state, uint32_t timeout_ms) { return bwm_esp_u8_cmd(BWM_CMD_GET_SYS_POWER_SAVE, NULL, 0, state, timeout_ms); } +int bwm_esp_get_ble_state(uint8_t *state, uint32_t timeout_ms) { + return bwm_esp_u8_cmd(BWM_CMD_GET_BLE_SPP_STATUS, NULL, 0, state, timeout_ms); +} + +int bwm_esp_host_value_set(uint8_t id, uint32_t value, uint32_t timeout_ms) { + uint8_t req[5] = { id }; + memcpy(&req[1], &value, sizeof(value)); + return bwm_cmd(BWM_CMD_SET_SYS_HOST_VALUE, req, sizeof(req), NULL, NULL, timeout_ms); +} + +int bwm_esp_host_value_get(uint8_t id, uint32_t *value, uint32_t timeout_ms) { + uint8_t resp[5] = {0}; + uint16_t len = sizeof(resp); + int res = bwm_cmd(BWM_CMD_GET_SYS_HOST_VALUE, &id, 1, resp, &len, timeout_ms); + if (res != PM3_SUCCESS) { + return res; + } + if (len < sizeof(resp) || resp[0] == 0) { + return PM3_ENODATA; + } + memcpy(value, &resp[1], sizeof(*value)); + return PM3_SUCCESS; +} + int bwm_esp_set_power_save(bool on, uint8_t *state) { // Applied at once on the ESP (no reboot) and saved to its NVS; the reply // carries the state the ESP ended up in. diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 63422b996..eb5ea9202 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -93,6 +93,18 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); #define BWM_CMD_SET_SYS_POWER_SAVE 1019 // req: u8 0=off 1=on; resp: u8 applied state #define BWM_CMD_GET_SYS_POWER_SAVE 1020 // resp: u8 state int bwm_esp_get_power_save(uint8_t *state, uint32_t timeout_ms); +// Host settings the module stores for us (app_host NVS); the ids are ours. +#define BWM_CMD_SET_SYS_HOST_VALUE 1021 // req: [id u8][value u32]; resp: u32 +#define BWM_CMD_GET_SYS_HOST_VALUE 1022 // req: [id u8]; resp: [present u8][value u32] +#define BWM_HOSTVAL_AUTOOFF_ENABLED 0 +#define BWM_HOSTVAL_AUTOOFF_IDLE_S 1 +#define BWM_HOSTVAL_AUTOOFF_UNPLUG 2 +int bwm_esp_host_value_set(uint8_t id, uint32_t value, uint32_t timeout_ms); +// PM3_SUCCESS with the value, PM3_ENODATA if never stored, else the link error. +int bwm_esp_host_value_get(uint8_t id, uint32_t *value, uint32_t timeout_ms); +#define BWM_CMD_GET_BLE_SPP_STATUS 4020 // resp: u8 0=stopped 1=advertising 2=client connected +#define BWM_BLE_STATE_CONNECTED 2 +int bwm_esp_get_ble_state(uint8_t *state, uint32_t timeout_ms); // ESP WiFi modem power-save type (0 none, 1 min, 2 max), persisted on the ESP. #define BWM_CMD_SET_WIFI_CFG_PS_MODE 2052 // req: u8 mode; resp: u8 applied mode #define BWM_CMD_GET_WIFI_CFG_PS_MODE 2053 // resp: u8 mode diff --git a/client/src/cmdbwm.c b/client/src/cmdbwm.c index 5ebb5df5f..922b607d7 100644 --- a/client/src/cmdbwm.c +++ b/client/src/cmdbwm.c @@ -35,40 +35,77 @@ #include "util_posix.h" static int CmdBwmAutoOff(const char *Cmd) { - // Positional sub-action (no dashes): hw bwm autooff on | off + // Positional sub-action (no dashes): hw bwm autooff [on|off] [--idle ] char verb[16] = {0}; - sscanf(Cmd, "%15s", verb); + int consumed = 0; + sscanf(Cmd, "%15s%n", verb, &consumed); bool on = (strcmp(verb, "on") == 0); bool off = (strcmp(verb, "off") == 0); + const char *rest = (on || off) ? Cmd + consumed : Cmd; - if (!on && !off) { - // Not a recognised sub-action: render help (also serves -h / empty), - // or error on a stray token, then stop. - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw bwm autooff", - "Toggle automatic power-off when the PM5 is unplugged from USB (BWM only).\n" - "Default is " _GREEN_("on") ". When on, the board powers itself down ~10s after\n" - "USB is removed, so a BWM-equipped PM5 doesn't silently drain the battery.\n" - "Button power-on is unaffected. Disable for standalone/BLE use on battery.\n" - _YELLOW_("Runtime only:") " resets to on at each boot.", - "hw bwm autooff off --> disable auto power-off\n" - "hw bwm autooff on --> re-enable auto power-off"); - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - PrintAndLogEx(WARNING, "specify " _YELLOW_("on") " or " _YELLOW_("off")); + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwm autooff", + "Show or set automatic power-off (PM5 only). Two triggers:\n" + " - USB was present and the cable is pulled: off at once, so a BWM-equipped\n" + " PM5 doesn't silently drain the battery; --unplug off only restarts the\n" + " idle clock instead;\n" + " - on battery with no command, button press or BLE/WiFi client for --idle\n" + " seconds: off (0 = never, the default).\n" + "Button power-on is unaffected. Stored on the BWM; without a module, defaults.", + "hw bwm autooff --> show the current state\n" + "hw bwm autooff off --> disable both triggers\n" + "hw bwm autooff on --> re-enable\n" + "hw bwm autooff --idle 300 --> also off after 5 min idle on battery\n" + "hw bwm autooff --idle 0 --> USB-unplug trigger only (default)\n" + "hw bwm autooff --unplug off --idle 300 --> survive the unplug, off 5 min idle later"); + void *argtable[] = { + arg_param_begin, + arg_int0("i", "idle", "", "idle timeout on battery in seconds, 0 = never"), + arg_str0("u", "unplug", "", "power off when USB is pulled (default on)"), + arg_param_end + }; + CLIExecWithReturn(ctx, rest, argtable, true); + int idle = arg_get_int_def(ctx, 1, -1); + uint8_t ubuf[8] = {0}; + int ulen = 0; + int ures = CLIParamStrToBuf(arg_get_str(ctx, 2), ubuf, sizeof(ubuf) - 1, &ulen); + CLIParserFree(ctx); + if (ures) { + PrintAndLogEx(WARNING, "--unplug takes " _YELLOW_("on") " or " _YELLOW_("off")); + return PM3_EINVARG; + } + uint8_t unplug = BWM_AUTOOFF_KEEP_U8; + if (ulen) { + if (strcmp((char *)ubuf, "on") == 0) { + unplug = 1; + } else if (strcmp((char *)ubuf, "off") == 0) { + unplug = 0; + } else { + PrintAndLogEx(WARNING, "--unplug takes " _YELLOW_("on") " or " _YELLOW_("off")); + return PM3_EINVARG; + } + } + if ((idle < -1) || (idle > (int)BWM_AUTOOFF_IDLE_MAX_S)) { + PrintAndLogEx(WARNING, "idle must be 0 to %lu seconds", (unsigned long)BWM_AUTOOFF_IDLE_MAX_S); return PM3_EINVARG; } - uint8_t payload = off ? 0 : 1; // on -> 1 (enable), off -> 0 (disable) + struct { + uint8_t action; + uint8_t enabled; + uint32_t idle_s; + uint8_t unplug; + } PACKED payload = { + .action = (on || off || (idle >= 0) || ulen) ? BWM_AUTOOFF_ACTION_SET : BWM_AUTOOFF_ACTION_GET, + .enabled = on ? 1 : (off ? 0 : BWM_AUTOOFF_KEEP_U8), + .idle_s = (idle >= 0) ? (uint32_t)idle : BWM_AUTOOFF_KEEP_U32, + .unplug = unplug, + }; clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_AUTOOFF, &payload, sizeof(payload)); + SendCommandNG(CMD_PM5_BWM_AUTOOFF, (uint8_t *)&payload, sizeof(payload)); PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_PM5_BWM_AUTOOFF, &resp, 2500) == false) { + if (WaitForResponseTimeout(CMD_PM5_BWM_AUTOOFF, &resp, 5000) == false) { PrintAndLogEx(WARNING, "command timeout (is this a PM5?)"); return PM3_ETIMEOUT; } @@ -80,7 +117,34 @@ static int CmdBwmAutoOff(const char *Cmd) { PrintAndLogEx(FAILED, "failed to set auto power-off"); return resp.status; } - PrintAndLogEx(SUCCESS, "Auto power-off %s.", payload ? _GREEN_("enabled") : _YELLOW_("disabled")); + bool enabled = (resp.length >= 1) && (resp.data.asBytes[0] != 0); + PrintAndLogEx(SUCCESS, "Auto power-off....... %s", enabled ? _GREEN_("on") : _YELLOW_("off")); + if (resp.length < 5) { + // firmware before the idle timeout: it only reports the switch + return PM3_SUCCESS; + } + uint32_t idle_s; + memcpy(&idle_s, &resp.data.asBytes[1], sizeof(idle_s)); + if (idle_s) { + PrintAndLogEx(INFO, "Idle timeout......... " _YELLOW_("%u") " s", idle_s); + } else { + PrintAndLogEx(INFO, "Idle timeout......... off"); + } + if (resp.length >= sizeof(bwm_autooff_status_t)) { + const bwm_autooff_status_t *st = (const bwm_autooff_status_t *)resp.data.asBytes; + const char *live = (st->ble_live == 2) ? "client connected" : (st->ble_live == 1) ? "advertising, no client" : (st->ble_live == 0) ? "off" : "no answer"; + PrintAndLogEx(INFO, "Unplug power-off..... %s", st->unplug ? "on" : _YELLOW_("off") " (an unplug only restarts the idle clock)"); + PrintAndLogEx(INFO, "Stored on module..... %s", st->persisted ? "yes" : _YELLOW_("no")); + PrintAndLogEx(INFO, "USB power............ %s", st->usb ? "present" : "absent"); + PrintAndLogEx(INFO, "USB seen since boot.. %s", st->usb_seen ? "yes" : "no"); + PrintAndLogEx(INFO, "Unplug trigger....... %s", (enabled && st->unplug && st->usb_seen) ? _GREEN_("armed") : "not armed"); + PrintAndLogEx(INFO, "Tracked client....... %s", st->link ? "connected (BLE or WiFi)" : "none"); + PrintAndLogEx(INFO, "Module BLE state..... %s", live); + PrintAndLogEx(INFO, "Idle for............. %u s", st->idle_now_s); + if (st->persisted == 0) { + PrintAndLogEx(HINT, "no module, or one too old to store it: the defaults return at the next boot"); + } + } return PM3_SUCCESS; } @@ -842,7 +906,7 @@ static int CmdBwmWifiPower(const char *Cmd) { static command_t BwmCommandTable[] = { {"help", CmdHelpBwm, AlwaysAvailable, "This help"}, - {"autooff", CmdBwmAutoOff, IfBwm, "Toggle auto power-off on USB unplug"}, + {"autooff", CmdBwmAutoOff, IfBwm, "Show/set auto power-off (USB unplug, idle on battery)"}, {"charge", CmdBwmCharge, IfBwm, "Enable/disable battery charging (one-shot)"}, {"name", CmdBwmName, IfBwm, "Get/set the BWM BLE advertising name"}, {"powersave", CmdBwmPowerSave, IfBwm, "Show/set the BWM power-save switch (DFS, light sleep, slow adv)"}, diff --git a/doc/commands.json b/doc/commands.json index b3a21fc71..6c96bcfe3 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -11045,16 +11045,22 @@ }, "hw bwm help": { "command": "hw bwm help", - "description": "help This help --------------------------------------------------------------------------------------- hw bwm autooff available offline: no Toggle automatic power-off when the PM5 is unplugged from USB (BWM only). Default is on. When on, the board powers itself down ~10s after USB is removed, so a BWM-equipped PM5 doesn't silently drain the battery. Button power-on is unaffected. Disable for standalone/BLE use on battery. Runtime only: resets to on at each boot.", + "description": "help This help --------------------------------------------------------------------------------------- hw bwm autooff available offline: no Show or set automatic power-off (PM5 only). Two triggers: - USB was present and the cable is pulled: off at once, so a BWM-equipped PM5 doesn't silently drain the battery; --unplug off only restarts the idle clock instead; - on battery with no command, button press or BLE/WiFi client for --idle seconds: off (0 = never, the default). Button power-on is unaffected. Stored on the BWM; without a module, defaults.", "notes": [ - "hw bwm autooff off -> disable auto power-off", - "hw bwm autooff on -> re-enable auto power-off" + "hw bwm autooff -> show the current state", + "hw bwm autooff off -> disable both triggers", + "hw bwm autooff on -> re-enable", + "hw bwm autooff --idle 300 -> also off after 5 min idle on battery", + "hw bwm autooff --idle 0 -> USB-unplug trigger only (default)", + "hw bwm autooff --unplug off --idle 300 -> survive the unplug, off 5 min idle later" ], "offline": true, "options": [ - "-h, --help This help" + "-h, --help This help", + "-i, --idle idle timeout on battery in seconds, 0 = never", + "-u, --unplug power off when USB is pulled (default on)" ], - "usage": "hw bwm autooff [-h]" + "usage": "hw bwm autooff [-h] [-i ] [-u ]" }, "hw bwm name": { "command": "hw bwm name", @@ -16696,6 +16702,6 @@ "metadata": { "commands_extracted": 929, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-24T08:30:49+00:00" + "extracted_on": "2026-09-24T08:31:45+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index a58fe479f..adba1355d 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -1048,7 +1048,7 @@ Check column "offline" for their availability. |command |offline |description |------- |------- |----------- |`hw bwm help `|Y |`This help` -|`hw bwm autooff `|N |`Toggle auto power-off on USB unplug` +|`hw bwm autooff `|N |`Show/set auto power-off (USB unplug, idle on battery)` |`hw bwm charge `|N |`Enable/disable battery charging (one-shot)` |`hw bwm name `|N |`Get/set the BWM BLE advertising name` |`hw bwm powersave `|N |`Show/set the BWM power-save switch (DFS, light sleep, slow adv)` diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 66adbe114..9361a0540 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -915,8 +915,30 @@ typedef struct { #define CMD_PM5_BWM_SET_CAP 0x0179 // PM5, enable/disable BWM battery charging (AW32001E CEB). Used by `hw bwm charge`. #define CMD_PM5_BWM_CHARGE_EN 0x017A -// PM5, toggle automatic power-off on USB unplug. Used by `hw bwm autooff`. +// PM5, automatic power-off: USB unplug, and idle on battery. Used by `hw bwm autooff`. +// req: [action:u8][enabled:u8][idle_s:u32 LE][unplug:u8, optional] (0xFF / +// 0xFFFFFFFF = keep); a 1-byte req is the old enable flag. resp: +// bwm_autooff_status_t. Persisted on the BWM. #define CMD_PM5_BWM_AUTOOFF 0x017B +// Both actions are non-zero: an old firmware reads byte 0 as the enable flag, +// so a new client can only ever leave it enabled. +#define BWM_AUTOOFF_ACTION_GET 0x01 +#define BWM_AUTOOFF_ACTION_SET 0x02 +#define BWM_AUTOOFF_KEEP_U8 0xFF +#define BWM_AUTOOFF_KEEP_U32 0xFFFFFFFFUL +#define BWM_AUTOOFF_IDLE_MAX_S (7UL * 24 * 3600) // ms tick wraps at 49 days +// Reply of CMD_PM5_BWM_AUTOOFF: the setting plus the live inputs of the idle decision. +typedef struct { + uint8_t enabled; + uint32_t idle_s; // 0 = idle trigger off + uint32_t idle_now_s; // since the last interaction + uint8_t link; // tracked wireless client (LINK_STATE broadcast) + uint8_t ble_live; // module's answer right now: 0 off 1 advertising 2 connected, 0xFF no answer + uint8_t usb; // VUSB now + uint8_t usb_seen; // VUSB seen since boot (arms the unplug trigger) + uint8_t persisted; // 1 = the setting was saved on the module, 0 = module absent or too old + uint8_t unplug; // 1 = power off when USB is pulled (default); 0 = an unplug only restarts the idle clock +} PACKED bwm_autooff_status_t; #define CMD_PM5_BWM_WIFI 0x017C #define CMD_PM5_BWM_SET_VCHG 0x017D #define CMD_PM5_BWM_ESP_OTA 0x017E From 465cb47b5a414217f8412ef3348e6c220dd2020d Mon Sep 17 00:00:00 2001 From: Msprg <18015488+Msprg@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:38:43 +0200 Subject: [PATCH 6/7] pm5: BWM link flow control counts bytes in flight, sized to the ESP ring A forward frame on the BWM link is anywhere from ~30 bytes to ~2.1 KB (an NG reply at PM3_FPC_MAX_DATA), so a 16-frame ack window allowed ~33 KB in flight while the module held un-acked frames in a 4 KB UART ring: any radio stall of ~45 ms at 921600 baud overflowed it and the module dropped PM3 bytes. A frame cap tight enough for big frames stalled uploads, where the module defers small acks behind large incoming chunks (the old comment on BWM_FC_WINDOW). Count bytes instead: a FIFO of un-acked frame lengths (the module acks in order) with budget BWM_FC_BYTES = BWM_ESP_UART_RX_BUF (12288, the module's ring in the companion Proxmark5_BWM_esp32 PR) minus 1 KB slack, and a 64-frame FIFO cap for tiny frames. Change the ring size in both places; the armsrc -pedantic C99 build rejects a static assert tying them. The ack timeout no longer forgets the whole window. That made the gate open loop for the rest of the command: the acks of forgotten frames released younger ones early, so after the first eaten ack (bwm_cmd() drains the ring, #3648) the budget protected nothing. Now it forgets just enough of the oldest frames to send this one: a lost ack costs one wait and heals, and a stalled radio gets one frame per timeout on top of a full ring instead of a window. The wait is 200 ms instead of 50: a full window that stays full is the module stuck on the radio (a phone scanning stalls BLE for a few hundred ms), and every timeout leaks a frame into its ring. A CMD_ERROR broadcast naming SEND_FORWARD_DATA releases the slot like the ack (older module firmware answers that way when a WiFi forwarding type is configured and no channel takes the frame). hw status at debug level prints the window counters. Measured over BLE with the companion module firmware, which acks a frame as it leaves its ring: 86 KB/s honest against 88 KB/s open loop, and a 20 s run with the phone scanning meanwhile finishes clean. Co-Authored-By: Claude Fable 5.1 --- armsrc/appmain.c | 11 ++++++ armsrc/bwm_forward.c | 88 ++++++++++++++++++++++++++++++++++---------- armsrc/bwm_forward.h | 47 +++++++++++++++-------- 3 files changed, 111 insertions(+), 35 deletions(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index e546330c3..43cf97cf1 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -753,6 +753,17 @@ static void SendStatus(uint32_t wait) { Dbprintf(" BWM fw version...... " _YELLOW_("%s"), "unknown"); } } + if (g_dbglevel >= DBG_DEBUG) { + // Flow-control diagnostics (since boot). Read before this reply is queued. + bwm_fc_stats_t fc; + bwm_fwd_fc_stats(&fc); + Dbprintf(" BWM fwd frames...... " _YELLOW_("%u") " sent, " _YELLOW_("%u") " acked, " _YELLOW_("%u") " errored", + fc.frames, fc.acks, fc.errors); + Dbprintf(" BWM fwd gate........ " _YELLOW_("%u") " timeouts, " _YELLOW_("%u") " forgotten, peak in flight " _YELLOW_("%u") " B", + fc.timeouts, fc.forgotten, fc.bytes_max); + Dbprintf(" BWM fwd now......... " _YELLOW_("%u") " frames / " _YELLOW_("%u") " B in flight", + fc.in_flight_frames, fc.in_flight_bytes); + } #endif #ifdef PM5 pm5_power_print_status(); diff --git a/armsrc/bwm_forward.c b/armsrc/bwm_forward.c index 36291109e..b92132159 100644 --- a/armsrc/bwm_forward.c +++ b/armsrc/bwm_forward.c @@ -14,6 +14,7 @@ #include "bwm_forward.h" #include "bwm_uart_at32.h" +#include "bwm_wifi.h" // BWM_CMD_CMD_ERROR #include "pm3_cmd.h" // PM3_CMD_DATA_SIZE, PM3_* return codes #include "ticks_apis.h" // SpinDelay #include "string.h" @@ -49,11 +50,51 @@ static uint16_t bwm_crc16(const uint8_t *data, size_t len, uint16_t crc) { static void bwm_pump(void); // fwd decl: TX gate pumps RX to collect forward-frame acks -// --- Flow control (ack window) --------------------------------------------- -// s_fwd_inflight: forward frames sent but not yet acked by the ESP. Bumped on -// send, decremented when a SLAVE_RESP echoing cmd=SEND_FORWARD_DATA arrives. -// We may send while it is below BWM_FC_WINDOW; at the cap we wait for an ack. -static volatile int16_t s_fwd_inflight = 0; +// --- Flow control (byte window) -------------------------------------------- +// Forward frames sent but not yet acked by the ESP: their lengths, oldest +// first (acks arrive in order), and their sum. Bumped on every forward frame, +// popped by the parser on the ESP's SLAVE_RESP. See BWM_FC_BYTES. +static volatile uint16_t s_fc_len[BWM_FC_MAX_FRAMES]; +static volatile uint8_t s_fc_head = 0; +static volatile uint8_t s_fc_count = 0; +static volatile uint32_t s_fc_bytes = 0; +static bwm_fc_stats_t s_fc_stats; // diagnostics since boot, see bwm_fwd_fc_stats() + +static void fc_reset(void) { + s_fc_head = 0; + s_fc_count = 0; + s_fc_bytes = 0; +} + +static void fc_push(uint16_t len) { + s_fc_stats.frames++; + s_fc_len[(uint8_t)((s_fc_head + s_fc_count) % BWM_FC_MAX_FRAMES)] = len; + s_fc_count++; + s_fc_bytes += len; + if (s_fc_bytes > s_fc_stats.bytes_max) { + s_fc_stats.bytes_max = s_fc_bytes; + } +} + +// Release the oldest frame: its ack (or CMD_ERROR) came, or the gate gave up on +// it. Acks are not matched to frames, so an ack that was eaten elsewhere (bwm_cmd() +// drains the ring, see #3648) or one that comes after the gate forgot its frame +// just shifts the window by one frame: the gate is one frame too pessimistic or +// too optimistic until the window drains, and drains it does after every command. +static void fc_pop(void) { + if (s_fc_count == 0) { + return; + } + s_fc_bytes -= s_fc_len[s_fc_head]; + s_fc_head = (uint8_t)((s_fc_head + 1) % BWM_FC_MAX_FRAMES); + s_fc_count--; +} + +void bwm_fwd_fc_stats(bwm_fc_stats_t *out) { + *out = s_fc_stats; + out->in_flight_frames = s_fc_count; + out->in_flight_bytes = s_fc_bytes; +} // Set true by the parser when a SLAVE_RESP echoing BWM_CMD_SET_UART_BAUD arrives // (the ESP's ack for a baud-set request). Consumed by bwm_fwd_negotiate_baud(). @@ -83,18 +124,23 @@ int bwm_fwd_writebuffer_sync(const uint8_t *data, size_t len) { } size_t idx = 0; - // Flow control: block while the in-flight window is full, waiting for the - // ESP to ack an earlier forward frame. bwm_pump() drains the IRQ-filled RX - // ring, so acks are collected even while we sit inside a tight download loop - // (the reply_old firehose). The spin cap is a safety valve so a dead or - // disconnected ESP can't hard-hang us. A window >= 1 means single command - // replies never block - only sustained bursts hit the cap. + // Flow control: block while this frame would not fit the in-flight byte budget + // (or the length FIFO is full). bwm_pump() drains the IRQ-filled RX ring, so + // acks land even inside a tight download loop (the reply_old firehose). The + // time cap is a safety valve so a dead or disconnected ESP can't hard-hang us. { uint32_t t0 = GetTickCount(); - while (s_fwd_inflight >= BWM_FC_WINDOW) { + uint32_t need = (uint32_t)len + BWM_TX_OVERHEAD; + while ((s_fc_count >= BWM_FC_MAX_FRAMES) || (s_fc_bytes + need > BWM_FC_BYTES)) { bwm_pump(); if (GetTickCountDelta(t0) > BWM_FC_ACK_TIMEOUT_MS) { - s_fwd_inflight = 0; // best-effort: assume the pipe cleared, never hard-hang + s_fc_stats.timeouts++; + // Never hard-hang: make room for this frame only, so a stalled ESP + // gets one frame per timeout on top of its ring, not a second window. + while (s_fc_count && ((s_fc_count >= BWM_FC_MAX_FRAMES) || (s_fc_bytes + need > BWM_FC_BYTES))) { + fc_pop(); + s_fc_stats.forgotten++; + } break; } } @@ -115,7 +161,7 @@ int bwm_fwd_writebuffer_sync(const uint8_t *data, size_t len) { frame[idx++] = (uint8_t)((crc >> 8) & 0xFF); int wr = bwm_uart_write(frame, idx); - s_fwd_inflight++; // one more forward frame awaiting its ack + fc_push((uint16_t)idx); // one more forward frame awaiting its ack return wr; } @@ -253,10 +299,14 @@ static void bwm_feed_byte(bwm_parser_t *p, uint8_t byte) { fifo_push(p->payload[i]); } } else if ((p->is_bcast == false) && p->cmd == BWM_CMD_SEND_FORWARD_DATA) { - // SLAVE_RESP ack for a forward frame -> one slot freed - if (s_fwd_inflight > 0) { - s_fwd_inflight--; - } + // SLAVE_RESP ack for a forward frame -> its bytes leave the budget + s_fc_stats.acks++; + fc_pop(); + } else if (p->is_bcast && p->cmd == BWM_CMD_CMD_ERROR && p->len >= 2 && + (((uint16_t)p->payload[0] | ((uint16_t)p->payload[1] << 8)) == BWM_CMD_SEND_FORWARD_DATA)) { + // the ESP could not deliver a forward frame: this is its answer instead of the ack + s_fc_stats.errors++; + fc_pop(); } else if ((p->is_bcast == false) && p->cmd == BWM_CMD_SET_UART_BAUD) { // SLAVE_RESP ack for a baud-set request (see negotiate below) s_baud_ack = true; @@ -406,7 +456,7 @@ static void bwm_link_reset(void) { s_p.state = S_IDLE; s_fifo_head = 0; s_fifo_tail = 0; - s_fwd_inflight = 0; + fc_reset(); } bool bwm_fwd_negotiate_baud(uint32_t target) { diff --git a/armsrc/bwm_forward.h b/armsrc/bwm_forward.h index 4623368d5..6d9e01c03 100644 --- a/armsrc/bwm_forward.h +++ b/armsrc/bwm_forward.h @@ -53,27 +53,29 @@ // echoing this cmd (len 0) at the OLD baud, then commits to the new baud. #define BWM_CMD_SET_UART_BAUD 1011 #define BWM_CMD_GET_UART_BAUD 1009 // read back the ESP's live baud (negotiation verify) -// Flow control (ack window) - ARM-side only, no BWM firmware change required. -// The ESP already replies to every forward frame with a SLAVE_RESP echoing -// cmd=SEND_FORWARD_DATA, and it sends that ack only *after* app_ble_send() has -// drained the frame to BLE. So the un-acked count is a live measure of how far -// ahead of the wireless link we are. We allow up to BWM_FC_WINDOW frames in -// flight, then block for an ack before sending more - which paces us to the real -// BLE/WiFi rate and prevents the ESP UART-RX overrun that dropped bulk downloads. -// WINDOW frames must fit the ESP UART RX FIFO + wireless send buffer. -// Ceiling on un-acked forward frames. On a download the ESP acks steadily so -// this never bites; it only matters on a bidirectional UPLOAD, where the ESP -// defers the small acks while forwarding large incoming chunks. A tight value -// (4) let inflight hit the cap and stall the AT32 past the client timeout, so -// keep enough headroom to ride out delayed acks. Only ~1 response is ever -// really in flight during an upload, so this does not risk an ESP overrun. -#define BWM_FC_WINDOW 16 // max un-acked forward frames in flight +// Flow control (byte window). The ESP acks each forward frame with a SLAVE_RESP +// echoing SEND_FORWARD_DATA once it has taken the frame out of its UART ring +// (module firmware from the companion PR; older firmware acks after the radio, +// which also works, just slower), and acks come back in order, so un-acked bytes +// bound what that ring holds. The window is sized to the ring (UART_RX_BUF_SIZE +// in the BWM firmware's app_cmd_uart.h): keep BWM_ESP_UART_RX_BUF equal to it. +// Frames run ~30 B to ~2.1 KB, hence bytes. A frame the ESP cannot deliver is +// answered with CMD_ERROR instead and leaves the window the same way. When acks +// stall on a full window the gate waits BWM_FC_ACK_TIMEOUT_MS, then forgets just +// enough of the oldest frames to send this one. Acks are counted, not matched: +// a lost or late ack shifts the window by a frame until it drains at idle. +#define BWM_ESP_UART_RX_BUF 12288 +#define BWM_FC_BYTES (BWM_ESP_UART_RX_BUF - 1024) // in-flight bytes allowed; slack for the ESP's FIFO and parser lag +#define BWM_FC_MAX_FRAMES 64 // depth of the in-flight length FIFO; also caps tiny frames in flight #ifndef BWM_FC_ACK_TIMEOUT_MS // Hard cap (ms) on how long a forward write may block the main loop waiting for // acks. A spin COUNT was unbounded in wall-clock time and could hang the main // loop long enough that the client gives up and the device looks dead (USB still // enumerates on interrupts). Time-bounded => the main loop is always serviced. -#define BWM_FC_ACK_TIMEOUT_MS 50 // safety valve: proceed if acks stall, never hard-hang +// A full window that stays full this long means the ESP is stuck on the radio +// (a phone scanning stalls BLE for a few hundred ms); every timeout then leaks +// one frame into a full ring, so wait long enough for the usual stalls to pass. +#define BWM_FC_ACK_TIMEOUT_MS 200 // safety valve: proceed if acks stall, never hard-hang #endif // safety valve: give up waiting for credit (avoid hard hang) #define BWM_CRC16_POLY 0x1021 @@ -93,6 +95,19 @@ uint32_t bwm_read_ng(uint8_t *data, size_t len); // >0 when raw bytes are waiting on the FPC USART (gate for receive_ng()). uint16_t bwm_fwd_rxdata_available(void); +// Flow-control diagnostics since boot (hw status at debug level). +typedef struct { + uint32_t frames; // forward frames sent + uint32_t acks; // SLAVE_RESP acks seen + uint32_t errors; // CMD_ERROR answers seen (undeliverable frames) + uint32_t timeouts; // gate waited BWM_FC_ACK_TIMEOUT_MS on a full window + uint32_t forgotten; // frames the gate stopped counting on those timeouts + uint32_t bytes_max; // peak bytes in flight + uint8_t in_flight_frames; // now + uint32_t in_flight_bytes; // now +} bwm_fc_stats_t; +void bwm_fwd_fc_stats(bwm_fc_stats_t *out); + // True while the ESP reports a client on BLE or on its WiFi TCP server (the // LINK_STATE broadcast, sent on change only). ESP firmware without it: never true. bool bwm_fwd_link_connected(void); From 39bbf20cce3119bf26d6abaff7453a4222f0d363 Mon Sep 17 00:00:00 2001 From: Msprg <18015488+Msprg@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:42:36 +0200 Subject: [PATCH 7/7] pm5: hw bwm ble, the module's BLE settings incl. on/off and pairing The module ships with BLE open: anyone in range can connect and run commands, and there was no way to turn the radio off or to require pairing from the PM5, although the module already implements bonding with a static passkey (LE Secure Connections + MITM, SPP characteristic encrypted) behind commands nothing exposed. New CMD_PM5_BWM_BLE (0x0183): one action byte, then a full status snapshot back (bwm_ble_status_t: switch, state, bonding, passkey, TX power, address, name, bonded devices). Client menu `hw bwm ble`: status everything above, with a warning while open on | off persisted radio switch (needs the companion Proxmark5_BWM_esp32 PR; off = nothing can connect, USB/WiFi only) pairing on|off [-k] require the 6-digit passkey, set the passkey; a change of on/off restarts the stack (drops a connected client), the factory key 123456 is flagged forget -i N | --all remove bonded devices txpower -a/-c dBm advertising / connection power, -24..18 and 20 The firmware replies before restarting the stack so the reply survives when the command itself arrived over BLE. Fields a module without the switch cannot answer read 0xFF and print as unknown. A module that does not answer at all ends the status snapshot after its first query instead of running into the client's timeout. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 1 + armsrc/appmain.c | 61 +++++++++ armsrc/bwm_wifi.c | 77 +++++++++++ armsrc/bwm_wifi.h | 26 ++++ client/src/cmdbwm.c | 303 ++++++++++++++++++++++++++++++++++++++++++++ doc/commands.json | 86 ++++++++++++- doc/commands.md | 15 +++ include/pm3_cmd.h | 22 ++++ 8 files changed, 589 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 253e69430..4700683d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Added `hw bwm ble` - the BWM's BLE settings: `on`/`off` (persisted radio switch), `pairing` (require a 6-digit passkey, LE Secure Connections; the module ships open), `forget` bonded devices, `txpower`, `status` (@Msprg) - Changed `hw bwm autooff` - `--idle ` adds an opt-in power-off after that long idle on battery (no command, button press or BLE/WiFi client), off by default; `--unplug off` makes an unplug only restart that idle clock instead of powering off at once; all stored on the BWM, `hw status` shows it (@Msprg) - Added `hw bwm wifipower` - turn the BWM WiFi fully off, or set its modem power-save type (none/min/max), persisted on the module (@Msprg) - Added `hw bwm powersave` - show/set the BWM (ESP32) power-save switch: DFS, light sleep and slow advertising after 30 s, or the stock always-on behaviour, persisted on the module (@Msprg) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 43cf97cf1..5cc53c3cb 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -4466,6 +4466,67 @@ static void PacketReceived(PacketCommandNG *packet) { reply_ng(CMD_PM5_BWM_POWERSAVE, res, &state, (res == PM3_SUCCESS) ? 1 : 0); #else reply_ng(CMD_PM5_BWM_POWERSAVE, PM3_ENOTIMPL, NULL, 0); +#endif + break; + } + case CMD_PM5_BWM_BLE: { +#ifdef WITH_BWM_FORWARD + // BLE settings on the module: one action, then a full status snapshot back. + if (packet->length < 1) { + reply_ng(CMD_PM5_BWM_BLE, PM3_EINVARG, NULL, 0); + break; + } + uint8_t action = packet->data.asBytes[0]; + const uint8_t *arg = &packet->data.asBytes[1]; + uint16_t alen = packet->length - 1; + int res = PM3_SUCCESS; + bool restart = false; // a bonding change applies at the next stack start + switch (action) { + case BWM_BLE_ACTION_STATUS: + break; + case BWM_BLE_ACTION_ENABLE: { + uint8_t stored = 0; + res = (alen >= 1) ? bwm_esp_set_ble_enable(arg[0] != 0, &stored) : PM3_EINVARG; + break; + } + case BWM_BLE_ACTION_PAIRING: + res = (alen >= 1) ? bwm_esp_set_ble_bonding(arg[0] != 0) : PM3_EINVARG; + restart = (res == PM3_SUCCESS); + break; + case BWM_BLE_ACTION_KEY: + res = (alen >= 6) ? bwm_esp_set_ble_key(arg) : PM3_EINVARG; + break; + case BWM_BLE_ACTION_FORGET: + res = (alen >= 1) ? bwm_esp_ble_forget(arg[0]) : PM3_EINVARG; + break; + case BWM_BLE_ACTION_TXPOWER: + res = (alen >= 2) ? bwm_esp_set_ble_txpower(arg[0], arg[1]) : PM3_EINVARG; + break; + default: + res = PM3_EINVARG; + break; + } + if (res != PM3_SUCCESS) { + reply_ng(CMD_PM5_BWM_BLE, res, NULL, 0); + break; + } + bwm_ble_status_t st; + res = bwm_esp_ble_status(&st); + if (res != PM3_SUCCESS) { + reply_ng(CMD_PM5_BWM_BLE, res, NULL, 0); + if (restart) { + (void)bwm_esp_ble_restart(); + } + break; + } + reply_ng(CMD_PM5_BWM_BLE, PM3_SUCCESS, (uint8_t *)&st, sizeof(st)); + if (restart) { + // Reply first: over BLE the restart drops the client's own link. + SpinDelay(500); + (void)bwm_esp_ble_restart(); + } +#else + reply_ng(CMD_PM5_BWM_BLE, PM3_ENOTIMPL, NULL, 0); #endif break; } diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 8b5b405d2..ffff5921b 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -360,6 +360,83 @@ int bwm_esp_host_value_get(uint8_t id, uint32_t *value, uint32_t timeout_ms) { return PM3_SUCCESS; } +int bwm_esp_set_ble_enable(bool on, uint8_t *stored) { + uint8_t v = on ? 1 : 0; + return bwm_esp_u8_cmd(BWM_CMD_SET_BLE_ENABLE, &v, 1, stored, 3000); +} + +int bwm_esp_set_ble_bonding(bool on) { + uint8_t v = on ? 1 : 0; + return bwm_cmd(BWM_CMD_SET_BLE_BONDING_ENABLE, &v, 1, NULL, NULL, 3000); +} + +int bwm_esp_set_ble_key(const uint8_t key[6]) { + return bwm_cmd(BWM_CMD_SET_BLE_BONDING_KEY, key, 6, NULL, NULL, 3000); +} + +int bwm_esp_ble_forget(uint8_t idx) { + if (idx == 0xFF) { + return bwm_cmd(BWM_CMD_CLEAR_BLE_BONDED, NULL, 0, NULL, NULL, 3000); + } + return bwm_cmd(BWM_CMD_DEL_BLE_BONDED, &idx, 1, NULL, NULL, 3000); +} + +int bwm_esp_set_ble_txpower(uint8_t type, uint8_t level) { + uint8_t req[2] = { type, level }; + return bwm_cmd(BWM_CMD_SET_BLE_TX_POWER, req, sizeof(req), NULL, NULL, 3000); +} + +int bwm_esp_ble_restart(void) { + uint8_t state = 0; + int res = bwm_esp_get_ble_state(&state, 3000); + if (res != PM3_SUCCESS || state == 0) { + return res; // stopped (or switched off): nothing to restart + } + res = bwm_cmd(BWM_CMD_STOP_BLE_SPP, NULL, 0, NULL, NULL, 3000); + if (res != PM3_SUCCESS) { + return res; + } + return bwm_cmd(BWM_CMD_START_BLE_SPP, NULL, 0, NULL, NULL, 3000); +} + +int bwm_esp_ble_status(bwm_ble_status_t *st) { + memset(st, 0xFF, sizeof(*st)); + // The first query wakes a light-sleeping module; the rest land while it is up. + // Its timeout is also the only one we pay when no module answers at all. + int res = bwm_esp_get_ble_state(&st->state, 1500); + if (res == PM3_ETIMEOUT) { + return res; + } + (void)bwm_esp_u8_cmd(BWM_CMD_GET_BLE_ENABLE, NULL, 0, &st->enabled, 800); // older fw: CMD_ERROR or silence + (void)bwm_esp_u8_cmd(BWM_CMD_GET_BLE_BONDING_ENABLE, NULL, 0, &st->bonding, 1500); + uint16_t len = sizeof(st->passkey); + (void)bwm_cmd(BWM_CMD_GET_BLE_BONDING_KEY, NULL, 0, (uint8_t *)st->passkey, &len, 1500); + uint8_t type = 0; + (void)bwm_esp_u8_cmd(BWM_CMD_GET_BLE_TX_POWER, &type, 1, &st->txp_adv, 1500); + type = 1; + (void)bwm_esp_u8_cmd(BWM_CMD_GET_BLE_TX_POWER, &type, 1, &st->txp_conn, 1500); + len = sizeof(st->addr); + (void)bwm_cmd(BWM_CMD_GET_BLE_DEVICE_ADDR, NULL, 0, st->addr, &len, 1500); + memset(st->name, 0, sizeof(st->name)); + len = sizeof(st->name) - 1; + if (bwm_esp_get_ble_name((uint8_t *)st->name, &len) != PM3_SUCCESS) { + st->name[0] = 0; + } + st->bonded_count = 0; + uint8_t n = 0; + if (bwm_esp_u8_cmd(BWM_CMD_GET_BLE_BONDED_NUMS, NULL, 0, &n, 1500) == PM3_SUCCESS) { + for (uint8_t i = 0; i < n && i < BWM_BLE_BONDED_MAX; i++) { + len = 7; + if (bwm_cmd(BWM_CMD_GET_BLE_BONDED_ADDR, &i, 1, st->bonded[i], &len, 1500) == PM3_SUCCESS && len == 7) { + st->bonded_count++; + } else { + break; + } + } + } + return PM3_SUCCESS; +} + int bwm_esp_set_power_save(bool on, uint8_t *state) { // Applied at once on the ESP (no reboot) and saved to its NVS; the reply // carries the state the ESP ended up in. diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index eb5ea9202..3a58b605c 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -10,6 +10,7 @@ #define BWM_WIFI_H #include "common.h" +#include "pm3_cmd.h" // bwm_ble_status_t // app_com command codes (authoritative, from BWM main/app_com_defs.h) #define BWM_CMD_SET_TO_WIFI_DISABLE_MODE 2000 // no payload: tear down WiFi, back to BLE-only @@ -105,6 +106,31 @@ int bwm_esp_host_value_get(uint8_t id, uint32_t *value, uint32_t timeout_ms); #define BWM_CMD_GET_BLE_SPP_STATUS 4020 // resp: u8 0=stopped 1=advertising 2=client connected #define BWM_BLE_STATE_CONNECTED 2 int bwm_esp_get_ble_state(uint8_t *state, uint32_t timeout_ms); + +// BLE settings behind `hw bwm ble` (app_com_defs.h, BLE block @4000). +#define BWM_CMD_GET_BLE_DEVICE_ADDR 4007 // resp: 6 bytes +#define BWM_CMD_SET_BLE_BONDING_ENABLE 4008 // req: u8; takes effect at the next stack start +#define BWM_CMD_GET_BLE_BONDING_ENABLE 4009 // resp: u8 +#define BWM_CMD_SET_BLE_BONDING_KEY 4010 // req: 6 ASCII digits +#define BWM_CMD_GET_BLE_BONDING_KEY 4011 // resp: 6 ASCII digits +#define BWM_CMD_GET_BLE_BONDED_NUMS 4012 // resp: u8 count +#define BWM_CMD_GET_BLE_BONDED_ADDR 4013 // req: u8 index; resp: addr[6] + type +#define BWM_CMD_DEL_BLE_BONDED 4014 // req: u8 index +#define BWM_CMD_CLEAR_BLE_BONDED 4015 // no payload +#define BWM_CMD_SET_BLE_TX_POWER 4018 // req: [type][level] +#define BWM_CMD_GET_BLE_TX_POWER 4019 // req: u8 type; resp: u8 level +#define BWM_CMD_SET_BLE_ENABLE 4023 // req: u8 (persisted, applied at once); resp: u8 stored. Newer ESP fw only +#define BWM_CMD_GET_BLE_ENABLE 4024 // resp: u8 +int bwm_esp_set_ble_enable(bool on, uint8_t *stored); +int bwm_esp_set_ble_bonding(bool on); +int bwm_esp_set_ble_key(const uint8_t key[6]); +int bwm_esp_ble_forget(uint8_t idx); // 0xFF = all +int bwm_esp_set_ble_txpower(uint8_t type, uint8_t level); +// Stop + start the stack so a bonding change applies; no-op while it is stopped. +int bwm_esp_ble_restart(void); +// Best-effort snapshot for `hw bwm ble`; fields the module cannot answer read 0xFF. +// PM3_ETIMEOUT (nothing filled in) when the module does not answer the first query. +int bwm_esp_ble_status(bwm_ble_status_t *st); // ESP WiFi modem power-save type (0 none, 1 min, 2 max), persisted on the ESP. #define BWM_CMD_SET_WIFI_CFG_PS_MODE 2052 // req: u8 mode; resp: u8 applied mode #define BWM_CMD_GET_WIFI_CFG_PS_MODE 2053 // resp: u8 mode diff --git a/client/src/cmdbwm.c b/client/src/cmdbwm.c index 922b607d7..9057bc223 100644 --- a/client/src/cmdbwm.c +++ b/client/src/cmdbwm.c @@ -904,9 +904,312 @@ static int CmdBwmWifiPower(const char *Cmd) { return PM3_SUCCESS; } + +// --------------------------------------------------------------------------- +// hw bwm ble: the module's BLE settings (CMD_PM5_BWM_BLE) +// --------------------------------------------------------------------------- + +static const char *ble_state_str(uint8_t state) { + switch (state) { + case 0: return "off"; + case 1: return "advertising, no client"; + case 2: return "client connected"; + default: return "unknown"; + } +} + +// esp_power_level_t index <-> dBm: -24 dBm + 3 dB per step, index 15 = +20 dBm +static int ble_level_to_dbm(uint8_t level) { + if (level == 15) return 20; + return -24 + 3 * (int)level; +} + +static int ble_dbm_to_level(int dbm) { + if (dbm == 20) return 15; + if (dbm < -24 || dbm > 18 || ((dbm + 24) % 3) != 0) return -1; + return (dbm + 24) / 3; +} + +static void ble_print_status(const bwm_ble_status_t *st) { + PrintAndLogEx(INFO, "--- " _CYAN_("BWM BLE") " ---------------------------"); + if (st->enabled == 0xFF) { + PrintAndLogEx(INFO, "BLE................ %s " _YELLOW_("(module firmware without the on/off switch)"), ble_state_str(st->state)); + } else if (st->enabled == 0) { + PrintAndLogEx(INFO, "BLE................ " _YELLOW_("off") " (persisted; radio silent, USB/WiFi only)"); + } else { + PrintAndLogEx(INFO, "BLE................ " _GREEN_("on") ", %s", ble_state_str(st->state)); + } + if (st->name[0]) { + PrintAndLogEx(INFO, "Name............... " _YELLOW_("%s"), st->name); + } + if (st->addr[0] != 0xFF || st->addr[5] != 0xFF) { + PrintAndLogEx(INFO, "Address............ %02X:%02X:%02X:%02X:%02X:%02X", + st->addr[0], st->addr[1], st->addr[2], st->addr[3], st->addr[4], st->addr[5]); + } + if (st->bonding == 1) { + PrintAndLogEx(INFO, "Pairing............ " _GREEN_("required") " (LE Secure Connections, passkey " _YELLOW_("%.6s") ")", st->passkey); + if (memcmp(st->passkey, "123456", 6) == 0) { + PrintAndLogEx(WARNING, "the passkey is the factory default, change it: " _YELLOW_("hw bwm ble pairing -k <6 digits>")); + } + } else if (st->bonding == 0) { + PrintAndLogEx(INFO, "Pairing............ " _RED_("not required") " - anyone in range can connect and use the device"); + PrintAndLogEx(HINT, "enable it with " _YELLOW_("hw bwm ble pairing on")); + } + if (st->bonded_count != 0xFF) { + PrintAndLogEx(INFO, "Bonded devices..... %u%s", st->bonded_count, (st->bonded_count == BWM_BLE_BONDED_MAX) ? " (or more)" : ""); + for (uint8_t i = 0; i < st->bonded_count; i++) { + const uint8_t *a = st->bonded[i]; + PrintAndLogEx(INFO, " [%u] %02X:%02X:%02X:%02X:%02X:%02X (%s)", i, + a[0], a[1], a[2], a[3], a[4], a[5], (a[6] == 0) ? "public" : "random"); + } + } + if (st->txp_adv != 0xFF && st->txp_conn != 0xFF) { + PrintAndLogEx(INFO, "TX power........... advertising %d dBm, connection %d dBm", + ble_level_to_dbm(st->txp_adv), ble_level_to_dbm(st->txp_conn)); + } +} + +// One CMD_PM5_BWM_BLE round trip. Prints the status on success unless quiet. +static int ble_action(uint8_t action, const uint8_t *arg, size_t alen, bool quiet) { + uint8_t payload[1 + 8] = { action }; + if (alen > sizeof(payload) - 1) { + return PM3_EINVARG; + } + if (alen) { + memcpy(&payload[1], arg, alen); + } + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_BLE, payload, 1 + alen); + PacketResponseNG resp; + // Up to a dozen module round trips behind one reply, plus a light-sleep wake. + if (WaitForResponseTimeout(CMD_PM5_BWM_BLE, &resp, 8000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5? over BLE, a pairing or on/off change drops your own link)"); + return PM3_ETIMEOUT; + } + if (resp.status == PM3_ENOTIMPL) { + PrintAndLogEx(WARNING, "firmware built without the BWM link"); + return resp.status; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "module refused or did not answer (%s)", (resp.status == PM3_ETIMEOUT) ? "timeout" : "error"); + return resp.status; + } + if (resp.length < sizeof(bwm_ble_status_t)) { + PrintAndLogEx(FAILED, "short reply (%u bytes)", resp.length); + return PM3_EFAILED; + } + if (quiet == false) { + ble_print_status((const bwm_ble_status_t *)resp.data.asBytes); + } + return PM3_SUCCESS; +} + +static int CmdBwmBleStatus(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwm ble status", + "Show the module's BLE settings: on/off, name, address, pairing and passkey,\n" + "bonded devices, TX power.", + "hw bwm ble status"); + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + return ble_action(BWM_BLE_ACTION_STATUS, NULL, 0, false); +} + +static int ble_set_enable(const char *Cmd, bool on) { + CLIParserContext *ctx; + CLIParserInit(&ctx, on ? "hw bwm ble on" : "hw bwm ble off", + "Switch the module's BLE radio on or off. Persisted on the module, applied at\n" + "once. Off means nothing can connect over BLE at all; the PM5 stays reachable\n" + "over USB (and WiFi if configured). Needs a BWM firmware with the BLE switch.\n" + _YELLOW_("Over BLE, `off` cuts your own connection."), + on ? "hw bwm ble on" : "hw bwm ble off"); + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + uint8_t v = on ? 1 : 0; + return ble_action(BWM_BLE_ACTION_ENABLE, &v, 1, false); +} + +static int CmdBwmBleOn(const char *Cmd) { + return ble_set_enable(Cmd, true); +} + +static int CmdBwmBleOff(const char *Cmd) { + return ble_set_enable(Cmd, false); +} + +static int CmdBwmBlePairing(const char *Cmd) { + // Positional sub-action (no dashes): hw bwm ble pairing [on|off] [-k ] + char verb[16] = {0}; + int consumed = 0; + sscanf(Cmd, "%15s%n", verb, &consumed); + bool on = (strcmp(verb, "on") == 0); + bool off = (strcmp(verb, "off") == 0); + const char *rest = (on || off) ? Cmd + consumed : Cmd; + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwm ble pairing", + "Require pairing before a BLE client can use the device. The module ships with\n" + "pairing " _RED_("off") ": anyone in range can connect and run commands. With it on, a\n" + "client must pair once with the 6-digit passkey (LE Secure Connections, MITM)\n" + "and the data characteristic only works over the encrypted link; paired\n" + "(bonded) devices reconnect without the key until you forget them.\n" + "Both settings persist on the module. A change of on/off restarts the BLE\n" + "stack, which " _YELLOW_("drops a connected client - reconnect (and pair) afterwards.") "\n" + "The factory passkey is 123456; set your own.", + "hw bwm ble pairing on --> require pairing\n" + "hw bwm ble pairing on -k 482913 --> require pairing with this passkey\n" + "hw bwm ble pairing -k 482913 --> change the passkey only\n" + "hw bwm ble pairing off --> open access again"); + void *argtable[] = { + arg_param_begin, + arg_str0("k", "key", "<6 digits>", "passkey a client must enter"), + arg_param_end + }; + CLIExecWithReturn(ctx, rest, argtable, true); + uint8_t key[8] = {0}; + int keylen = 0; + int kres = CLIParamStrToBuf(arg_get_str(ctx, 1), key, sizeof(key) - 1, &keylen); + CLIParserFree(ctx); + if (kres) { + PrintAndLogEx(WARNING, "the passkey must be exactly 6 digits"); + return PM3_EINVARG; + } + + if (keylen) { + bool ok = (keylen == 6); + for (int i = 0; ok && i < 6; i++) { + ok = isdigit(key[i]); + } + if (ok == false) { + PrintAndLogEx(WARNING, "the passkey must be exactly 6 digits"); + return PM3_EINVARG; + } + int res = ble_action(BWM_BLE_ACTION_KEY, key, 6, (on || off)); + if (res != PM3_SUCCESS) { + return res; + } + if (!on && !off) { + PrintAndLogEx(SUCCESS, "passkey set"); + return PM3_SUCCESS; + } + } else if (!on && !off) { + PrintAndLogEx(WARNING, "specify " _YELLOW_("on") ", " _YELLOW_("off") " and/or " _YELLOW_("-k <6 digits>")); + return PM3_EINVARG; + } + uint8_t v = on ? 1 : 0; + int res = ble_action(BWM_BLE_ACTION_PAIRING, &v, 1, false); + if (res == PM3_SUCCESS) { + PrintAndLogEx(SUCCESS, "pairing %s; the BLE stack is restarting, a connected client is dropped", on ? _GREEN_("required") : _YELLOW_("not required")); + } + return res; +} + +static int CmdBwmBleForget(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwm ble forget", + "Remove bonded (paired) devices from the module, so they must pair again with\n" + "the passkey. Indexes are the ones `hw bwm ble status` lists.", + "hw bwm ble forget --all\n" + "hw bwm ble forget -i 0"); + void *argtable[] = { + arg_param_begin, + arg_int0("i", "idx", "", "index of the bonded device to remove"), + arg_lit0(NULL, "all", "remove all bonded devices"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + int idx = arg_get_int_def(ctx, 1, -1); + bool all = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + if ((all && idx >= 0) || (!all && idx < 0)) { + PrintAndLogEx(WARNING, "specify either " _YELLOW_("-i ") " or " _YELLOW_("--all")); + return PM3_EINVARG; + } + if (idx > 254) { + PrintAndLogEx(WARNING, "index out of range"); + return PM3_EINVARG; + } + uint8_t v = all ? 0xFF : (uint8_t)idx; + return ble_action(BWM_BLE_ACTION_FORGET, &v, 1, false); +} + +static int CmdBwmBleTxPower(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwm ble txpower", + "Set the module's BLE transmit power for advertising and/or connections.\n" + "Steps of 3 dB from -24 to 18 dBm, plus 20. Lower saves a little battery and\n" + "shrinks the range at which the device can be found. Persisted on the module.", + "hw bwm ble txpower -a 0 -c 0 --> 0 dBm for both\n" + "hw bwm ble txpower -a -6 --> quieter advertising only"); + void *argtable[] = { + arg_param_begin, + arg_int0("a", "adv", "", "advertising power"), + arg_int0("c", "conn", "", "connection power"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool has_adv = (arg_get_int_count(ctx, 1) > 0); + bool has_conn = (arg_get_int_count(ctx, 2) > 0); + int adv = arg_get_int_def(ctx, 1, 0); + int conn = arg_get_int_def(ctx, 2, 0); + CLIParserFree(ctx); + if ((has_adv == false) && (has_conn == false)) { + PrintAndLogEx(WARNING, "specify " _YELLOW_("-a ") " and/or " _YELLOW_("-c ")); + return PM3_EINVARG; + } + int la = has_adv ? ble_dbm_to_level(adv) : 0; + int lc = has_conn ? ble_dbm_to_level(conn) : 0; + if (la < 0 || lc < 0) { + PrintAndLogEx(WARNING, "valid values: -24, -21, ... 15, 18, 20 dBm"); + return PM3_EINVARG; + } + int res = PM3_SUCCESS; + if (has_adv) { + uint8_t v[2] = { 0, (uint8_t)la }; + res = ble_action(BWM_BLE_ACTION_TXPOWER, v, 2, has_conn); + } + if (res == PM3_SUCCESS && has_conn) { + uint8_t v[2] = { 1, (uint8_t)lc }; + res = ble_action(BWM_BLE_ACTION_TXPOWER, v, 2, false); + } + return res; +} + +static int CmdHelpBwmBle(const char *Cmd); +static command_t BwmBleCommandTable[] = { + {"help", CmdHelpBwmBle, AlwaysAvailable, "This help"}, + {"status", CmdBwmBleStatus, IfBwm, "Show BLE settings: on/off, pairing, bonded devices, TX power"}, + {"on", CmdBwmBleOn, IfBwm, "Switch BLE on (persisted)"}, + {"off", CmdBwmBleOff, IfBwm, "Switch BLE off (persisted) - nothing can connect"}, + {"pairing", CmdBwmBlePairing, IfBwm, "Require pairing with a passkey, set the passkey"}, + {"forget", CmdBwmBleForget, IfBwm, "Remove bonded devices"}, + {"txpower", CmdBwmBleTxPower, IfBwm, "Set advertising / connection TX power"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHelpBwmBle(const char *Cmd) { + (void)Cmd; + CmdsHelp(BwmBleCommandTable); + return PM3_SUCCESS; +} + +static int CmdBwmBle(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(BwmBleCommandTable, Cmd); +} + static command_t BwmCommandTable[] = { {"help", CmdHelpBwm, AlwaysAvailable, "This help"}, {"autooff", CmdBwmAutoOff, IfBwm, "Show/set auto power-off (USB unplug, idle on battery)"}, + {"ble", CmdBwmBle, IfBwm, "{ BLE: on/off, pairing, bonded devices, TX power... }"}, {"charge", CmdBwmCharge, IfBwm, "Enable/disable battery charging (one-shot)"}, {"name", CmdBwmName, IfBwm, "Get/set the BWM BLE advertising name"}, {"powersave", CmdBwmPowerSave, IfBwm, "Show/set the BWM power-save switch (DFS, light sleep, slow adv)"}, diff --git a/doc/commands.json b/doc/commands.json index 6c96bcfe3..104290840 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -11030,6 +11030,88 @@ ], "usage": "hw break [-h]" }, + "hw bwm ble forget": { + "command": "hw bwm ble forget", + "description": "Remove bonded (paired) devices from the module, so they must pair again with the passkey. Indexes are the ones `hw bwm ble status` lists.", + "notes": [ + "hw bwm ble forget --all", + "hw bwm ble forget -i 0" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-i, --idx index of the bonded device to remove", + "--all remove all bonded devices" + ], + "usage": "hw bwm ble forget [-h] [-i ] [--all]" + }, + "hw bwm ble help": { + "command": "hw bwm ble help", + "description": "help This help --------------------------------------------------------------------------------------- hw bwm ble status available offline: no Show the module's BLE settings: on/off, name, address, pairing and passkey, bonded devices, TX power.", + "notes": [ + "hw bwm ble status" + ], + "offline": true, + "options": [ + "-h, --help This help" + ], + "usage": "hw bwm ble status [-h]" + }, + "hw bwm ble off": { + "command": "hw bwm ble off", + "description": "Switch the module's BLE radio on or off. Persisted on the module, applied at once. Off means nothing can connect over BLE at all; the PM5 stays reachable over USB (and WiFi if configured). Needs a BWM firmware with the BLE switch. Over BLE, `off` cuts your own connection.", + "notes": [ + "hw bwm ble off" + ], + "offline": false, + "options": [ + "-h, --help This help" + ], + "usage": "hw bwm ble off [-h]" + }, + "hw bwm ble on": { + "command": "hw bwm ble on", + "description": "Switch the module's BLE radio on or off. Persisted on the module, applied at once. Off means nothing can connect over BLE at all; the PM5 stays reachable over USB (and WiFi if configured). Needs a BWM firmware with the BLE switch. Over BLE, `off` cuts your own connection.", + "notes": [ + "hw bwm ble on" + ], + "offline": false, + "options": [ + "-h, --help This help" + ], + "usage": "hw bwm ble on [-h]" + }, + "hw bwm ble pairing": { + "command": "hw bwm ble pairing", + "description": "Require pairing before a BLE client can use the device. The module ships with pairing off: anyone in range can connect and run commands. With it on, a client must pair once with the 6-digit passkey (LE Secure Connections, MITM) and the data characteristic only works over the encrypted link; paired (bonded) devices reconnect without the key until you forget them. Both settings persist on the module. A change of on/off restarts the BLE stack, which drops a connected client - reconnect (and pair) afterwards. The factory passkey is 123456; set your own.", + "notes": [ + "hw bwm ble pairing on -> require pairing", + "hw bwm ble pairing on -k 482913 -> require pairing with this passkey", + "hw bwm ble pairing -k 482913 -> change the passkey only", + "hw bwm ble pairing off -> open access again" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-k, --key <6 digits> passkey a client must enter" + ], + "usage": "hw bwm ble pairing [-h] [-k <6 digits>]" + }, + "hw bwm ble txpower": { + "command": "hw bwm ble txpower", + "description": "Set the module's BLE transmit power for advertising and/or connections. Steps of 3 dB from -24 to 18 dBm, plus 20. Lower saves a little battery and shrinks the range at which the device can be found. Persisted on the module.", + "notes": [ + "hw bwm ble txpower -a 0 -c 0 -> 0 dBm for both", + "hw bwm ble txpower -a -6 -> quieter advertising only" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-a, --adv advertising power", + "-c, --conn connection power" + ], + "usage": "hw bwm ble txpower [-h] [-a ] [-c ]" + }, "hw bwm charge": { "command": "hw bwm charge", "description": "Enable or disable BWM battery charging by clearing/setting the AW32001E charge-enable bit (CEB, REG01[3]). PM5 only. One-shot: the charger watchdog reverts this after ~160 s unless serviced, so charging may stop on its own. Use to nudge a top-up.", @@ -16700,8 +16782,8 @@ } }, "metadata": { - "commands_extracted": 929, + "commands_extracted": 935, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-24T08:31:45+00:00" + "extracted_on": "2026-09-24T08:32:19+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index adba1355d..5fb6c6f9d 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -1059,6 +1059,21 @@ Check column "offline" for their availability. |`hw bwm wifipower `|N |`WiFi fully off, or the modem power-save type (none/min/max)` +### hw bwm ble + + { BLE: on/off, pairing, bonded devices, TX power... } + +|command |offline |description +|------- |------- |----------- +|`hw bwm ble help `|Y |`This help` +|`hw bwm ble status `|N |`Show BLE settings: on/off, pairing, bonded devices, TX power` +|`hw bwm ble on `|N |`Switch BLE on (persisted)` +|`hw bwm ble off `|N |`Switch BLE off (persisted) - nothing can connect` +|`hw bwm ble pairing `|N |`Require pairing with a passkey, set the passkey` +|`hw bwm ble forget `|N |`Remove bonded devices` +|`hw bwm ble txpower `|N |`Set advertising / connection TX power` + + ### lf { Low frequency commands... } diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 9361a0540..dee23a9ff 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -961,6 +961,28 @@ typedef struct { #define BWM_WIFI_PS_MIN 1 // sleeps between DTIM beacons (ESP-IDF default) #define BWM_WIFI_PS_MAX 2 // sleeps for the listen interval #define BWM_WIFI_STATE_OFF 0xFF // wifi_state byte: WiFi stack down (BLE-only) +// PM5, BWM BLE settings (`hw bwm ble`). req: [action:u8][args]; resp: bwm_ble_status_t +// on success. Unknown fields (module firmware without the command) read 0xFF. +#define CMD_PM5_BWM_BLE 0x0183 +#define BWM_BLE_ACTION_STATUS 0x00 +#define BWM_BLE_ACTION_ENABLE 0x01 // [on:u8], persisted on the module +#define BWM_BLE_ACTION_PAIRING 0x02 // [on:u8], persisted; restarts the stack (drops a BLE client) +#define BWM_BLE_ACTION_KEY 0x03 // [6 ASCII digits], persisted +#define BWM_BLE_ACTION_FORGET 0x04 // [idx:u8, 0xFF = all bonded devices] +#define BWM_BLE_ACTION_TXPOWER 0x05 // [type:u8 0 adv 1 conn][level:u8 esp_power_level_t 0..15, -24 dBm + 3/step, 15 = 20 dBm] +#define BWM_BLE_BONDED_MAX 8 +typedef struct { + uint8_t enabled; // persisted switch + uint8_t state; // 0 off, 1 advertising, 2 client connected + uint8_t bonding; // 1 = pairing with passkey required, SPP characteristic encrypted + char passkey[6]; // ASCII digits, not NUL-terminated + uint8_t txp_adv; // esp_power_level_t index + uint8_t txp_conn; + uint8_t addr[6]; // as the module reports it (big-endian display order) + char name[16]; // NUL-padded + uint8_t bonded_count; + uint8_t bonded[BWM_BLE_BONDED_MAX][7]; // addr[6] + type, first bonded_count valid +} PACKED bwm_ble_status_t; #define BWM_OTA_ACTION_BEGIN 0x00 #define BWM_OTA_ACTION_WRITE 0x01 #define BWM_OTA_ACTION_END 0x02