diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e242c24..44f0c9c16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ 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) +- 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 `lf t55xx dump -p` - reuses the password already confirmed by `lf t55xx detect` (@iceman1001) - Fixed `lf t55xx p1detect`, `lf search` - T5577 clones with a non-Atmel manufacturer byte now recognised (@iceman1001) - Added `PLATFORM_EXTRAS=CEP` on PM5 - wires up the Flipper Zero link on the Type-C extended port (handshake + NG frames over SPI, independent transport flag so it can run alongside BWM); RF-dependent functionality still needs the FPGA side, tracked in #3667 (@TomHarkness) 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 4ba695537..7778cd88d 100644 --- a/armsrc/Makefile +++ b/armsrc/Makefile @@ -144,6 +144,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 @@ -211,6 +218,7 @@ THUMBSRC = start.c \ $(SRC_CEP) \ $(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 37226bdc0..1678a10cb 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -92,38 +92,95 @@ #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 -// 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; } @@ -131,24 +188,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 @@ -599,15 +726,53 @@ 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"); + } + 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"); } } + 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(); +#endif +#ifdef WITH_PM5_AUTOOFF + pm5_autooff_print_status(); #endif #ifdef WITH_CEP Dbprintf(" CEP (Flipper) link.. " _YELLOW_("%s"), cep_is_active() ? "attached" : "not attached"); @@ -4216,7 +4381,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; @@ -4300,22 +4465,164 @@ 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; + } + 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; + } + 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; } 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 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; @@ -4400,12 +4707,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 + #ifdef WITH_CEP cep_init(); // USART1/SPI1 + CC-controller I2C bring-up; AFTER usb_enable() #endif @@ -4440,7 +4754,16 @@ 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 +#ifdef WITH_PM5_AUTOOFF + pm5_autooff_touch(); // a command, over any transport +#endif last_activity_label = GetTickCountLabel(); last_activity_tick = GetTickCount(); } else if (ret != PM3_ENODATA) { @@ -4461,6 +4784,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 @@ -4529,5 +4857,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_forward.c b/armsrc/bwm_forward.c index b7eaf6445..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(). @@ -63,6 +104,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 @@ -71,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; } } @@ -103,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; } @@ -241,16 +299,23 @@ 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; } 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 @@ -391,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 8d8e0515c..6d9e01c03 100644 --- a/armsrc/bwm_forward.h +++ b/armsrc/bwm_forward.h @@ -47,32 +47,35 @@ #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. #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 @@ -92,6 +95,25 @@ 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); +// 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_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/bwm_wifi.c b/armsrc/bwm_wifi.c index 81d74224f..ffff5921b 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; } } @@ -313,8 +317,139 @@ 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); +} + +// 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(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_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_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. + uint8_t v = on ? 1 : 0; + 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 9d7b08a1c..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 @@ -88,7 +89,55 @@ 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); +// 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); + +// 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 +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); 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/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/cmdbwm.c b/client/src/cmdbwm.c index ebbcdfca5..9057bc223 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; } @@ -697,15 +761,463 @@ 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}; + 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; + } + + PacketResponseNG resp; + 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; +} + + +// --------------------------------------------------------------------------- +// 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, IfBwm, "This help"}, - {"autooff", CmdBwmAutoOff, IfBwm, "Toggle auto power-off on USB unplug"}, + {"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)"}, {"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)"}, {"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/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 f22d25c99..00d9339b0 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", @@ -11024,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.", @@ -11039,16 +11127,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", @@ -11064,6 +11158,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.", @@ -11125,6 +11233,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", @@ -11255,6 +11379,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", @@ -16645,8 +16782,8 @@ } }, "metadata": { - "commands_extracted": 926, + "commands_extracted": 935, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-22T14:39:12+00:00" + "extracted_on": "2026-09-24T08:32:19+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index 824aa49f4..5fb6c6f9d 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)` @@ -1047,13 +1048,30 @@ 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)` |`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)` |`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)` + + +### 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 diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index f02bad7b1..33f0564ce 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -919,8 +919,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 @@ -928,6 +950,43 @@ 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 +// 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 +// 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) +// 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