This commit is contained in:
Antiklesys
2026-09-05 17:30:52 +08:00
7 changed files with 693 additions and 142 deletions
+78
View File
@@ -589,6 +589,18 @@ static void SendStatus(uint32_t wait) {
#endif
#ifdef WITH_BWM_FORWARD
Dbprintf(" BWM link baud....... " _YELLOW_("%u") " bps", bwm_uart_get_baud());
{
// 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).
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) {
bwm_ver[bwm_ver_len] = 0x00;
Dbprintf(" BWM fw version...... " _YELLOW_("%s"), bwm_ver);
} else {
Dbprintf(" BWM fw version...... " _YELLOW_("%s"), "unknown");
}
}
#endif
printConnSpeed(wait);
DbpString(_CYAN_("Various"));
@@ -3940,6 +3952,72 @@ static void PacketReceived(PacketCommandNG *packet) {
}
#else
reply_ng(CMD_PM5_BWM_WIFI, PM3_ENOTIMPL, NULL, 0);
#endif
break;
}
case CMD_PM5_BWM_ESP_OTA: {
// ESP32 OTA over the existing BWM app_com link (see bwm_wifi.c).
// Payload: [action:u8] + action-specific data.
// BEGIN: u32 LE total image size
// WRITE: firmware chunk
// END: (no payload) finalize + set boot partition
#if defined(WITH_BWM_FORWARD)
uint8_t action = packet->data.asBytes[0];
int res = PM3_EINVARG;
bool replied = false;
switch (action) {
case BWM_OTA_ACTION_VERSION: {
// Return the running ESP firmware version string so the client
// can confirm an update actually took (esp. when the finalize
// 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);
reply_ng(CMD_PM5_BWM_ESP_OTA, res, ver, (res == PM3_SUCCESS) ? vlen : 0);
replied = true;
break;
}
case BWM_OTA_ACTION_BEGIN: {
uint32_t total_size = 0;
if (packet->length >= 5) {
memcpy(&total_size, packet->data.asBytes + 1, sizeof(total_size));
}
res = bwm_esp_ota_begin(total_size);
break;
}
case BWM_OTA_ACTION_WRITE:
// Defensive: bwm_esp_ota_write()'s app_com frame buffer caps a
// single chunk at BWM_OTA_CHUNK_MAX (see bwm_wifi.c: bwm_cmd()).
// The client is expected to respect this, but fail explicitly
// here rather than let bwm_cmd() silently overflow/reject.
if (packet->length - 1 > BWM_OTA_CHUNK_MAX) {
res = PM3_EOVFLOW;
} else {
res = bwm_esp_ota_write(packet->data.asBytes + 1, packet->length - 1);
}
break;
case BWM_OTA_ACTION_END:
res = bwm_esp_ota_end();
if (res == PM3_SUCCESS) {
// Finalize succeeded and the new partition is now marked
// bootable; the ESP won't switch to it on its own, so
// kick the reboot here (DEV.md 12.8). Best-effort: don't
// fail the whole OTA over a lost reboot ack.
(void)bwm_esp_reboot();
}
break;
case BWM_OTA_ACTION_ABORT:
res = bwm_esp_ota_abort();
break;
default:
res = PM3_EINVARG;
break;
}
if (replied == false) {
reply_ng(CMD_PM5_BWM_ESP_OTA, res, NULL, 0);
}
#else
reply_ng(CMD_PM5_BWM_ESP_OTA, PM3_ENOTIMPL, NULL, 0);
#endif
break;
}
+10
View File
@@ -150,6 +150,16 @@ int bwm_uart_write(const uint8_t *data, size_t len) {
}
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
// is why a stalled OTA "recovers on re-run" but mostly fails in a session.
// Clear it here so reception resumes on its own. ROERR clears by reading STS
// then DT; we only do that when the flag is actually set - the DMA is stalled
// then, so the byte we consume is the already-lost overrun byte.
if (usart_flag_get(BWM_UART, USART_ROERR_FLAG) != RESET) {
(void)BWM_UART->sts;
(void)BWM_UART->dt;
}
return (uint16_t)((bwm_uart_rx_head() - s_rx_tail) & (BWM_RX_RING_SZ - 1));
}
+108 -7
View File
@@ -24,9 +24,30 @@ static uint16_t wifi_crc16(const uint8_t *d, size_t n, uint16_t crc) {
// Runs only during setup, so it fully owns the RX stream here.
typedef enum { W_H1, W_H2, W_CL, W_CH, W_LL, W_LH, W_PL, W_KL, W_KH } wstate_t;
#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
// Drop whatever is already sitting in the UART RX ring so a late ACK from a
// timed-out command cannot be consumed as the next command's response (same
// cmd code, e.g. a slow OTA_BEGIN ack arriving after we already gave up).
static void bwm_cmd_drain_rx(void) {
uint8_t buf[64];
uint32_t t0 = GetTickCount();
while (bwm_uart_rx_available()) {
uint16_t avail = bwm_uart_rx_available();
(void)bwm_uart_read(buf, (uint32_t)MIN(avail, (uint16_t)sizeof(buf)));
if (GetTickCountDelta(t0) > 50) {
break;
}
}
}
int bwm_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len,
uint8_t *resp, uint16_t *resp_len, uint32_t timeout_ms) {
bwm_cmd_drain_rx();
// ---- build + send HOST_CMD frame ----
uint8_t frame[8 + 256];
if (req_len > sizeof(frame) - 8) {
@@ -63,6 +84,7 @@ int bwm_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len,
uint8_t buf[64];
uint16_t avail = bwm_uart_rx_available();
if (avail == 0) {
SpinDelay(1);
continue;
}
uint32_t n = bwm_uart_read(buf, (uint32_t)MIN(avail, (uint16_t)sizeof(buf)));
@@ -130,13 +152,18 @@ int bwm_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len,
}
return PM3_SUCCESS;
}
// a CMD_ERROR broadcast referencing our cmd -> failure.
// (payload carries the failing cmd; treat any error report
// that arrives while we wait as a failure of this step.)
if (!is_resp && rcmd == BWM_CMD_CMD_ERROR) {
return PM3_EFAILED;
if (!is_resp && rcmd == BWM_CMD_CMD_ERROR && rlen >= 2) {
uint16_t failed_cmd = (uint16_t)pbuf[0] | ((uint16_t)pbuf[1] << 8);
if (failed_cmd == cmd) {
int32_t esp_err = 0;
if (rlen >= 6) {
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);
return PM3_EFAILED;
}
}
// otherwise: some other frame (e.g. a stray broadcast) - ignore
}
st = W_H1;
break;
@@ -256,7 +283,12 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out) {
}
int bwm_wifi_forward_down(void) {
// Once WiFi is gone the status query returns a non-timeout error.
// The ESP tears down the STA + TCP server and persists to NVS BEFORE it acks,
// and its command task blocks during the teardown - so that ack can outlast
// any timeout or be dropped. A plain ack-wait therefore reports failure on a
// disable that actually worked. Instead: fire the disable, then CONFIRM by
// polling the connect status. Once WiFi is gone the status query returns a
// non-timeout error (the subsystem is down) - that is our proof of success.
(void)bwm_cmd(BWM_CMD_SET_TO_WIFI_DISABLE_MODE, NULL, 0, NULL, NULL, 3000);
uint32_t t0 = GetTickCount();
@@ -274,3 +306,72 @@ int bwm_wifi_forward_down(void) {
}
return PM3_ETIMEOUT;
}
// ---------------------------------------------------------------------------
// ESP OTA forwarders. Each maps a host request to one ESP OTA app_com command.
// esp_ota_begin erases the target partition and esp_ota_end finalizes + sets the
// 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_ota_begin(uint32_t total_size) {
// SILENCE ESP log forwarding for the OTA. With it on, the ESP's background
// log broadcasts (WiFi/coex/BLE) interleave with the per-chunk acks across
// the thousands of round-trips; one landing in an ack window drops that
// chunk's reply -> random PM3_ETIMEOUT. Restored in bwm_esp_ota_end().
// Best-effort: ignore failure.
uint8_t off = 0;
(void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &off, 1, NULL, NULL, 500);
// Stop BLE so NimBLE is not hitting flash (NVS / auto-suspend) while we
// erase and program the OTA slot. WiFi stays as-is: tearing it down here
// is slow and the USB OTA path does not need it off.
(void)bwm_cmd(BWM_CMD_STOP_BLE_SPP, NULL, 0, NULL, NULL, 2000);
// Do NOT retune the UART here. Version already proved the current baud
// works; dropping 921600 -> 460800/460000 can leave the ESP on one rate
// and the AT32 on the other, after which every OTA_BEGIN times out.
uint8_t p[4] = {
(uint8_t)(total_size & 0xFF), (uint8_t)((total_size >> 8) & 0xFF),
(uint8_t)((total_size >> 16) & 0xFF), (uint8_t)((total_size >> 24) & 0xFF)
};
return bwm_cmd(BWM_CMD_OTA_BEGIN, p, sizeof(p), NULL, NULL, BWM_OTA_BEGIN_TIMEOUT_MS);
}
int bwm_esp_ota_write(const uint8_t *data, uint16_t len) {
// Documented OTA has no resume/offset: the payload is the raw chunk, written
// sequentially. A dropped chunk cannot be re-sent (it would double-write and
// fail the OTA_END size check) - recovery is to restart the whole OTA, which
// the client does. Keep a generous timeout so a slow flash write is not
// mistaken for a drop.
return bwm_cmd(BWM_CMD_OTA_WRITE, data, len, NULL, NULL, BWM_OTA_WRITE_TIMEOUT_MS);
}
int bwm_esp_ota_end(void) {
int r = bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000);
uint8_t on = 1;
(void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &on, 1, NULL, NULL, 500);
(void)bwm_cmd(BWM_CMD_START_BLE_SPP, NULL, 0, NULL, NULL, 2000);
return r;
}
int bwm_esp_ota_abort(void) {
// Restore logs + BLE. Leave the UART baud alone (see begin). The ESP's
// incomplete OTA state is discarded by the next BEGIN.
uint8_t on = 1;
(void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &on, 1, NULL, NULL, 500);
(void)bwm_cmd(BWM_CMD_START_BLE_SPP, NULL, 0, NULL, NULL, 2000);
return PM3_SUCCESS;
}
int bwm_esp_reboot(void) {
// The ESP acks then calls esp_restart() - the ack itself may or may not
// make it back before the UART goes away, so treat a timeout here as a
// benign race, not a failure: the reboot was still requested.
int r = bwm_cmd(BWM_CMD_REBOOT, NULL, 0, NULL, NULL, 3000);
return (r == PM3_ETIMEOUT) ? PM3_SUCCESS : r;
}
+28
View File
@@ -35,6 +35,8 @@
#define BWM_WIFI_FORWARD_TCP_SERVER 0 // wifi_forward_type_t::WIFI_FORWARD_TCP_SERVER
#define BWM_CMD_CMD_ERROR 8091 // slave bcast: command error report
#define BWM_CMD_LOG_FORWARD_ENABLE 1014 // payload: u8 (0=stop, non-zero=start)
#define BWM_CMD_LOG_MESSAGE 8090 // slave bcast: ESP_LOGx output (string)
// Low-level: send one app_com HOST_CMD and wait for its SLAVE_RESP.
// resp/resp_len may be NULL if no response payload is expected.
@@ -65,4 +67,30 @@ int bwm_wifi_forward_down(void);
#define BWM_WIFI_STATE_OFF 0xFF
int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out);
// ESP OTA over the BWM UART link (no header/soldering): drives the ESP's own
// OTA commands to reflash a *working* BWM to a new ESP image.
#define BWM_CMD_OTA_BEGIN 1800 // req: u32 total size
#define BWM_CMD_OTA_WRITE 1801 // req: firmware chunk
#define BWM_CMD_OTA_END 1802 // no payload: finalize + set boot partition
// After a working OTA_END, the ESP has marked the new partition bootable but
// does not reboot on its own - REBOOT must be sent explicitly (DEV.md 12.8).
#define BWM_CMD_REBOOT 1803
// BEGIN erases (or finishes aborting) an OTA slot. A 1.8 MB erase on ESP32-C2
// commonly takes 20-40 s, so this must be well above the old 15 s race.
#ifndef BWM_OTA_BEGIN_TIMEOUT_MS
#define BWM_OTA_BEGIN_TIMEOUT_MS 60000
#endif
#ifndef BWM_OTA_WRITE_TIMEOUT_MS
#define BWM_OTA_WRITE_TIMEOUT_MS 20000
#endif
#define BWM_CMD_GET_VERSION_INFO 1000 // resp: running firmware version string
#define BWM_CMD_STOP_BLE_SPP 4022 // no payload: stop BLE during OTA (flash contention)
#define BWM_CMD_START_BLE_SPP 4021 // no payload: restore BLE after OTA
int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen);
int bwm_esp_ota_begin(uint32_t total_size);
int bwm_esp_ota_write(const uint8_t *data, uint16_t len);
int bwm_esp_ota_end(void);
int bwm_esp_reboot(void);
int bwm_esp_ota_abort(void);
#endif
+382 -102
View File
@@ -1508,32 +1508,35 @@ static int CmdTearoff(const char *Cmd) {
}
static int CmdBwmAutoOff(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hw bwmautooff",
"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 bwmautooff --off --> disable auto power-off\n"
"hw bwmautooff --on --> re-enable auto power-off");
// Positional sub-action (no dashes): hw bwm autooff on | off
char verb[16] = {0};
sscanf(Cmd, "%15s", verb);
bool on = (strcmp(verb, "on") == 0);
bool off = (strcmp(verb, "off") == 0);
void *argtable[] = {
arg_param_begin,
arg_lit0(NULL, "on", "enable auto power-off (default)"),
arg_lit0(NULL, "off", "disable auto power-off"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, true);
bool on = arg_get_lit(ctx, 1);
bool off = arg_get_lit(ctx, 2);
CLIParserFree(ctx);
if (on && off) {
PrintAndLogEx(WARNING, "pick one of --on / --off");
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"));
return PM3_EINVARG;
}
uint8_t payload = off ? 0 : 1; // default (neither flag) = enable
uint8_t payload = off ? 0 : 1; // on -> 1 (enable), off -> 0 (disable)
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_AUTOOFF, &payload, sizeof(payload));
@@ -1555,48 +1558,15 @@ static int CmdBwmAutoOff(const char *Cmd) {
}
static int CmdBWMWifi(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hw bwmwifi",
"Bring up the BWM in STA + TCP-server mode: join a WiFi network and\n"
"start a TCP server so the client can connect over WiFi. PM5 only.",
"hw bwmwifi --ssid Home --pwd secret --> port 7777\n"
"hw bwmwifi --ssid Home --pwd secret --port 9000\n"
"hw bwmwifi --status --> show connection state + IP");
// Sub-actions that carry no other arguments are positional keywords now:
// hw bwm wifi status (was --status)
// hw bwm wifi stop (was --stop)
// Bringing WiFi up still takes value flags, so it stays the default
// (no-keyword) form: hw bwm wifi --ssid <ssid> --pwd <pwd> [--port <n>]
char verb[16] = {0};
sscanf(Cmd, "%15s", verb);
void *argtable[] = {
arg_param_begin,
arg_str0(NULL, "ssid", "<ssid>", "WiFi SSID to join (omit with --stop)"),
arg_str0(NULL, "pwd", "<pwd>", "WiFi password (omit for open network)"),
arg_int0(NULL, "port", "<dec>", "TCP server listen port (default 7777)"),
arg_str0(NULL, "hostname", "<name>", "DHCP hostname (default Proxmark5)"),
arg_lit0(NULL, "stop", "tear down WiFi and return to BLE-only"),
arg_lit0(NULL, "status", "show current WiFi connection state + IP"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, true);
uint8_t ssid[64] = {0};
int ssid_len = 0;
CLIParamStrToBuf(arg_get_str(ctx, 1), ssid, sizeof(ssid) - 1, &ssid_len);
uint8_t pwd[64] = {0};
int pwd_len = 0;
CLIParamStrToBuf(arg_get_str(ctx, 2), pwd, sizeof(pwd) - 1, &pwd_len);
int port = arg_get_int_def(ctx, 3, 7777);
uint8_t host[33] = {0};
int host_len = 0;
CLIParamStrToBuf(arg_get_str(ctx, 4), host, sizeof(host) - 1, &host_len);
if (host_len == 0) {
strcpy((char *)host, "Proxmark5");
host_len = 9;
}
bool stop = arg_get_lit(ctx, 5);
bool status = arg_get_lit(ctx, 6);
CLIParserFree(ctx);
if (status) {
if (strcmp(verb, "status") == 0) {
uint8_t q[1] = { BWM_WIFI_ACTION_STATUS };
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_WIFI, q, sizeof(q));
@@ -1616,7 +1586,7 @@ static int CmdBWMWifi(const char *Cmd) {
}
switch (state) {
case 0xFF:
PrintAndLogEx(INFO, "BWM WiFi disabled (BLE-only). Bring it up with " _YELLOW_("hw bwmwifi --ssid <ssid> --pwd <pwd>"));
PrintAndLogEx(INFO, "BWM WiFi disabled (BLE-only). Bring it up with " _YELLOW_("hw bwm wifi --ssid <ssid> --pwd <pwd>"));
break;
case 2: // connected
if (ip) {
@@ -1645,7 +1615,7 @@ static int CmdBWMWifi(const char *Cmd) {
return PM3_SUCCESS;
}
if (stop) {
if (strcmp(verb, "stop") == 0) {
uint8_t off[1] = { BWM_WIFI_ACTION_STOP };
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_WIFI, off, sizeof(off));
@@ -1662,8 +1632,47 @@ static int CmdBWMWifi(const char *Cmd) {
return PM3_SUCCESS;
}
CLIParserContext *ctx;
CLIParserInit(&ctx, "hw bwm wifi",
"Bring up the BWM in STA + TCP-server mode: join a WiFi network and\n"
"start a TCP server so the client can connect over WiFi. PM5 only.\n"
"Sub-actions (no dashes): 'status' shows state, 'stop' tears WiFi down.",
"hw bwm wifi status --> show connection state + IP\n"
"hw bwm wifi stop --> tear down WiFi, back to BLE-only\n"
"hw bwm wifi --ssid Home --pwd secret --> bring up, port 7777\n"
"hw bwm wifi --ssid Home --pwd secret --port 9000");
void *argtable[] = {
arg_param_begin,
arg_str0(NULL, "ssid", "<ssid>", "WiFi SSID to join"),
arg_str0(NULL, "pwd", "<pwd>", "WiFi password (omit for open network)"),
arg_int0(NULL, "port", "<dec>", "TCP server listen port (default 7777)"),
arg_str0(NULL, "hostname", "<name>", "DHCP hostname (default Proxmark5)"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, true);
uint8_t ssid[64] = {0};
int ssid_len = 0;
CLIParamStrToBuf(arg_get_str(ctx, 1), ssid, sizeof(ssid) - 1, &ssid_len);
uint8_t pwd[64] = {0};
int pwd_len = 0;
CLIParamStrToBuf(arg_get_str(ctx, 2), pwd, sizeof(pwd) - 1, &pwd_len);
int port = arg_get_int_def(ctx, 3, 7777);
uint8_t host[33] = {0};
int host_len = 0;
CLIParamStrToBuf(arg_get_str(ctx, 4), host, sizeof(host) - 1, &host_len);
if (host_len == 0) {
strcpy((char *)host, "Proxmark5");
host_len = 9;
}
CLIParserFree(ctx);
if (ssid_len == 0) {
PrintAndLogEx(FAILED, "an SSID is required (or use --stop to tear down)");
PrintAndLogEx(FAILED, "an SSID is required (or " _YELLOW_("hw bwm wifi stop") " to tear down)");
return PM3_EINVARG;
}
if (port < 1 || port > 65535) {
@@ -1699,7 +1708,7 @@ static int CmdBWMWifi(const char *Cmd) {
}
if (resp.status != PM3_SUCCESS) {
PrintAndLogEx(FAILED, "BWM WiFi bring-up failed (check SSID/password and signal)");
PrintAndLogEx(HINT, "If it may have joined after DHCP, check: " _YELLOW_("hw bwmwifi --status"));
PrintAndLogEx(HINT, "If it may have joined after DHCP, check: " _YELLOW_("hw bwm wifi status"));
return resp.status;
}
@@ -1713,31 +1722,34 @@ static int CmdBWMWifi(const char *Cmd) {
}
static int CmdBwmCharge(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hw bwmcharge",
"Enable or disable BWM battery charging by clearing/setting the\n"
"AW32001E charge-enable bit (CEB, REG01[3]). PM5 only.\n"
_RED_("One-shot:") " the charger watchdog reverts this after ~160 s unless\n"
"serviced, so charging may stop on its own. Use to nudge a top-up.",
"hw bwmcharge -on --> enable charging\n"
"hw bwmcharge --off --> disable charging");
// Positional sub-action (no dashes): hw bwm charge on | off
char verb[16] = {0};
sscanf(Cmd, "%15s", verb);
bool on = (strcmp(verb, "on") == 0);
bool off = (strcmp(verb, "off") == 0);
void *argtable[] = {
arg_param_begin,
arg_lit0(NULL, "on", "enable charging (default)"),
arg_lit0(NULL, "off", "disable charging"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, true);
bool on = arg_get_lit(ctx, 1);
bool off = arg_get_lit(ctx, 2);
CLIParserFree(ctx);
if (on && off) {
PrintAndLogEx(WARNING, "pick one of --on / --off");
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 charge",
"Enable or disable BWM battery charging by clearing/setting the\n"
"AW32001E charge-enable bit (CEB, REG01[3]). PM5 only.\n"
_RED_("One-shot:") " the charger watchdog reverts this after ~160 s unless\n"
"serviced, so charging may stop on its own. Use to nudge a top-up.",
"hw bwm charge off --> disable charging\n"
"hw bwm charge on --> enable charging");
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; // default (neither flag) = enable
uint8_t payload = off ? 0 : 1; // on -> 1 (enable), off -> 0 (disable)
PrintAndLogEx(INFO, "%s BWM battery charging...", off ? "Disabling" : "Enabling");
clearCommandBuffer();
@@ -1761,14 +1773,14 @@ static int CmdBwmCharge(const char *Cmd) {
static int CmdBwmVchg(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hw bwmvchg",
CLIParserInit(&ctx, "hw bwm vchg",
"Set the BWM charger (AW32001E) charge-voltage regulation target.\n"
"Lowering it below 4.2 V reduces top-of-charge stress and extends cell\n"
"life. Snaps to the nearest 15 mV step; clamped to 3600..4200 mV. This is\n"
"a runtime register write (reverts on the charger watchdog / POR); the\n"
"firmware re-applies the " _YELLOW_("4100 mV") " default at every boot. PM5 only.",
"hw bwmvchg --> set charge voltage to default 4100 mV (->4.095 V)\n"
"hw bwmvchg --mv 4200 --> set charge voltage to 4200 mV");
"hw bwm vchg --> set charge voltage to default 4100 mV (->4.095 V)\n"
"hw bwm vchg --mv 4200 --> set charge voltage to 4200 mV");
void *argtable[] = {
arg_param_begin,
@@ -1805,13 +1817,13 @@ static int CmdBwmVchg(const char *Cmd) {
static int CmdBwmSetCap(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hw bwmsetcap",
CLIParserInit(&ctx, "hw bwm setcap",
"Program the BWM fuel gauge (BQ27427) Design Capacity for the fitted cell.\n"
"Run ONCE after fitting or replacing the battery. This triggers a gauge\n"
"config-update; do not run it repeatedly, as that disrupts the Impedance\n"
"Track learning cycle. PM5 only.",
"hw bwmsetcap --> set design capacity to default 500 mAh\n"
"hw bwmsetcap --cap 500 --> set design capacity to 500 mAh");
"hw bwm setcap --> set design capacity to default 500 mAh\n"
"hw bwm setcap --cap 500 --> set design capacity to 500 mAh");
void *argtable[] = {
arg_param_begin,
@@ -1932,7 +1944,7 @@ static int CmdPing(const char *Cmd) {
if (len > PM3_CMD_DATA_SIZE)
len = PM3_CMD_DATA_SIZE;
if (len) {
PrintAndLogEx(INFO, "Ping sent with payload len... " _YELLOW_("%d"), len);
} else {
@@ -2294,6 +2306,278 @@ static int CmdPM5QCTest(const char *Cmd) {
return PM3_SUCCESS;
}
static void progressbar(long sent, long total, int style) {
int percent = (int)((double)sent / total * 100);
// Use \r at the start to move the cursor back to the beginning of the line
printf("\rProgress: [%d%%]", percent);
// Force stdout to print immediately without waiting for a newline
fflush(stdout);
}
// One full OTA attempt: BEGIN -> WRITE... -> END. The BWM OTA has no resume
// (DEV.md 8.4): a dropped chunk can't be re-sent, so any failure here means the
// caller must restart the whole thing.
static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms) {
PacketResponseNG resp;
// BEGIN: tell the BWM how many bytes are coming. The ESP erases the idle
// OTA slot here; that can take 20-40 s on a 4 MB ESP32-C2, so wait longer
// than the device-side 60 s timeout plus USB round-trip.
uint8_t beg[5] = { BWM_OTA_ACTION_BEGIN,
(uint8_t)(fwlen & 0xFF), (uint8_t)((fwlen >> 8) & 0xFF),
(uint8_t)((fwlen >> 16) & 0xFF), (uint8_t)((fwlen >> 24) & 0xFF) };
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_ESP_OTA, beg, sizeof(beg));
if (WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 75000) == false) {
PrintAndLogEx(FAILED, "OTA begin timed out (ESP is likely still erasing the OTA slot)");
PrintAndLogEx(HINT, "Wait a few seconds and retry; do not power-cycle mid-erase.");
return PM3_ETIMEOUT;
}
if (resp.status != PM3_SUCCESS) {
PrintAndLogEx(FAILED, "OTA begin failed (status %d)%s", resp.status,
(resp.status == PM3_ETIMEOUT) ? " - UART timeout waiting for ESP" : "");
return resp.status;
}
PrintAndLogEx(INFO, "Uploading " _YELLOW_("%zu") " bytes of ESP firmware over the BWM link...", fwlen);
// WRITE chunks. Bounded by BWM_OTA_CHUNK_MAX (the ESP forwards each WRITE over
// its own small app_com UART frame - see bwm_wifi.c), not just the USB frame.
size_t maxchunk = MIN((size_t)g_conn.max_cmd_data_size - 1, (size_t)BWM_OTA_CHUNK_MAX);
uint8_t *buf = calloc(1, maxchunk + 1);
if (buf == NULL) {
return PM3_EMALLOC;
}
size_t sent = 0;
while (sent < fwlen) {
size_t n = MIN(maxchunk, fwlen - sent);
buf[0] = BWM_OTA_ACTION_WRITE;
memcpy(buf + 1, fw + sent, n);
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_ESP_OTA, buf, (uint16_t)(n + 1));
bool got = WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 25000);
if (!got || resp.status != PM3_SUCCESS) {
PrintAndLogEx(NORMAL, "");
if (!got) {
PrintAndLogEx(WARNING, "OTA write stalled at offset %zu (no response)", sent);
} else if (resp.status == PM3_ETIMEOUT) {
PrintAndLogEx(WARNING, "OTA write timed out at offset %zu (ESP did not ACK that chunk)", sent);
} else {
PrintAndLogEx(WARNING, "OTA write rejected at offset %zu (status %d)", sent, resp.status);
}
free(buf);
return PM3_EFAILED;
}
sent += n;
progressbar(sent, fwlen, STYLE_MIXED);
// Pace the stream. The client->AT32 hop (USB/BLE) is far faster than the
// AT32->ESP UART, so back-to-back writes can outrun the UART and drop a
// chunk -> esp_ota_end() then sees written < total and aborts. A small
// gap gives the UART time to drain. (BLE is naturally paced, which is why
// it "worked" and bursty USB did not - see nemanjan00.)
if (write_delay_ms) {
msleep(write_delay_ms);
}
}
free(buf);
PrintAndLogEx(NORMAL, "");
// END: finalize + set the new boot partition
uint8_t end[1] = { BWM_OTA_ACTION_END };
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_ESP_OTA, end, sizeof(end));
bool got_end = WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 30000);
if (got_end && (resp.status == PM3_SUCCESS)) {
return PM3_SUCCESS;
}
if (got_end) {
// The BWM answered END with an error. The common one is a size mismatch:
// esp_ota_end() found written < total, i.e. chunks were dropped in transit.
// That is a genuine failure (boot partition NOT switched) - restart, and
// hint at pacing, which is the usual cure.
PrintAndLogEx(WARNING, "OTA finalize rejected (status %d) - data was lost in transit", resp.status);
PrintAndLogEx(HINT, "Try a per-write delay: " _YELLOW_("hw bwm upgrade -f <fw> --delay 20"));
return PM3_EFAILED;
}
// No answer at all. Over BLE the END auto-reboot drops the link before the ack
// returns, so a timeout here means "reached END, reboot likely happened" -
// verify by version rather than discarding a possibly-good flash.
return PM3_ETIMEOUT;
}
// Query the BWM's running firmware version string (APP_CMD_GET_VERSION_INFO).
static int bwm_get_version(char *out, size_t outlen) {
uint8_t a[1] = { BWM_OTA_ACTION_VERSION };
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_ESP_OTA, a, sizeof(a));
PacketResponseNG r;
if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &r, 5000) == false) || (r.status != PM3_SUCCESS)) {
return PM3_EFAILED;
}
uint16_t n = (r.length < (uint16_t)(outlen - 1)) ? r.length : (uint16_t)(outlen - 1);
memcpy(out, r.data.asBytes, n);
out[n] = 0;
return PM3_SUCCESS;
}
static int CmdBWMUpgrade(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hw bwm upgrade",
"Reflash the BWM (ESP32) firmware over the BWM link - no header, no soldering.\n"
"Requires a BWM that still responds; this updates a wrong-version ESP, it cannot\n"
"recover a fully bricked one (that still needs the 5-pin header + esptool).",
"hw bwm upgrade -f bwm_esp32.bin");
void *argtable[] = {
arg_param_begin,
arg_str1("f", "file", "<fn>", "ESP32 firmware image (.bin)"),
arg_int0(NULL, "delay", "<ms>", "per-chunk delay to pace the slow AT32<->ESP UART (default 20)"),
arg_param_end,
};
CLIExecWithReturn(ctx, Cmd, argtable, false);
int fnlen = 0;
char fn[FILE_PATH_SIZE] = {0};
CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)fn, sizeof(fn), &fnlen);
uint32_t write_delay_ms = (uint32_t)arg_get_int_def(ctx, 2, 20);
CLIParserFree(ctx);
if (fnlen == 0) {
PrintAndLogEx(FAILED, "no filename given");
return PM3_EINVARG;
}
uint8_t *fw = NULL;
size_t fwlen = 0;
if ((loadFile_safe(fn, "", (void **)&fw, &fwlen) != PM3_SUCCESS) || (fwlen == 0)) {
PrintAndLogEx(FAILED, "could not read " _YELLOW_("%s"), fn);
return PM3_EFILE;
}
// Safeguard: refuse to flash anything that is not an ESP32-C2 app image. The
// BWM ESP is an ESP32-C2; a wrong/other-chip image would brick it.
// [0x00] == 0xE9 -> ESP image magic
// [0x0C..0x0D] == 0x000C -> chip_id ESP32-C2 (LE uint16)
if (fwlen < 16) {
PrintAndLogEx(FAILED, "file is too small to be an ESP firmware image (%zu bytes)", fwlen);
free(fw);
return PM3_EFILE;
}
if (fw[0] != 0xE9) {
PrintAndLogEx(FAILED, "refusing to flash: not an ESP image (magic " _YELLOW_("0x%02X") ", expected 0xE9)", fw[0]);
free(fw);
return PM3_EFILE;
}
uint16_t chip_id = (uint16_t)(fw[0x0C] | (fw[0x0D] << 8));
if (chip_id != 0x000C) {
PrintAndLogEx(FAILED, "refusing to flash: image chip_id " _YELLOW_("0x%04X") " is not ESP32-C2 (0x000C)", chip_id);
free(fw);
return PM3_EFILE;
}
// Record the running version first, so we can confirm the update actually took
// even when the finalize ack is lost (the case that used to discard a completed
// flash and restart from scratch).
char ver_before[64] = {0};
bool have_before = (bwm_get_version(ver_before, sizeof(ver_before)) == PM3_SUCCESS);
if (have_before) {
PrintAndLogEx(INFO, "Current BWM firmware..... " _YELLOW_("%s"), ver_before);
}
// No resume (DEV.md 8.4): a chunk lost mid-transfer restarts the whole upload.
const int max_attempts = 6; // a single dropped chunk restarts the whole upload;
// more attempts make an all-fail run rare until per-chunk
// retry (offset-idempotent ESP write) lands.
for (int attempt = 1; attempt <= max_attempts; attempt++) {
if (attempt > 1) {
// Abort the in-flight ESP OTA (if any) and give a slow erase a chance
// to finish before we BEGIN again. Otherwise attempt N's BEGIN races
// attempt N-1's still-running erase and times out.
PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts);
uint8_t ab[1] = { BWM_OTA_ACTION_ABORT };
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_ESP_OTA, ab, sizeof(ab));
PacketResponseNG abortr;
(void)WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &abortr, 8000);
msleep(3000);
}
int res = bwm_ota_once(fw, fwlen, write_delay_ms);
// Failed during BEGIN/WRITE: image incomplete, restart the whole thing.
if ((res != PM3_SUCCESS) && (res != PM3_ETIMEOUT)) {
continue;
}
// Reached OTA_END (acked, or ack lost). The image is written and the boot
// partition is set - reboot into it and confirm by version.
if (res == PM3_ETIMEOUT) {
PrintAndLogEx(INFO, "finalize ack not seen - all data was sent, confirming by version...");
}
// The device's OTA_END handler already reboots the ESP into the new image
// (that is what drops the finalize ack over BLE). Just wait for it to come
// back and re-link, then confirm by version.
PrintAndLogEx(INFO, "BWM rebooting into the new image (link drops briefly)...");
msleep(8000); // reboot + re-negotiate baud + re-link
char ver_after[64] = {0};
bool have_after = (bwm_get_version(ver_after, sizeof(ver_after)) == PM3_SUCCESS);
if (have_after && have_before) {
if (strncmp(ver_before, ver_after, sizeof(ver_before)) != 0) {
PrintAndLogEx(SUCCESS, "BWM firmware updated: %s -> " _YELLOW_("%s"), ver_before, ver_after);
free(fw);
return PM3_SUCCESS;
}
PrintAndLogEx(WARNING, "BWM still reports " _YELLOW_("%s") " - update did not take, retrying", ver_after);
continue;
}
if (have_after) {
PrintAndLogEx(SUCCESS, "BWM now running " _YELLOW_("%s"), ver_after);
free(fw);
return PM3_SUCCESS;
}
// Could not re-read the version (link dropped on reboot, common over BLE).
// All data was uploaded, so treat as done and let the user confirm.
PrintAndLogEx(WARNING, "Could not re-read BWM version after reboot (link dropped?)");
PrintAndLogEx(HINT, "Reconnect and run " _YELLOW_("hw status") " to confirm the version.");
free(fw);
return PM3_SUCCESS;
}
free(fw);
// Exhausted retries without a confirmed update - restore the link and report.
PacketResponseNG resp;
uint8_t ab[1] = { BWM_OTA_ACTION_ABORT };
clearCommandBuffer();
SendCommandNG(CMD_PM5_BWM_ESP_OTA, ab, sizeof(ab));
(void)WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 8000);
PrintAndLogEx(FAILED, "BWM firmware update could not be confirmed after %d attempts", max_attempts);
return PM3_EFAILED;
}
static int CmdHelpBwm(const char *Cmd);
static command_t BwmCommandTable[] = {
{"help", CmdHelpBwm, AlwaysAvailable, "This help"},
{"autooff", CmdBwmAutoOff, IfPm5, "Toggle auto power-off on USB unplug"},
{"charge", CmdBwmCharge, IfPm5, "Enable/disable battery charging (one-shot)"},
{"setcap", CmdBwmSetCap, IfPm5, "Set fuel-gauge design capacity (run once after battery change)"},
{"upgrade", CmdBWMUpgrade, IfPm5, "Reflash BWM (ESP32) firmware over the BWM link, no header"},
{"vchg", CmdBwmVchg, IfPm5, "Set charger charge-voltage target (default 4100 mV)"},
{"wifi", CmdBWMWifi, IfPm5, "Bring up WiFi (STA + TCP server) for a tcp: connection"},
{NULL, NULL, NULL, NULL}
};
static int CmdHelpBwm(const char *Cmd) {
(void)Cmd;
CmdsHelp(BwmCommandTable);
return PM3_SUCCESS;
}
static int CmdBwm(const char *Cmd) {
clearCommandBuffer();
return CmdsParse(BwmCommandTable, Cmd);
}
static command_t CommandTable[] = {
{"help", CmdHelp, AlwaysAvailable, "This help"},
{"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"},
@@ -2322,11 +2606,7 @@ static command_t CommandTable[] = {
{"setmux", CmdSetMux, IfPm3Present, "Set the ADC mux to a specific value"},
{"standalone", CmdStandalone, IfPm3Present, "Start installed standalone mode on device"},
{"tia", CmdTia, IfPm3Present, "Trigger a Timing Interval Acquisition to re-adjust the RealTimeCounter divider"},
{"bwmsetcap", CmdBwmSetCap, IfPm5, "Set BWM fuel-gauge design capacity (PM5, run once after battery change)"},
{"bwmvchg", CmdBwmVchg, IfPm5, "Set BWM charger charge-voltage target (PM5, default 4100 mV)"},
{"bwmcharge", CmdBwmCharge, IfPm5, "Enable/disable BWM battery charging (PM5, one-shot)"},
{"bwmautooff", CmdBwmAutoOff, IfPm5, "Toggle auto power-off on USB unplug (PM5, BWM)"},
{"bwmwifi", CmdBWMWifi, IfPm5, "Bring up BWM WiFi (STA + TCP server) for a tcp: connection (PM5)"},
{"bwm", CmdBwm, IfPm5, "{ BWM (battery/wireless module) commands... }"},
{"tune", CmdTune, IfPm3Lf, "Measure tuning of device antenna"},
{"decay", CmdDecay, IfPm3Present, "Measure HF antenna decay after field-off"},
{NULL, NULL, NULL, NULL}
+78 -33
View File
@@ -10,6 +10,7 @@ is compiled in with the BWM platform extra.
- [1. Battery / BWM control](#1-battery--bwm-control)
- [2. BLE](#2-ble)
- [3. WiFi](#3-wifi)
- [4. Update BWM firmware](#4-update-bwm-firmware)
- [Notes](#notes)
---
@@ -53,7 +54,7 @@ charge state, input limit, charge current/voltage/enable) and the **BQ27427** fu
gauge (state-of-charge %, voltage, current, remaining/full capacity, temperature,
health).
### `hw bwmsetcap` — set fuel-gauge design capacity
### `hw bwm setcap` — set fuel-gauge design capacity
Program the BQ27427 **Design Capacity** for the fitted cell.
@@ -62,32 +63,33 @@ Program the BQ27427 **Design Capacity** for the fitted cell.
| `--cap <mAh>` | Design capacity in mAh (default `500`) |
```
hw bwmsetcap # set design capacity to the default 500 mAh
hw bwmsetcap --cap 500 # set design capacity to 500 mAh
hw bwm setcap # set design capacity to the default 500 mAh
hw bwm setcap --cap 500 # set design capacity to 500 mAh
```
> [!WARNING]
> Run this **once** after fitting or replacing the battery. Running it repeatedly
> triggers a gauge config-update and disrupts the Impedance Track learning cycle.
### `hw bwmcharge` — enable/disable charging (one-shot)
### `hw bwm charge` — enable/disable charging (one-shot)
Clears/sets the AW32001E charge-enable bit (`CEB`, `REG01[3]`).
| Argument | Description |
| -------- | ------------------------------------ |
| `--off` | Disable charging (default is enable) |
| Sub-action | Description |
| ---------- | ----------------- |
| `on` | Enable charging |
| `off` | Disable charging |
```
hw bwmcharge # enable charging
hw bwmcharge --off # disable charging
hw bwm charge on # enable charging
hw bwm charge off # disable charging
```
> [!NOTE]
> **One-shot:** the charger watchdog reverts this after ~160 s unless serviced, so
> charging may stop on its own. Use it to nudge a top-up.
### `hw bwmvchg` — set charge-voltage limit
### `hw bwm vchg` — set charge-voltage limit
Sets the AW32001E charge-voltage regulation target (`VBAT_REG`, `REG04[7:2]`).
Lowering it below 4.2 V reduces top-of-charge stress and extends cell life.
@@ -97,8 +99,8 @@ Lowering it below 4.2 V reduces top-of-charge stress and extends cell life.
| `--mv <mV>` | Charge voltage in mV (default `4100`, clamped `3600`..`4200`) |
```
hw bwmvchg # set the default 4100 mV target (-> 4.095 V)
hw bwmvchg --mv 4200 # set 4200 mV
hw bwm vchg # set the default 4100 mV target (-> 4.095 V)
hw bwm vchg --mv 4200 # set 4200 mV
```
The value snaps to the nearest 15 mV hardware step, and the command reports the
@@ -111,20 +113,20 @@ voltage actually applied (e.g. `4100` → `4.095 V`; `4110` would be the next st
> the next power cycle (which re-applies it), so you don't need to re-run it per
> charge.
### `hw bwmautooff` — auto power-off on USB unplug
### `hw bwm autooff` — auto power-off on USB unplug
Toggles automatic power-off when the PM5 is unplugged from USB. **Default is `on`:**
the board powers down after USB is removed so a BWM-equipped PM5 doesn't silently
drain the battery. Button power-on is unaffected.
| Argument | Description |
| -------- | ------------------------------- |
| `--on` | Enable auto power-off (default) |
| `--off` | Disable auto power-off |
| Sub-action | Description |
| ---------- | ----------------------- |
| `on` | Enable auto power-off |
| `off` | Disable auto power-off |
```
hw bwmautooff --off # disable auto power-off
hw bwmautooff --on # re-enable auto power-off
hw bwm autooff off # disable auto power-off
hw bwm autooff on # re-enable auto power-off
```
> [!NOTE]
@@ -278,15 +280,15 @@ Bluetooth, so one setup covers a Blueshark-equipped Proxmark3 too.
## 3. WiFi (STA + TCP server)
`hw bwmwifi` joins your WiFi network as a **station** and starts a **TCP server** on
the BWM. You then connect the client over plain TCP. Run `hw bwmwifi` over an
existing link (USB, or an existing BLE connection). Use `--status` to check an
`hw bwm wifi` joins your WiFi network as a **station** and starts a **TCP server** on
the BWM. You then connect the client over plain TCP. Run `hw bwm wifi` over an
existing link (USB, or an existing BLE connection). Use `hw bwm wifi status` to check an
existing connection without touching the config.
### 3.1 Bring up WiFi
```
hw bwmwifi --ssid <ssid> [--pwd <password>] [--port <n>] [--hostname <name>]
hw bwm wifi --ssid <ssid> [--pwd <password>] [--port <n>] [--hostname <name>]
```
| Argument | Description |
@@ -295,13 +297,14 @@ hw bwmwifi --ssid <ssid> [--pwd <password>] [--port <n>] [--hostname <name>]
| `--pwd <password>` | WiFi password (omit for an open network) |
| `--port <dec>` | TCP server listen port (default `7777`) |
| `--hostname <name>` | DHCP hostname (default `Proxmark5`) |
| `--status` | Show current WiFi connection state + IP (no reconnect) |
| `--stop` | Tear down WiFi and return to BLE-only |
Two sub-actions take no other arguments and are positional (no dashes):
`hw bwm wifi status` and `hw bwm wifi stop` (see 3.3 and 3.4).
```
hw bwmwifi --ssid Home --pwd secret
hw bwmwifi --ssid Home --pwd secret --port 9000 --hostname pm5-lab
hw bwmwifi --ssid OpenGuestWiFi # open network
hw bwm wifi --ssid Home --pwd secret
hw bwm wifi --ssid Home --pwd secret --port 9000 --hostname pm5-lab
hw bwm wifi --ssid OpenGuestWiFi # open network
```
The join can take ~15 s. On success it prints the assigned IP and the connect
@@ -325,7 +328,7 @@ strings:
> don't do it at all). There is **no mDNS / `.local`** responder, so the IP is the
> reliable path.
### 3.3 Check WiFi status — `hw bwmwifi --status`
### 3.3 Check WiFi status — `hw bwm wifi status`
Queries the BWM's current connection state and IP **without** reconnecting or
changing the WiFi config. Handy to confirm a join actually completed — the
@@ -333,7 +336,7 @@ bring-up occasionally reports failure if the DHCP lease lands late, even though
the STA connected a moment later.
```
hw bwmwifi --status
hw bwm wifi status
```
```
@@ -346,21 +349,63 @@ hw bwmwifi --status
> not connected right after a bring-up, wait a few seconds and re-check — the
> lease may still be in flight.
### 3.4 Stop WiFi — `hw bwmwifi --stop`
### 3.4 Stop WiFi — `hw bwm wifi stop`
Tears down the WiFi station and TCP server and returns the BWM to **BLE-only**.
Run it over an existing link (USB or BLE); it does not need any other arguments.
```
hw bwmwifi --stop
hw bwm wifi stop
```
> [!NOTE]
> If you issue `--stop` over the **WiFi** connection itself, that connection
> If you issue `hw bwm wifi stop` over the **WiFi** connection itself, that connection
> drops as the TCP server goes away — reconnect over USB or BLE afterwards.
---
## 4. Update BWM firmware — `hw bwm upgrade`
Reflashes the BWM (ESP32-C2) firmware **over the existing BWM link** — no 5-pin
header, no soldering, no `esptool`. Run it over whatever link you already have
(USB or BLE). It updates a BWM that still responds; it **cannot** recover a fully
bricked one — that still needs the production 5-pin header + `esptool`.
```
hw bwm upgrade -f <bwm_esp32.bin> [--delay <ms>]
```
| Argument | Description |
| -------------- | ------------------------------------------------------------- |
| `-f`, `--file` | ESP32-C2 firmware image `.bin` **(required)** |
| `--delay <ms>` | Per-chunk delay pacing the slow AT32-ESP UART (default `20`) |
The client refuses to flash anything that is not an ESP32-C2 app image (it checks
the ESP image magic `0xE9` and `chip_id 0x000C`), so a wrong-chip image can't
brick the module.
It prints the running version first, uploads the image, then reboots the ESP into
the new image and confirms by reading the version back:
```
[=] Current BWM firmware..... v1.2.3
[=] Uploading 812345 bytes of ESP firmware over the BWM link...
[=] BWM rebooting into the new image (link drops briefly)...
[+] BWM firmware updated: v1.2.3 -> v1.3.0
```
> [!NOTE]
> There is **no resume**: a chunk dropped mid-transfer restarts the whole upload,
> and the client retries automatically (up to 6 attempts). If uploads keep failing
> on the slow link, raise the pacing, e.g. `--delay 40`.
> [!NOTE]
> The link drops briefly while the ESP reboots into the new image. Over BLE the
> post-reboot version read sometimes can't complete even though the flash
> succeeded — reconnect and run `hw status` to confirm the running version.
---
## Notes
- Bulk transfers over BLE/WiFi (`lf read`, `mem dump`, trace download) are paced by
+9
View File
@@ -886,6 +886,15 @@ typedef struct {
#define CMD_PM5_BWM_AUTOOFF 0x017B
#define CMD_PM5_BWM_WIFI 0x017C
#define CMD_PM5_BWM_SET_VCHG 0x017D
#define CMD_PM5_BWM_ESP_OTA 0x017E
#define BWM_OTA_ACTION_BEGIN 0x00
#define BWM_OTA_ACTION_WRITE 0x01
#define BWM_OTA_ACTION_END 0x02
#define BWM_OTA_ACTION_ABORT 0x03
#define BWM_OTA_ACTION_VERSION 0x04
#define BWM_OTA_ACTION_REBOOT 0x05
// Max firmware bytes per WRITE action.
#define BWM_OTA_CHUNK_MAX 240
// CMD_PM5_BWM_WIFI payload: [action:u8][port:u16 LE][ssid\0][pwd\0][hostname\0]
#define BWM_WIFI_ACTION_START 0x00 // join AP + start TCP server
#define BWM_WIFI_ACTION_STOP 0x01 // tear down, back to BLE-only