Merge pull request #3626 from Msprg/pm5-idle-lowpower

PM5: idle power save, BWM power/WiFi/BLE settings, idle auto power-off, byte-window flow control
This commit is contained in:
Iceman
2026-09-26 21:51:43 +07:00
committed by GitHub
22 changed files with 1894 additions and 137 deletions
+5
View File
@@ -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 <sec>` 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)
+1
View File
@@ -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 ()
+8
View File
@@ -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) \
+369 -37
View File
@@ -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
}
}
+16 -7
View File
@@ -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
+84 -19
View File
@@ -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) {
+38 -16
View File
@@ -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
+40 -1
View File
@@ -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;
}
+10
View File
@@ -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);
+138 -3
View File
@@ -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) {
+50 -1
View File
@@ -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);
+241
View File
@@ -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);
}
+46
View File
@@ -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__
+3
View File
@@ -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
+539 -27
View File
@@ -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 <sec>]
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", "<sec>", "idle timeout on battery in seconds, 0 = never"),
arg_str0("u", "unplug", "<on|off>", "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 <ssid> --pwd <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 <digits>]
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", "<dec>", "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 <idx>") " 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", "<dBm>", "advertising power"),
arg_int0("c", "conn", "<dBm>", "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 <dBm>") " and/or " _YELLOW_("-c <dBm>"));
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}
};
+45
View File
@@ -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)"},
+7
View File
@@ -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
//-----------------------------------------------------------------------------
+13
View File
@@ -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);
}
+11 -10
View File
@@ -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
+151 -14
View File
@@ -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 <n> abort online phase if total candidates exceed this limit",
"--parity-mask <hex> parity filter mask 1..F (default 1 = vetted staticnested_1nt behavior)"
"--parity-mask <hex> parity filter mask 1..F (default 1 = vetted staticnested_1nt behavior)",
"--suffix <txt> 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 <n>] [--parity-mask <hex>]"
"usage": "hf mf sen [-h] [--keep-nonces] [--no-oob] [--reader] [--offline-only] [--online-confirm] [--max-online-candidates <n>] [--parity-mask <hex>] [--suffix <txt>] [--ns]"
},
"hf mf setmod": {
"command": "hf mf setmod",
@@ -8699,7 +8703,7 @@
"--dfname <hex> Certificate application DF name (1-16 bytes)",
"--fid <hex> Certificate file ID (1 byte)",
"--no-auth Read certificate file without authentication",
"--ca <name|cert|pubkey|path|skip> CA input, or `skip` to skip certificate signature validation. Repeat --ca for multiple entries",
"--ca <name|cert|pubkey|path|skip> CA input, or `skip` to skip validation",
"--keyaid <hex> Key application ID (default: cert app)",
"--keyisoid <hex> Key application ISO DF ID (2 bytes)",
"--keydfname <hex> 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 <fn> 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 <fn>"
"usage": "hf mfdes view [-hv] [-f <fn>] [--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 <dec> index of the bonded device to remove",
"--all remove all bonded devices"
],
"usage": "hw bwm ble forget [-h] [-i <dec>] [--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 <dBm> advertising power",
"-c, --conn <dBm> connection power"
],
"usage": "hw bwm ble txpower [-h] [-a <dBm>] [-c <dBm>]"
},
"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 <sec> idle timeout on battery in seconds, 0 = never",
"-u, --unplug <on|off> power off when USB is pulled (default on)"
],
"usage": "hw bwm autooff [-h]"
"usage": "hw bwm autooff [-h] [-i <sec>] [-u <on|off>]"
},
"hw bwm name": {
"command": "hw bwm name",
@@ -11064,6 +11158,20 @@
],
"usage": "hw bwm name [-h] [--set <name>]"
},
"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 <ssid>] [--pwd <pwd>] [--port <dec>] [--hostname <name>]"
},
"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 <ssid> --pwd <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 <dec>]"
},
"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"
}
}
+19 -1
View File
@@ -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
+60 -1
View File
@@ -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