diff --git a/armsrc/appmain.c b/armsrc/appmain.c index c25fe4b81..ea4896e76 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -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; } diff --git a/armsrc/bwm_uart_at32.c b/armsrc/bwm_uart_at32.c index 3ab6b87c8..e796f1945 100644 --- a/armsrc/bwm_uart_at32.c +++ b/armsrc/bwm_uart_at32.c @@ -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)); } diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index d0cdb9fed..99636819f 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -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; +} diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 2b8339f0d..d9528137c 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -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 diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index adf12d0e1..a30c6dd22 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -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 --pwd [--port ] + char verb[16] = {0}; + sscanf(Cmd, "%15s", verb); - void *argtable[] = { - arg_param_begin, - arg_str0(NULL, "ssid", "", "WiFi SSID to join (omit with --stop)"), - arg_str0(NULL, "pwd", "", "WiFi password (omit for open network)"), - arg_int0(NULL, "port", "", "TCP server listen port (default 7777)"), - arg_str0(NULL, "hostname", "", "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 --pwd ")); + PrintAndLogEx(INFO, "BWM WiFi disabled (BLE-only). Bring it up with " _YELLOW_("hw bwm wifi --ssid --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", "", "WiFi SSID to join"), + arg_str0(NULL, "pwd", "", "WiFi password (omit for open network)"), + arg_int0(NULL, "port", "", "TCP server listen port (default 7777)"), + arg_str0(NULL, "hostname", "", "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 --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", "", "ESP32 firmware image (.bin)"), + arg_int0(NULL, "delay", "", "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} diff --git a/doc/md/PM5_Start_Here/PM5-BWM-USAGE.md b/doc/md/PM5_Start_Here/PM5-BWM-USAGE.md index c3a75b358..b16fe983c 100644 --- a/doc/md/PM5_Start_Here/PM5-BWM-USAGE.md +++ b/doc/md/PM5_Start_Here/PM5-BWM-USAGE.md @@ -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 ` | 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 ` | 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 [--pwd ] [--port ] [--hostname ] +hw bwm wifi --ssid [--pwd ] [--port ] [--hostname ] ``` | Argument | Description | @@ -295,13 +297,14 @@ hw bwmwifi --ssid [--pwd ] [--port ] [--hostname ] | `--pwd ` | WiFi password (omit for an open network) | | `--port ` | TCP server listen port (default `7777`) | | `--hostname ` | 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 [--delay ] +``` + +| Argument | Description | +| -------------- | ------------------------------------------------------------- | +| `-f`, `--file` | ESP32-C2 firmware image `.bin` **(required)** | +| `--delay ` | 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 diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 45edec322..452a2cbc7 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -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