From 1d80a4164a2abaf23d195f9b260cd01bf11ffc70 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 06:34:49 +0200 Subject: [PATCH 01/89] Add files via upload Signed-off-by: Niel Nielsen --- armsrc/bwm wifi.c | 302 ++++++++++++++++++++++++++++++++++++++++++++++ armsrc/bwm wifi.h | 77 ++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 armsrc/bwm wifi.c create mode 100644 armsrc/bwm wifi.h diff --git a/armsrc/bwm wifi.c b/armsrc/bwm wifi.c new file mode 100644 index 000000000..fb5c3ce79 --- /dev/null +++ b/armsrc/bwm wifi.c @@ -0,0 +1,302 @@ +//----------------------------------------------------------------------------- +// BWM WiFi bring-up (PM5 / AT32 side). See bwm_wifi.h. +//----------------------------------------------------------------------------- +#include "bwm_wifi.h" +#include "bwm_uart_at32.h" // bwm_uart_write / bwm_uart_rx_available / bwm_uart_read +#include "bwm_forward.h" // app_com header bytes + BWM_CMD_* forward codes +#include "pm3_cmd.h" // PM3_SUCCESS / PM3_ETIMEOUT / PM3_EFAILED / PM3_EOVFLOW +#include "ticks_apis.h" // GetTickCount / GetTickCountDelta +#include "dbprint.h" +#include "string.h" + +// app_com CRC-16/CCITT-FALSE (same as bwm_forward) +static uint16_t wifi_crc16(const uint8_t *d, size_t n, uint16_t crc) { + for (size_t i = 0; i < n; i++) { + crc ^= (uint16_t)d[i] << 8; + for (int b = 0; b < 8; b++) + crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1); + } + return crc; +} + +// Minimal one-shot app_com response parser: scans the UART RX stream for a +// SLAVE_RESP (0x2D 0x3D) echoing want_cmd, or a SLAVE_BCAST CMD_ERROR (8091). +// 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; + +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) { + + // ---- build + send HOST_CMD frame ---- + uint8_t frame[8 + 256]; + if (req_len > sizeof(frame) - 8) { + return PM3_EOVFLOW; + } + size_t idx = 0; + frame[idx++] = BWM_HDR_HOST_CMD_1; + frame[idx++] = BWM_HDR_HOST_CMD_2; + frame[idx++] = (uint8_t)(cmd & 0xFF); + frame[idx++] = (uint8_t)(cmd >> 8); + frame[idx++] = (uint8_t)(req_len & 0xFF); + frame[idx++] = (uint8_t)(req_len >> 8); + if (req_len) { + memcpy(&frame[idx], req, req_len); + idx += req_len; + } + uint16_t crc = wifi_crc16(frame, idx, 0xFFFF); + frame[idx++] = (uint8_t)(crc & 0xFF); + frame[idx++] = (uint8_t)(crc >> 8); + bwm_uart_write(frame, idx); + + // ---- wait for the matching SLAVE_RESP ---- + wstate_t st = W_H1; + bool is_resp = false; + uint16_t rcmd = 0, rlen = 0, rgot = 0, rcrc_recv = 0, rcrc_calc = 0; + uint8_t pbuf[256]; + uint8_t h1 = 0; + + uint32_t t0 = GetTickCount(); + for (;;) { + if (GetTickCountDelta(t0) > timeout_ms) { + return PM3_ETIMEOUT; + } + uint8_t buf[64]; + uint16_t avail = bwm_uart_rx_available(); + if (avail == 0) { + continue; + } + uint32_t n = bwm_uart_read(buf, (uint32_t)MIN(avail, (uint16_t)sizeof(buf))); + for (uint32_t i = 0; i < n; i++) { + uint8_t byte = buf[i]; + switch (st) { + case W_H1: + if (byte == BWM_HDR_SLAVE_RESP_1 || byte == BWM_HDR_SLAVE_BCAST_1) { + h1 = byte; + is_resp = (byte == BWM_HDR_SLAVE_RESP_1); + st = W_H2; + } + break; + case W_H2: { + bool ok = (is_resp && byte == BWM_HDR_SLAVE_RESP_2) || + (!is_resp && byte == BWM_HDR_SLAVE_BCAST_2); + if (ok) { + uint8_t hdr[2] = { h1, byte }; + rcrc_calc = wifi_crc16(hdr, 2, 0xFFFF); + rgot = 0; + st = W_CL; + } else { + st = W_H1; + } + break; + } + case W_CL: + rcmd = byte; + rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); + st = W_CH; + break; + case W_CH: + rcmd |= (uint16_t)byte << 8; + rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); + st = W_LL; + break; + case W_LL: + rlen = byte; + rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); + st = W_LH; + break; + case W_LH: + rlen |= (uint16_t)byte << 8; + rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); + st = (rlen ? W_PL : W_KL); + break; + case W_PL: + if (rgot < sizeof(pbuf)) pbuf[rgot] = byte; + rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); + if (++rgot >= rlen) st = W_KL; + break; + case W_KL: + rcrc_recv = byte; + st = W_KH; + break; + case W_KH: { + rcrc_recv |= (uint16_t)byte << 8; + if (rcrc_recv == rcrc_calc) { + if (is_resp && rcmd == cmd) { + // matching ack for our command + if (resp && resp_len) { + uint16_t cpy = MIN(rlen, *resp_len); + memcpy(resp, pbuf, cpy); + *resp_len = cpy; + } + 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; + } + // otherwise: some other frame (e.g. a stray broadcast) - ignore + } + st = W_H1; + break; + } + } + } + } +} + +// --------------------------------------------------------------------------- + +static int step(uint16_t cmd, const uint8_t *req, uint16_t rl, + uint8_t *resp, uint16_t *resp_len, uint32_t to, const char *what) { + int r = bwm_cmd(cmd, req, rl, resp, resp_len, to); + if (r != PM3_SUCCESS) { + Dbprintf("[bwm-wifi] %s failed (%d)", what, r); + } + return r; +} + +int bwm_wifi_forward_up(const char *ssid, const char *password, + const char *hostname, uint16_t tcp_port, uint32_t *ip_out) { + int r; + uint8_t b; + const uint32_t TO = 2000; // per-config-step timeout (ms) + + // 0) force a clean slate. If the BWM is already in forward mode (persisted + // NVS state or a failed boot auto-connect), SET_TO_WIFI_FORWARD_MODE + // takes an "already forward, skip init" path and leaves the WiFi context + // uninitialized, which then rejects SET_SSID. Disabling first guarantees + // the next forward-mode command runs the init path. Best-effort: the + // disable handler always acks, so ignore its result. + (void)bwm_cmd(BWM_CMD_SET_TO_WIFI_DISABLE_MODE, NULL, 0, NULL, NULL, TO); + + // 1) forwarding target = TCP server + b = BWM_WIFI_FORWARD_TCP_SERVER; + if ((r = step(BWM_CMD_SET_TO_WIFI_FORWARD_MODE, &b, 1, NULL, NULL, TO, "set forward mode")) != PM3_SUCCESS) return r; + + // 2) STA credentials + if ((r = step(BWM_CMD_SET_WIFI_CONNECT_CFG_SSID, (const uint8_t *)ssid, (uint16_t)strlen(ssid), NULL, NULL, TO, "set ssid")) != PM3_SUCCESS) return r; + if ((r = step(BWM_CMD_SET_WIFI_CONNECT_CFG_PWD, (const uint8_t *)password, (uint16_t)strlen(password), NULL, NULL, TO, "set password")) != PM3_SUCCESS) return r; + + // 3) TCP listen port (LE) + uint8_t p2[2] = { (uint8_t)(tcp_port & 0xFF), (uint8_t)(tcp_port >> 8) }; + if ((r = step(BWM_CMD_SET_TCP_SERVER_PORT, p2, 2, NULL, NULL, TO, "set tcp port")) != PM3_SUCCESS) return r; + + // 3b) DHCP hostname - must be set before the join so it rides the DHCP request + if ((r = step(BWM_CMD_SET_WIFI_CFG_HOST_NAME, (const uint8_t *)hostname, (uint16_t)strlen(hostname), NULL, NULL, TO, "set hostname")) != PM3_SUCCESS) return r; + + // 4) join the AP and wait for it to finish (up to ~15s) + if ((r = step(BWM_CMD_START_WIFI_CONNECT_TASK, NULL, 0, NULL, NULL, TO, "start connect")) != PM3_SUCCESS) return r; + uint8_t secs = 25; // iPhone/phone hotspots can be slow to become joinable + uint8_t wr[2]; + uint16_t wl = sizeof(wr); + if ((r = step(BWM_CMD_WAIT_FOR_WIFI_CONNECT_TASK, &secs, 1, wr, &wl, 30000, "wait connect")) != PM3_SUCCESS) return r; + + // 5) wait for DHCP. The STA reports "connected" on association, before it + // has an address, so poll GET_IP until a non-zero IP appears. Phone + // hotspots can take several seconds to hand one out. + uint32_t ip = 0; + uint32_t dhcp_start = GetTickCount(); + for (;;) { + uint8_t ipb[12]; + uint16_t il = sizeof(ipb); + if ((bwm_cmd(BWM_CMD_GET_WIFI_CFG_IP_ADDR, NULL, 0, ipb, &il, TO) == PM3_SUCCESS) && (il >= 4)) { + ip = (uint32_t)ipb[0] | ((uint32_t)ipb[1] << 8) | ((uint32_t)ipb[2] << 16) | ((uint32_t)ipb[3] << 24); + if (ip != 0) { + break; + } + } + if (GetTickCountDelta(dhcp_start) > BWM_WIFI_DHCP_WAIT_MS) { + break; // gave up waiting for a lease + } + uint32_t t = GetTickCount(); // brief pause before re-polling + while (GetTickCountDelta(t) < BWM_WIFI_DHCP_POLL_MS) { } + } + if (ip == 0) { + Dbprintf("[bwm-wifi] joined but no IP after DHCP wait (result=%u err=%u)", wr[0], wr[1]); + return PM3_EFAILED; + } + + // 6) start the TCP server (binds the STA interface now that it has an IP) + if ((r = step(BWM_CMD_START_TCP_SERVER, NULL, 0, NULL, NULL, TO, "start tcp server")) != PM3_SUCCESS) return r; + + *ip_out = ip; + return PM3_SUCCESS; +} + +int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out) { + const uint32_t TO = 2000; + *ip_out = 0; + *state = BWM_WIFI_STATE_OFF; + + // Ask the ESP for the WiFi connect state (not just the IP): this tells apart + // off / disconnected / connecting / connected, which the IP alone cannot. + uint8_t st = 0; + uint16_t sl = sizeof(st); + int r = bwm_cmd(BWM_CMD_GET_WIFI_CONNECT_STATUS, NULL, 0, &st, &sl, TO); + if (r == PM3_ETIMEOUT) { + return r; // BWM genuinely not responding + } + if (r != PM3_SUCCESS || sl < 1) { + *state = BWM_WIFI_STATE_OFF; // BWM answered; WiFi subsystem is off (BLE-only) + return PM3_SUCCESS; + } + *state = st; // 0 down, 1 connecting, 2 connected, 3 reconnecting, 4 stopped + + // Only chase an IP when actually connected. + if (st == BWM_WIFI_STATE_CONNECTED) { + uint8_t ipb[12]; + uint16_t il = sizeof(ipb); + if ((bwm_cmd(BWM_CMD_GET_WIFI_CFG_IP_ADDR, NULL, 0, ipb, &il, TO) == PM3_SUCCESS) && (il >= 4)) { + *ip_out = (uint32_t)ipb[0] | ((uint32_t)ipb[1] << 8) | ((uint32_t)ipb[2] << 16) | ((uint32_t)ipb[3] << 24); + } + } + return PM3_SUCCESS; +} + +int bwm_wifi_forward_down(void) { + // 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(); + while (GetTickCountDelta(t0) < 12000) { // overall cap + uint8_t st = 0; + uint16_t sl = sizeof(st); + int q = bwm_cmd(BWM_CMD_GET_WIFI_CONNECT_STATUS, NULL, 0, &st, &sl, 500); + // ETIMEOUT while the ESP is busy tearing down -> keep waiting. + // SUCCESS with a state -> still up mid-teardown -> keep waiting. + // any other (EFAILED etc.) -> BWM answered but WiFi is gone -> disabled. + if (q != PM3_SUCCESS && q != PM3_ETIMEOUT) { + return PM3_SUCCESS; + } + SpinDelay(300); + } + 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. +// --------------------------------------------------------------------------- +int bwm_esp_ota_begin(uint32_t total_size) { + 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, 15000); +} + +int bwm_esp_ota_write(const uint8_t *data, uint16_t len) { + return bwm_cmd(BWM_CMD_OTA_WRITE, data, len, NULL, NULL, 8000); +} + +int bwm_esp_ota_end(void) { + return bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000); +} diff --git a/armsrc/bwm wifi.h b/armsrc/bwm wifi.h new file mode 100644 index 000000000..800b2a225 --- /dev/null +++ b/armsrc/bwm wifi.h @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------- +// BWM WiFi bring-up (PM5 / AT32 side). +// +// Drives the BWM ESP32 into STA + TCP-server forward mode over the existing +// app_com UART4 link, so the pm3 client can connect with `tcp::`. +// This is the request/response counterpart to bwm_forward's transparent path: +// it is used at setup time, when no PM3 traffic is being forwarded. +//----------------------------------------------------------------------------- +#ifndef BWM_WIFI_H +#define BWM_WIFI_H + +#include "common.h" + +// 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 +#define BWM_CMD_SET_TO_WIFI_FORWARD_MODE 2001 // payload: 1 byte forward type (0 = TCP server) +#define BWM_CMD_GET_WIFI_CFG_IP_ADDR 2020 // resp: 3x uint32 LE {ip, netmask, gw} + +// After association the STA reports "connected" before DHCP completes, so we +// poll GET_IP until a non-zero address appears (or give up). +#ifndef BWM_WIFI_DHCP_WAIT_MS +#define BWM_WIFI_DHCP_WAIT_MS 20000 // total time to wait for a DHCP lease +#endif +#ifndef BWM_WIFI_DHCP_POLL_MS +#define BWM_WIFI_DHCP_POLL_MS 500 // gap between GET_IP polls +#endif +#define BWM_CMD_SET_WIFI_CONNECT_CFG_SSID 2023 // payload: SSID bytes +#define BWM_CMD_SET_WIFI_CONNECT_CFG_PWD 2025 // payload: password bytes +#define BWM_CMD_SET_WIFI_CFG_HOST_NAME 2021 // payload: hostname bytes (no NUL) +#define BWM_CMD_START_WIFI_CONNECT_TASK 2048 // no payload +#define BWM_CMD_GET_WIFI_CONNECT_STATUS 2050 // resp: 1 byte status +#define BWM_CMD_WAIT_FOR_WIFI_CONNECT_TASK 2051 // payload: 1 byte timeout(s); resp: {result, err_reason} +#define BWM_CMD_START_TCP_SERVER 2201 // no payload +#define BWM_CMD_SET_TCP_SERVER_PORT 2205 // payload: uint16 LE port + +#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 + +// 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. +// Returns PM3_SUCCESS on the matching ack, PM3_EFAILED on a CMD_ERROR report, +// PM3_ETIMEOUT if no response within timeout_ms. +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); + +// High-level: full STA-join + TCP-server bring-up. On success writes the +// BWM's IPv4 (host byte order, a in low byte) to *ip_out. +int bwm_wifi_forward_up(const char *ssid, const char *password, + const char *hostname, uint16_t tcp_port, uint32_t *ip_out); + +// Tear down WiFi forward mode (disconnect STA + stop TCP server, back to +// BLE-only). Persisted on the BWM so it stays off across reboots. +int bwm_wifi_forward_down(void); + +// Query current forward-mode connection state without reconfiguring. Writes the +// BWM IPv4 (host order, a in low byte; 0 if none) to *ip_out and 1/0 to +// *connected (true == has a DHCP lease). Returns PM3_EFAILED if the BWM/UART +// does not answer. +// WiFi connect state reported by --status (mirrors the ESP enum; 0xFF = off). +#define BWM_WIFI_STATE_DISCONNECTED 0 +#define BWM_WIFI_STATE_CONNECTING 1 +#define BWM_WIFI_STATE_CONNECTED 2 +#define BWM_WIFI_STATE_RECONNECT 3 +#define BWM_WIFI_STATE_STOPPED 4 +#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 +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); + +#endif From 50b0259978013b82ba210c35bd738dc45fc4dca0 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 06:36:07 +0200 Subject: [PATCH 02/89] Add files via upload Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 94 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index adf12d0e1..64593f530 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -1930,9 +1930,9 @@ static int CmdPing(const char *Cmd) { uint32_t len = arg_get_u32_def(ctx, 1, 32); CLIParserFree(ctx); - if (len > PM3_CMD_DATA_SIZE) - len = PM3_CMD_DATA_SIZE; - + if (len > pm3_max_cmd_data_size()) + len = pm3_max_cmd_data_size(); + if (len) { PrintAndLogEx(INFO, "Ping sent with payload len... " _YELLOW_("%d"), len); } else { @@ -2294,6 +2294,93 @@ static int CmdPM5QCTest(const char *Cmd) { return PM3_SUCCESS; } +static int CmdBWMUpgrade(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwmupgrade", + "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 bwmupgrade -f bwm_esp32.bin"); + void *argtable[] = { + arg_param_begin, + arg_str1("f", "file", "", "ESP32 firmware image (.bin)"), + 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); + 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; + } + + PacketResponseNG resp; + + // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) + 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); + free(fw); + return PM3_EFAILED; + } + PrintAndLogEx(INFO, "Uploading " _YELLOW_("%zu") " bytes of ESP firmware over the BWM link...", fwlen); + + // WRITE chunks (one action byte + as much firmware as fits the negotiated frame) + size_t maxchunk = (size_t)pm3_max_cmd_data_size() - 1; + uint8_t *buf = calloc(1, maxchunk + 1); + if (buf == NULL) { + free(fw); + 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)); + if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 12000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(FAILED, "OTA write failed at offset %zu", sent); + free(buf); + free(fw); + return PM3_EFAILED; + } + sent += n; + PrintAndLogEx(INPLACE, " %zu / %zu bytes (%zu%%)", sent, fwlen, (sent * 100) / fwlen); + } + free(buf); + PrintAndLogEx(NORMAL, ""); + + // END: finalize + set the new boot partition; the BWM reboots into it + uint8_t end[1] = { BWM_OTA_ACTION_END }; + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_ESP_OTA, end, sizeof(end)); + if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 30000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(FAILED, "OTA finalize failed"); + free(fw); + return PM3_EFAILED; + } + free(fw); + PrintAndLogEx(SUCCESS, "BWM firmware updated - the BWM will reboot into the new image"); + PrintAndLogEx(HINT, "Give it a few seconds, then re-check with " _YELLOW_("hw status")); + return PM3_SUCCESS; +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"}, @@ -2327,6 +2414,7 @@ static command_t CommandTable[] = { {"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)"}, + {"bwmupgrade", CmdBWMUpgrade, IfPm5, "Reflash BWM (ESP32) firmware over the BWM link, no header (PM5)"}, {"tune", CmdTune, IfPm3Lf, "Measure tuning of device antenna"}, {"decay", CmdDecay, IfPm3Present, "Measure HF antenna decay after field-off"}, {NULL, NULL, NULL, NULL} From 36d6535d7ad1aacbd70cfa371e6211d30ca1c765 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 06:40:39 +0200 Subject: [PATCH 03/89] Add files via upload Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 64593f530..20ef54714 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2361,7 +2361,7 @@ static int CmdBWMUpgrade(const char *Cmd) { return PM3_EFAILED; } sent += n; - PrintAndLogEx(INPLACE, " %zu / %zu bytes (%zu%%)", sent, fwlen, (sent * 100) / fwlen); + print_progress(sent, fwlen, STYLE_MIXED); } free(buf); PrintAndLogEx(NORMAL, ""); From a0573bf3cc3fc217310b242655a603eae068b2a7 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 07:04:56 +0200 Subject: [PATCH 04/89] Update pm3_cmd.h Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index f2b6c03bf..7c2234805 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -883,6 +883,10 @@ 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 // 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 From e89c30eebaf4feb416b202d5a646c1e81134edfd Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 07:06:38 +0200 Subject: [PATCH 05/89] Update length check for command data size Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 20ef54714..f37f81cec 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -1930,8 +1930,8 @@ static int CmdPing(const char *Cmd) { uint32_t len = arg_get_u32_def(ctx, 1, 32); CLIParserFree(ctx); - if (len > pm3_max_cmd_data_size()) - len = pm3_max_cmd_data_size(); + if (len > PM3_CMD_DATA_SIZE) + len = PM3_CMD_DATA_SIZE; if (len) { PrintAndLogEx(INFO, "Ping sent with payload len... " _YELLOW_("%d"), len); From 4860890b5ec331936c16fea3820f1ffe313bb9bc Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 07:10:42 +0200 Subject: [PATCH 06/89] Add comms.h include to cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index f37f81cec..5a76a7b68 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -48,6 +48,7 @@ #include "flash.h" // reboot to bootloader mode #include "proxgui.h" #include "graph.h" // for graph data +#include "comms.h" #include "lua.h" From 9cf3960f471f5e2515495fd16f96dfe9b6c641ef Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 07:11:35 +0200 Subject: [PATCH 07/89] Declare pm3_max_cmd_data_size function Added a new function declaration for pm3_max_cmd_data_size. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 5a76a7b68..35d70914d 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -52,6 +52,8 @@ #include "lua.h" +size_t pm3_max_cmd_data_size(void); + static int CmdHelp(const char *Cmd); static void lookup_chipid_short(uint32_t iChipID, uint32_t mem_used, uint32_t flash_size) { From ed8fa3bd040aa1215f3d8b89f374ac759cdb326d Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 07:19:02 +0200 Subject: [PATCH 08/89] Remove unused function declaration Removed unused function declaration for pm3_max_cmd_data_size. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 35d70914d..5a76a7b68 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -52,8 +52,6 @@ #include "lua.h" -size_t pm3_max_cmd_data_size(void); - static int CmdHelp(const char *Cmd); static void lookup_chipid_short(uint32_t iChipID, uint32_t mem_used, uint32_t flash_size) { From b6a1e0caf83059375ead4c9a4c440fb9f31a6fb8 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 07:20:03 +0200 Subject: [PATCH 09/89] Add function prototype for pm3_max_cmd_data_size Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 5a76a7b68..c2dc34b66 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -53,6 +53,7 @@ #include "lua.h" static int CmdHelp(const char *Cmd); +size_t pm3_max_cmd_data_size(void); static void lookup_chipid_short(uint32_t iChipID, uint32_t mem_used, uint32_t flash_size) { // AT32 (PM5): the chip id is an ARM DBGMCU IDCODE, not an Atmel CIDR, so the From bb42ca8ca75c33ba13a9ebcb07acddc409d19cc2 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 08:03:06 +0200 Subject: [PATCH 10/89] Remove unused include for comms.h Removed unused comms.h include from cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index c2dc34b66..f37f81cec 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -48,12 +48,10 @@ #include "flash.h" // reboot to bootloader mode #include "proxgui.h" #include "graph.h" // for graph data -#include "comms.h" #include "lua.h" static int CmdHelp(const char *Cmd); -size_t pm3_max_cmd_data_size(void); static void lookup_chipid_short(uint32_t iChipID, uint32_t mem_used, uint32_t flash_size) { // AT32 (PM5): the chip id is an ARM DBGMCU IDCODE, not an Atmel CIDR, so the From efed954b259434c2d720ebb03e9eda7ef8f00d0e Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 08:20:33 +0200 Subject: [PATCH 11/89] Update maxchunk calculation to use g_conn structure Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index f37f81cec..0a2659acc 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2340,7 +2340,7 @@ static int CmdBWMUpgrade(const char *Cmd) { PrintAndLogEx(INFO, "Uploading " _YELLOW_("%zu") " bytes of ESP firmware over the BWM link...", fwlen); // WRITE chunks (one action byte + as much firmware as fits the negotiated frame) - size_t maxchunk = (size_t)pm3_max_cmd_data_size() - 1; + size_t maxchunk = (size_t)g_conn.max_cmd_data_size - 1; uint8_t *buf = calloc(1, maxchunk + 1); if (buf == NULL) { free(fw); From 229e89ef5066cfe5fbf470228f0743018309b66d Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 08:57:12 +0200 Subject: [PATCH 12/89] Add OTA handling for BWM ESP commands Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index ab94d1607..492bd696a 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -3866,6 +3866,25 @@ static void PacketReceived(PacketCommandNG *packet) { break; } #ifdef WITH_BWM_STATUS + case CMD_PM5_BWM_ESP_OTA: { + uint8_t action = packet->data.asBytes[0]; + int ret; + switch (action) { + case BWM_OTA_ACTION_BEGIN: + ret = bwm_esp_ota_begin(*(uint32_t *)(packet->data.asBytes + 1)); + break; + case BWM_OTA_ACTION_WRITE: + ret = bwm_esp_ota_write(packet->data.asBytes + 1, packet->length - 1); + break; + case BWM_OTA_ACTION_END: + ret = bwm_esp_ota_end(); + break; + default: + ret = PM3_EINVARG; + } + reply_ng(CMD_PM5_BWM_ESP_OTA, ret, NULL, 0); + break; + } case CMD_PM5_BWM_SET_VCHG: { // Set the AW32001E charge-voltage target (REG04 VBAT_REG). // Payload: optional uint16 mV (LE); absent -> default. From 6df7d90236f93e44a9e52d1b365ce52bfdc9a91f Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 08:59:05 +0200 Subject: [PATCH 13/89] Remove CMD_PM5_BWM_ESP_OTA handling Removed BWM OTA command handling from appmain.c Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 492bd696a..ab94d1607 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -3866,25 +3866,6 @@ static void PacketReceived(PacketCommandNG *packet) { break; } #ifdef WITH_BWM_STATUS - case CMD_PM5_BWM_ESP_OTA: { - uint8_t action = packet->data.asBytes[0]; - int ret; - switch (action) { - case BWM_OTA_ACTION_BEGIN: - ret = bwm_esp_ota_begin(*(uint32_t *)(packet->data.asBytes + 1)); - break; - case BWM_OTA_ACTION_WRITE: - ret = bwm_esp_ota_write(packet->data.asBytes + 1, packet->length - 1); - break; - case BWM_OTA_ACTION_END: - ret = bwm_esp_ota_end(); - break; - default: - ret = PM3_EINVARG; - } - reply_ng(CMD_PM5_BWM_ESP_OTA, ret, NULL, 0); - break; - } case CMD_PM5_BWM_SET_VCHG: { // Set the AW32001E charge-voltage target (REG04 VBAT_REG). // Payload: optional uint16 mV (LE); absent -> default. From daaf9ca536701db0b2e6c68a45cc7707be9779eb Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:02:37 +0200 Subject: [PATCH 14/89] Add OTA handling for ESP32 in appmain.c Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index ab94d1607..00fb0e6d3 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -3924,6 +3924,40 @@ 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; + switch (action) { + 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: + res = bwm_esp_ota_write(packet->data.asBytes + 1, packet->length - 1); + break; + case BWM_OTA_ACTION_END: + res = bwm_esp_ota_end(); + break; + default: + res = PM3_EINVARG; + break; + } + reply_ng(CMD_PM5_BWM_ESP_OTA, res, NULL, 0); +#else + reply_ng(CMD_PM5_BWM_ESP_OTA, PM3_ENOTIMPL, NULL, 0); #endif break; } From 85e0e5b58fac7a74d6a8dbc78b50756909c86288 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:05:33 +0200 Subject: [PATCH 15/89] Delete armsrc/bwm wifi.c Signed-off-by: Niel Nielsen --- armsrc/bwm wifi.c | 302 ---------------------------------------------- 1 file changed, 302 deletions(-) delete mode 100644 armsrc/bwm wifi.c diff --git a/armsrc/bwm wifi.c b/armsrc/bwm wifi.c deleted file mode 100644 index fb5c3ce79..000000000 --- a/armsrc/bwm wifi.c +++ /dev/null @@ -1,302 +0,0 @@ -//----------------------------------------------------------------------------- -// BWM WiFi bring-up (PM5 / AT32 side). See bwm_wifi.h. -//----------------------------------------------------------------------------- -#include "bwm_wifi.h" -#include "bwm_uart_at32.h" // bwm_uart_write / bwm_uart_rx_available / bwm_uart_read -#include "bwm_forward.h" // app_com header bytes + BWM_CMD_* forward codes -#include "pm3_cmd.h" // PM3_SUCCESS / PM3_ETIMEOUT / PM3_EFAILED / PM3_EOVFLOW -#include "ticks_apis.h" // GetTickCount / GetTickCountDelta -#include "dbprint.h" -#include "string.h" - -// app_com CRC-16/CCITT-FALSE (same as bwm_forward) -static uint16_t wifi_crc16(const uint8_t *d, size_t n, uint16_t crc) { - for (size_t i = 0; i < n; i++) { - crc ^= (uint16_t)d[i] << 8; - for (int b = 0; b < 8; b++) - crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1); - } - return crc; -} - -// Minimal one-shot app_com response parser: scans the UART RX stream for a -// SLAVE_RESP (0x2D 0x3D) echoing want_cmd, or a SLAVE_BCAST CMD_ERROR (8091). -// 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; - -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) { - - // ---- build + send HOST_CMD frame ---- - uint8_t frame[8 + 256]; - if (req_len > sizeof(frame) - 8) { - return PM3_EOVFLOW; - } - size_t idx = 0; - frame[idx++] = BWM_HDR_HOST_CMD_1; - frame[idx++] = BWM_HDR_HOST_CMD_2; - frame[idx++] = (uint8_t)(cmd & 0xFF); - frame[idx++] = (uint8_t)(cmd >> 8); - frame[idx++] = (uint8_t)(req_len & 0xFF); - frame[idx++] = (uint8_t)(req_len >> 8); - if (req_len) { - memcpy(&frame[idx], req, req_len); - idx += req_len; - } - uint16_t crc = wifi_crc16(frame, idx, 0xFFFF); - frame[idx++] = (uint8_t)(crc & 0xFF); - frame[idx++] = (uint8_t)(crc >> 8); - bwm_uart_write(frame, idx); - - // ---- wait for the matching SLAVE_RESP ---- - wstate_t st = W_H1; - bool is_resp = false; - uint16_t rcmd = 0, rlen = 0, rgot = 0, rcrc_recv = 0, rcrc_calc = 0; - uint8_t pbuf[256]; - uint8_t h1 = 0; - - uint32_t t0 = GetTickCount(); - for (;;) { - if (GetTickCountDelta(t0) > timeout_ms) { - return PM3_ETIMEOUT; - } - uint8_t buf[64]; - uint16_t avail = bwm_uart_rx_available(); - if (avail == 0) { - continue; - } - uint32_t n = bwm_uart_read(buf, (uint32_t)MIN(avail, (uint16_t)sizeof(buf))); - for (uint32_t i = 0; i < n; i++) { - uint8_t byte = buf[i]; - switch (st) { - case W_H1: - if (byte == BWM_HDR_SLAVE_RESP_1 || byte == BWM_HDR_SLAVE_BCAST_1) { - h1 = byte; - is_resp = (byte == BWM_HDR_SLAVE_RESP_1); - st = W_H2; - } - break; - case W_H2: { - bool ok = (is_resp && byte == BWM_HDR_SLAVE_RESP_2) || - (!is_resp && byte == BWM_HDR_SLAVE_BCAST_2); - if (ok) { - uint8_t hdr[2] = { h1, byte }; - rcrc_calc = wifi_crc16(hdr, 2, 0xFFFF); - rgot = 0; - st = W_CL; - } else { - st = W_H1; - } - break; - } - case W_CL: - rcmd = byte; - rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); - st = W_CH; - break; - case W_CH: - rcmd |= (uint16_t)byte << 8; - rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); - st = W_LL; - break; - case W_LL: - rlen = byte; - rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); - st = W_LH; - break; - case W_LH: - rlen |= (uint16_t)byte << 8; - rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); - st = (rlen ? W_PL : W_KL); - break; - case W_PL: - if (rgot < sizeof(pbuf)) pbuf[rgot] = byte; - rcrc_calc = wifi_crc16(&byte, 1, rcrc_calc); - if (++rgot >= rlen) st = W_KL; - break; - case W_KL: - rcrc_recv = byte; - st = W_KH; - break; - case W_KH: { - rcrc_recv |= (uint16_t)byte << 8; - if (rcrc_recv == rcrc_calc) { - if (is_resp && rcmd == cmd) { - // matching ack for our command - if (resp && resp_len) { - uint16_t cpy = MIN(rlen, *resp_len); - memcpy(resp, pbuf, cpy); - *resp_len = cpy; - } - 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; - } - // otherwise: some other frame (e.g. a stray broadcast) - ignore - } - st = W_H1; - break; - } - } - } - } -} - -// --------------------------------------------------------------------------- - -static int step(uint16_t cmd, const uint8_t *req, uint16_t rl, - uint8_t *resp, uint16_t *resp_len, uint32_t to, const char *what) { - int r = bwm_cmd(cmd, req, rl, resp, resp_len, to); - if (r != PM3_SUCCESS) { - Dbprintf("[bwm-wifi] %s failed (%d)", what, r); - } - return r; -} - -int bwm_wifi_forward_up(const char *ssid, const char *password, - const char *hostname, uint16_t tcp_port, uint32_t *ip_out) { - int r; - uint8_t b; - const uint32_t TO = 2000; // per-config-step timeout (ms) - - // 0) force a clean slate. If the BWM is already in forward mode (persisted - // NVS state or a failed boot auto-connect), SET_TO_WIFI_FORWARD_MODE - // takes an "already forward, skip init" path and leaves the WiFi context - // uninitialized, which then rejects SET_SSID. Disabling first guarantees - // the next forward-mode command runs the init path. Best-effort: the - // disable handler always acks, so ignore its result. - (void)bwm_cmd(BWM_CMD_SET_TO_WIFI_DISABLE_MODE, NULL, 0, NULL, NULL, TO); - - // 1) forwarding target = TCP server - b = BWM_WIFI_FORWARD_TCP_SERVER; - if ((r = step(BWM_CMD_SET_TO_WIFI_FORWARD_MODE, &b, 1, NULL, NULL, TO, "set forward mode")) != PM3_SUCCESS) return r; - - // 2) STA credentials - if ((r = step(BWM_CMD_SET_WIFI_CONNECT_CFG_SSID, (const uint8_t *)ssid, (uint16_t)strlen(ssid), NULL, NULL, TO, "set ssid")) != PM3_SUCCESS) return r; - if ((r = step(BWM_CMD_SET_WIFI_CONNECT_CFG_PWD, (const uint8_t *)password, (uint16_t)strlen(password), NULL, NULL, TO, "set password")) != PM3_SUCCESS) return r; - - // 3) TCP listen port (LE) - uint8_t p2[2] = { (uint8_t)(tcp_port & 0xFF), (uint8_t)(tcp_port >> 8) }; - if ((r = step(BWM_CMD_SET_TCP_SERVER_PORT, p2, 2, NULL, NULL, TO, "set tcp port")) != PM3_SUCCESS) return r; - - // 3b) DHCP hostname - must be set before the join so it rides the DHCP request - if ((r = step(BWM_CMD_SET_WIFI_CFG_HOST_NAME, (const uint8_t *)hostname, (uint16_t)strlen(hostname), NULL, NULL, TO, "set hostname")) != PM3_SUCCESS) return r; - - // 4) join the AP and wait for it to finish (up to ~15s) - if ((r = step(BWM_CMD_START_WIFI_CONNECT_TASK, NULL, 0, NULL, NULL, TO, "start connect")) != PM3_SUCCESS) return r; - uint8_t secs = 25; // iPhone/phone hotspots can be slow to become joinable - uint8_t wr[2]; - uint16_t wl = sizeof(wr); - if ((r = step(BWM_CMD_WAIT_FOR_WIFI_CONNECT_TASK, &secs, 1, wr, &wl, 30000, "wait connect")) != PM3_SUCCESS) return r; - - // 5) wait for DHCP. The STA reports "connected" on association, before it - // has an address, so poll GET_IP until a non-zero IP appears. Phone - // hotspots can take several seconds to hand one out. - uint32_t ip = 0; - uint32_t dhcp_start = GetTickCount(); - for (;;) { - uint8_t ipb[12]; - uint16_t il = sizeof(ipb); - if ((bwm_cmd(BWM_CMD_GET_WIFI_CFG_IP_ADDR, NULL, 0, ipb, &il, TO) == PM3_SUCCESS) && (il >= 4)) { - ip = (uint32_t)ipb[0] | ((uint32_t)ipb[1] << 8) | ((uint32_t)ipb[2] << 16) | ((uint32_t)ipb[3] << 24); - if (ip != 0) { - break; - } - } - if (GetTickCountDelta(dhcp_start) > BWM_WIFI_DHCP_WAIT_MS) { - break; // gave up waiting for a lease - } - uint32_t t = GetTickCount(); // brief pause before re-polling - while (GetTickCountDelta(t) < BWM_WIFI_DHCP_POLL_MS) { } - } - if (ip == 0) { - Dbprintf("[bwm-wifi] joined but no IP after DHCP wait (result=%u err=%u)", wr[0], wr[1]); - return PM3_EFAILED; - } - - // 6) start the TCP server (binds the STA interface now that it has an IP) - if ((r = step(BWM_CMD_START_TCP_SERVER, NULL, 0, NULL, NULL, TO, "start tcp server")) != PM3_SUCCESS) return r; - - *ip_out = ip; - return PM3_SUCCESS; -} - -int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out) { - const uint32_t TO = 2000; - *ip_out = 0; - *state = BWM_WIFI_STATE_OFF; - - // Ask the ESP for the WiFi connect state (not just the IP): this tells apart - // off / disconnected / connecting / connected, which the IP alone cannot. - uint8_t st = 0; - uint16_t sl = sizeof(st); - int r = bwm_cmd(BWM_CMD_GET_WIFI_CONNECT_STATUS, NULL, 0, &st, &sl, TO); - if (r == PM3_ETIMEOUT) { - return r; // BWM genuinely not responding - } - if (r != PM3_SUCCESS || sl < 1) { - *state = BWM_WIFI_STATE_OFF; // BWM answered; WiFi subsystem is off (BLE-only) - return PM3_SUCCESS; - } - *state = st; // 0 down, 1 connecting, 2 connected, 3 reconnecting, 4 stopped - - // Only chase an IP when actually connected. - if (st == BWM_WIFI_STATE_CONNECTED) { - uint8_t ipb[12]; - uint16_t il = sizeof(ipb); - if ((bwm_cmd(BWM_CMD_GET_WIFI_CFG_IP_ADDR, NULL, 0, ipb, &il, TO) == PM3_SUCCESS) && (il >= 4)) { - *ip_out = (uint32_t)ipb[0] | ((uint32_t)ipb[1] << 8) | ((uint32_t)ipb[2] << 16) | ((uint32_t)ipb[3] << 24); - } - } - return PM3_SUCCESS; -} - -int bwm_wifi_forward_down(void) { - // 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(); - while (GetTickCountDelta(t0) < 12000) { // overall cap - uint8_t st = 0; - uint16_t sl = sizeof(st); - int q = bwm_cmd(BWM_CMD_GET_WIFI_CONNECT_STATUS, NULL, 0, &st, &sl, 500); - // ETIMEOUT while the ESP is busy tearing down -> keep waiting. - // SUCCESS with a state -> still up mid-teardown -> keep waiting. - // any other (EFAILED etc.) -> BWM answered but WiFi is gone -> disabled. - if (q != PM3_SUCCESS && q != PM3_ETIMEOUT) { - return PM3_SUCCESS; - } - SpinDelay(300); - } - 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. -// --------------------------------------------------------------------------- -int bwm_esp_ota_begin(uint32_t total_size) { - 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, 15000); -} - -int bwm_esp_ota_write(const uint8_t *data, uint16_t len) { - return bwm_cmd(BWM_CMD_OTA_WRITE, data, len, NULL, NULL, 8000); -} - -int bwm_esp_ota_end(void) { - return bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000); -} From efc01cd2f54951e1b605fbbc907e0e1de8f675e3 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:05:57 +0200 Subject: [PATCH 16/89] Delete armsrc/bwm wifi.h Signed-off-by: Niel Nielsen --- armsrc/bwm wifi.h | 77 ----------------------------------------------- 1 file changed, 77 deletions(-) delete mode 100644 armsrc/bwm wifi.h diff --git a/armsrc/bwm wifi.h b/armsrc/bwm wifi.h deleted file mode 100644 index 800b2a225..000000000 --- a/armsrc/bwm wifi.h +++ /dev/null @@ -1,77 +0,0 @@ -//----------------------------------------------------------------------------- -// BWM WiFi bring-up (PM5 / AT32 side). -// -// Drives the BWM ESP32 into STA + TCP-server forward mode over the existing -// app_com UART4 link, so the pm3 client can connect with `tcp::`. -// This is the request/response counterpart to bwm_forward's transparent path: -// it is used at setup time, when no PM3 traffic is being forwarded. -//----------------------------------------------------------------------------- -#ifndef BWM_WIFI_H -#define BWM_WIFI_H - -#include "common.h" - -// 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 -#define BWM_CMD_SET_TO_WIFI_FORWARD_MODE 2001 // payload: 1 byte forward type (0 = TCP server) -#define BWM_CMD_GET_WIFI_CFG_IP_ADDR 2020 // resp: 3x uint32 LE {ip, netmask, gw} - -// After association the STA reports "connected" before DHCP completes, so we -// poll GET_IP until a non-zero address appears (or give up). -#ifndef BWM_WIFI_DHCP_WAIT_MS -#define BWM_WIFI_DHCP_WAIT_MS 20000 // total time to wait for a DHCP lease -#endif -#ifndef BWM_WIFI_DHCP_POLL_MS -#define BWM_WIFI_DHCP_POLL_MS 500 // gap between GET_IP polls -#endif -#define BWM_CMD_SET_WIFI_CONNECT_CFG_SSID 2023 // payload: SSID bytes -#define BWM_CMD_SET_WIFI_CONNECT_CFG_PWD 2025 // payload: password bytes -#define BWM_CMD_SET_WIFI_CFG_HOST_NAME 2021 // payload: hostname bytes (no NUL) -#define BWM_CMD_START_WIFI_CONNECT_TASK 2048 // no payload -#define BWM_CMD_GET_WIFI_CONNECT_STATUS 2050 // resp: 1 byte status -#define BWM_CMD_WAIT_FOR_WIFI_CONNECT_TASK 2051 // payload: 1 byte timeout(s); resp: {result, err_reason} -#define BWM_CMD_START_TCP_SERVER 2201 // no payload -#define BWM_CMD_SET_TCP_SERVER_PORT 2205 // payload: uint16 LE port - -#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 - -// 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. -// Returns PM3_SUCCESS on the matching ack, PM3_EFAILED on a CMD_ERROR report, -// PM3_ETIMEOUT if no response within timeout_ms. -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); - -// High-level: full STA-join + TCP-server bring-up. On success writes the -// BWM's IPv4 (host byte order, a in low byte) to *ip_out. -int bwm_wifi_forward_up(const char *ssid, const char *password, - const char *hostname, uint16_t tcp_port, uint32_t *ip_out); - -// Tear down WiFi forward mode (disconnect STA + stop TCP server, back to -// BLE-only). Persisted on the BWM so it stays off across reboots. -int bwm_wifi_forward_down(void); - -// Query current forward-mode connection state without reconfiguring. Writes the -// BWM IPv4 (host order, a in low byte; 0 if none) to *ip_out and 1/0 to -// *connected (true == has a DHCP lease). Returns PM3_EFAILED if the BWM/UART -// does not answer. -// WiFi connect state reported by --status (mirrors the ESP enum; 0xFF = off). -#define BWM_WIFI_STATE_DISCONNECTED 0 -#define BWM_WIFI_STATE_CONNECTING 1 -#define BWM_WIFI_STATE_CONNECTED 2 -#define BWM_WIFI_STATE_RECONNECT 3 -#define BWM_WIFI_STATE_STOPPED 4 -#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 -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); - -#endif From 9c3732c9d67fff4b27faae581a50fd5174ffb389 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:06:23 +0200 Subject: [PATCH 17/89] Enhance comments in bwm_wifi.c for clarity Added comments to clarify the behavior of WiFi disable and OTA functions. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index d0cdb9fed..fb5c3ce79 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -256,7 +256,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 +279,24 @@ 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. +// --------------------------------------------------------------------------- +int bwm_esp_ota_begin(uint32_t total_size) { + 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, 15000); +} + +int bwm_esp_ota_write(const uint8_t *data, uint16_t len) { + return bwm_cmd(BWM_CMD_OTA_WRITE, data, len, NULL, NULL, 8000); +} + +int bwm_esp_ota_end(void) { + return bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000); +} From 9c257e40d65a985fc0449b760a0faf35eda45a7a Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:06:44 +0200 Subject: [PATCH 18/89] Add OTA commands for ESP firmware updates Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 2b8339f0d..800b2a225 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -65,4 +65,13 @@ 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 +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); + #endif From e5ed0d52e19fd16bc2845f34d0d28d0d1577ef0e Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:16:21 +0200 Subject: [PATCH 19/89] Add BWM OTA chunk size definition Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 7c2234805..15a08bed3 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -887,6 +887,11 @@ typedef struct { #define BWM_OTA_ACTION_BEGIN 0x00 #define BWM_OTA_ACTION_WRITE 0x01 #define BWM_OTA_ACTION_END 0x02 +// Max firmware bytes per WRITE action. Bounded by the BWM app_com UART link's +// internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is +// far smaller than the USB link's max_cmd_data_size - do not derive this from +// g_conn.max_cmd_data_size. +#define BWM_OTA_CHUNK_MAX 256 // 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 From eb34578ea9c73c4db6ef95d6fd664b456afa32ba Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:17:09 +0200 Subject: [PATCH 20/89] Refine maxchunk size calculation in cmdhw.c Updated maxchunk calculation to consider BWM_OTA_CHUNK_MAX. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 0a2659acc..91ca54b49 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2339,8 +2339,11 @@ static int CmdBWMUpgrade(const char *Cmd) { } PrintAndLogEx(INFO, "Uploading " _YELLOW_("%zu") " bytes of ESP firmware over the BWM link...", fwlen); - // WRITE chunks (one action byte + as much firmware as fits the negotiated frame) - size_t maxchunk = (size_t)g_conn.max_cmd_data_size - 1; + // WRITE chunks (one action byte + as much firmware as fits the negotiated frame). + // Bounded by BWM_OTA_CHUNK_MAX, not just the USB link's max_cmd_data_size: the + // firmware forwards each WRITE over the BWM app_com UART link, which has its + // own much smaller frame buffer (see bwm_wifi.c: bwm_cmd()). + 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) { free(fw); From 4fde8a0aa79ecd58ea72a036aac363f65a425a07 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:17:46 +0200 Subject: [PATCH 21/89] Implement overflow check in OTA write handling Added defensive checks for OTA write operation to prevent overflow. Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 00fb0e6d3..a6ec9bfbe 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -3946,7 +3946,15 @@ static void PacketReceived(PacketCommandNG *packet) { break; } case BWM_OTA_ACTION_WRITE: - res = bwm_esp_ota_write(packet->data.asBytes + 1, packet->length - 1); + // 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(); From 516f11a351d756f54b742490a84c61feb61f3bfd Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:33:29 +0200 Subject: [PATCH 22/89] Trigger reboot after successful OTA end Added reboot call after successful OTA finalization. Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index a6ec9bfbe..63f95740c 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -3958,6 +3958,13 @@ static void PacketReceived(PacketCommandNG *packet) { 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; default: res = PM3_EINVARG; From 120ab95ddcff8fbbb3ecea8d66ab2396ab03f5ff Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:34:16 +0200 Subject: [PATCH 23/89] Enhance CMD_ERROR handling and add reboot function Added error handling for CMD_ERROR broadcasts and implemented bwm_esp_reboot function. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index fb5c3ce79..66499ab89 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -130,11 +130,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; + // CMD_ERROR broadcasts are type 8091 regardless of which + // command failed - the failing command is only identified + // by the first 2 bytes of the payload (DEV.md 4.6). Other + // subsystems (WiFi/BLE/SNTP, etc.) can raise CMD_ERROR for + // their own commands while we're waiting here; only treat + // this as our failure if the embedded cmd actually matches. + 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) { + return PM3_EFAILED; + } + // unrelated command's error - ignore, keep waiting } // otherwise: some other frame (e.g. a stray broadcast) - ignore } @@ -300,3 +307,11 @@ int bwm_esp_ota_write(const uint8_t *data, uint16_t len) { int bwm_esp_ota_end(void) { return bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000); } + +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; +} From c3c8aab87557641fd753cb0f0b149ec77825e32d Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:34:36 +0200 Subject: [PATCH 24/89] Add reboot command for OTA process Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 800b2a225..f66993684 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -70,8 +70,12 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); #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 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); #endif From cf239b35b97cdfd2d75ddf8bc7b911ac4e66a1cf Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:38:50 +0200 Subject: [PATCH 25/89] Fix missing newline at end of appmain.c Add missing newline at the end of the file Signed-off-by: Niel Nielsen From 39e2117114250b3ea04b931c40aa48052d6674df Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:43:05 +0200 Subject: [PATCH 26/89] Improve error handling in bwm_wifi.c Handle error reporting for failed commands and update reboot command handling. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 66499ab89..af597f705 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -139,6 +139,12 @@ int bwm_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len, 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; } // unrelated command's error - ignore, keep waiting From 8a27354d4ba9ee7d3afface22aa1298114130902 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:44:45 +0200 Subject: [PATCH 27/89] Refactor OTA response handling for clarity Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 91ca54b49..9f354b62d 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2356,9 +2356,14 @@ static int CmdBWMUpgrade(const char *Cmd) { memcpy(buf + 1, fw + sent, n); clearCommandBuffer(); SendCommandNG(CMD_PM5_BWM_ESP_OTA, buf, (uint16_t)(n + 1)); - if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 12000) == false) || (resp.status != PM3_SUCCESS)) { + bool got = WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 12000); + if (!got || resp.status != PM3_SUCCESS) { PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(FAILED, "OTA write failed at offset %zu", sent); + if (!got) { + PrintAndLogEx(FAILED, "OTA write failed at offset %zu (no response - link/BWM unresponsive)", sent); + } else { + PrintAndLogEx(FAILED, "OTA write failed at offset %zu (status %d)", sent, resp.status); + } free(buf); free(fw); return PM3_EFAILED; From 0b936d2610dc2c8ad78585570bf2500d4891918a Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:52:51 +0200 Subject: [PATCH 28/89] Enhance logging during OTA for better diagnostics Added logging for ESP output during OTA process to aid in debugging. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index af597f705..6e66a6435 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -149,7 +149,14 @@ int bwm_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len, } // unrelated command's error - ignore, keep waiting } - // otherwise: some other frame (e.g. a stray broadcast) - ignore + // otherwise: some other frame - surface ESP log lines so a + // crash/reset shows its cause instead of just going silent; + // anything else (e.g. a stray unrelated broadcast) is ignored. + if (!is_resp && rcmd == BWM_CMD_LOG_MESSAGE && rlen > 0) { + uint16_t plen = MIN(rlen, (uint16_t)(sizeof(pbuf) - 1)); + pbuf[plen] = 0; + Dbprintf("[esp-log] %s", (const char *)pbuf); + } } st = W_H1; break; @@ -299,6 +306,13 @@ int bwm_wifi_forward_down(void) { // boot slot, so those get generous timeouts. // --------------------------------------------------------------------------- int bwm_esp_ota_begin(uint32_t total_size) { + // Best-effort: ask the ESP to forward its own ESP_LOGx output as broadcasts + // for the duration of the OTA, so a crash/reset shows its cause in the pm3 + // debug console instead of just going silent. Ignore failure - OTA can + // still proceed without diagnostics if this doesn't take. + uint8_t on = 1; + (void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &on, 1, NULL, NULL, 500); + 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) From 01f03609e4e57284aab5f8a47e0e159e7e7d8740 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 09:53:09 +0200 Subject: [PATCH 29/89] Update bwm_wifi.h Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index f66993684..6baecbbc6 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. From d4427d3da520c8687302af3d3b54c8d58293c085 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 10:21:42 +0200 Subject: [PATCH 30/89] Adjust OTA timeouts and restore log forwarding Increased timeout values for OTA write and end commands, and restored log forwarding after OTA. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 6e66a6435..b9362cfaf 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -306,12 +306,13 @@ int bwm_wifi_forward_down(void) { // boot slot, so those get generous timeouts. // --------------------------------------------------------------------------- int bwm_esp_ota_begin(uint32_t total_size) { - // Best-effort: ask the ESP to forward its own ESP_LOGx output as broadcasts - // for the duration of the OTA, so a crash/reset shows its cause in the pm3 - // debug console instead of just going silent. Ignore failure - OTA can - // still proceed without diagnostics if this doesn't take. - uint8_t on = 1; - (void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &on, 1, NULL, NULL, 500); + // 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); uint8_t p[4] = { (uint8_t)(total_size & 0xFF), (uint8_t)((total_size >> 8) & 0xFF), @@ -321,11 +322,15 @@ int bwm_esp_ota_begin(uint32_t total_size) { } int bwm_esp_ota_write(const uint8_t *data, uint16_t len) { - return bwm_cmd(BWM_CMD_OTA_WRITE, data, len, NULL, NULL, 8000); + return bwm_cmd(BWM_CMD_OTA_WRITE, data, len, NULL, NULL, 15000); } int bwm_esp_ota_end(void) { - return bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000); + int r = bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000); + // Restore log forwarding (silenced for the OTA in bwm_esp_ota_begin). + uint8_t on = 1; + (void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &on, 1, NULL, NULL, 500); + return r; } int bwm_esp_reboot(void) { From 0dfcb7220690b879e276e13fe324601f28272ece Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 10:44:08 +0200 Subject: [PATCH 31/89] Enhance documentation for OTA write function Added comments to clarify OTA write behavior and timeout handling. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index b9362cfaf..545a095c3 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -322,6 +322,11 @@ int bwm_esp_ota_begin(uint32_t total_size) { } 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, 15000); } From 339f528bba550732f6070b3def7d132886a35355 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 10:45:06 +0200 Subject: [PATCH 32/89] Refactor BWM firmware upgrade process Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 135 ++++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 58 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 9f354b62d..85682ec7d 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,6 +2294,66 @@ static int CmdPM5QCTest(const char *Cmd) { return PM3_SUCCESS; } +// 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) { + PacketResponseNG resp; + + // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) + 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); + return PM3_EFAILED; + } + 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, 15000); + if (!got || resp.status != PM3_SUCCESS) { + PrintAndLogEx(NORMAL, ""); + if (!got) { + PrintAndLogEx(WARNING, "OTA write stalled at offset %zu (no response)", sent); + } else { + PrintAndLogEx(WARNING, "OTA write rejected at offset %zu (status %d)", sent, resp.status); + } + free(buf); + return PM3_EFAILED; + } + sent += n; + print_progress(sent, fwlen, STYLE_MIXED); + } + 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)); + if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 30000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(WARNING, "OTA finalize failed"); + return PM3_EFAILED; + } + return PM3_SUCCESS; +} + static int CmdBWMUpgrade(const char *Cmd) { CLIParserContext *ctx; CLIParserInit(&ctx, "hw bwmupgrade", @@ -2324,71 +2384,30 @@ static int CmdBWMUpgrade(const char *Cmd) { return PM3_EFILE; } - PacketResponseNG resp; - - // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) - 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { - PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); - free(fw); - return PM3_EFAILED; - } - PrintAndLogEx(INFO, "Uploading " _YELLOW_("%zu") " bytes of ESP firmware over the BWM link...", fwlen); - - // WRITE chunks (one action byte + as much firmware as fits the negotiated frame). - // Bounded by BWM_OTA_CHUNK_MAX, not just the USB link's max_cmd_data_size: the - // firmware forwards each WRITE over the BWM app_com UART link, which has its - // own much smaller frame buffer (see bwm_wifi.c: bwm_cmd()). - 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) { - free(fw); - 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, 12000); - if (!got || resp.status != PM3_SUCCESS) { - PrintAndLogEx(NORMAL, ""); - if (!got) { - PrintAndLogEx(FAILED, "OTA write failed at offset %zu (no response - link/BWM unresponsive)", sent); - } else { - PrintAndLogEx(FAILED, "OTA write failed at offset %zu (status %d)", sent, resp.status); - } - free(buf); - free(fw); - return PM3_EFAILED; + // The BWM OTA has no resume (DEV.md 8.4): a dropped chunk must restart the + // whole transfer. The link can drop the odd frame over thousands of chunks, + // so retry the full upload a few times before giving up. + const int max_attempts = 3; + int res = PM3_EFAILED; + for (int attempt = 1; attempt <= max_attempts; attempt++) { + if (attempt > 1) { + PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); + } + res = bwm_ota_once(fw, fwlen); + if (res == PM3_SUCCESS) { + break; } - sent += n; - print_progress(sent, fwlen, STYLE_MIXED); - } - free(buf); - PrintAndLogEx(NORMAL, ""); - - // END: finalize + set the new boot partition; the BWM reboots into it - uint8_t end[1] = { BWM_OTA_ACTION_END }; - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_ESP_OTA, end, sizeof(end)); - if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 30000) == false) || (resp.status != PM3_SUCCESS)) { - PrintAndLogEx(FAILED, "OTA finalize failed"); - free(fw); - return PM3_EFAILED; } free(fw); + + if (res != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "BWM firmware update failed after %d attempts", max_attempts); + return res; + } PrintAndLogEx(SUCCESS, "BWM firmware updated - the BWM will reboot into the new image"); PrintAndLogEx(HINT, "Give it a few seconds, then re-check with " _YELLOW_("hw status")); return PM3_SUCCESS; } - static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"}, From cac9e2d4cc9408f67d3e45cb14428ed4fcff9741 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 10:59:52 +0200 Subject: [PATCH 33/89] Implement OTA abort handling in cmdhw.c Added logic to restore fast link and log OTA abort action on firmware update failure. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 85682ec7d..a31a20589 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2401,6 +2401,12 @@ static int CmdBWMUpgrade(const char *Cmd) { free(fw); if (res != PM3_SUCCESS) { + // Restore the fast link + logs that the OTA lowered at BEGIN. + 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 failed after %d attempts", max_attempts); return res; } From c75f4640dff8ba6d05c8363715395e6a1028e0b5 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 11:00:29 +0200 Subject: [PATCH 34/89] Implement OTA end and abort handling in bwm_wifi Added functions to handle OTA end and abort scenarios, including restoring baud rate and log forwarding. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 545a095c3..c25e8b5ce 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -314,6 +314,11 @@ int bwm_esp_ota_begin(uint32_t total_size) { uint8_t off = 0; (void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &off, 1, NULL, NULL, 500); + // Drop the AT32<->ESP link to a slow, forgiving baud for the transfer. An OTA + // is one-shot so speed is irrelevant, and 921600 is marginal against the + // flash-write / BLE contention that drops the odd frame -> random timeouts. + (void)bwm_fwd_negotiate_baud(BWM_OTA_BAUD); + 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) @@ -331,13 +336,24 @@ int bwm_esp_ota_write(const uint8_t *data, uint16_t len) { } int bwm_esp_ota_end(void) { + // OTA_END goes out at the slow OTA baud (both ends still there); only after it + // do we restore the fast link and log forwarding (both set in _begin). int r = bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000); - // Restore log forwarding (silenced for the OTA in bwm_esp_ota_begin). + (void)bwm_fwd_negotiate_baud(BWM_UART_BAUD_TARGET); uint8_t on = 1; (void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &on, 1, NULL, NULL, 500); return r; } +int bwm_esp_ota_abort(void) { + // OTA gave up mid-transfer: restore the fast link + logs so the module is + // usable again. The ESP's incomplete OTA state is discarded by the next BEGIN. + (void)bwm_fwd_negotiate_baud(BWM_UART_BAUD_TARGET); + uint8_t on = 1; + (void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &on, 1, NULL, NULL, 500); + 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 From 365fade8dd989203130466409fa144071d42c5f1 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 11:00:53 +0200 Subject: [PATCH 35/89] Add OTA abort function to bwm_wifi.h Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 6baecbbc6..c03c87d93 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -75,9 +75,14 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); // 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 +// Run the OTA at a slow, reliable baud (restored to the fast rate at end/abort). +#ifndef BWM_OTA_BAUD +#define BWM_OTA_BAUD 460800 +#endif 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 From 224cf4ad0bd49b2181951c05b20237dfcb3dfb0c Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 11:05:48 +0200 Subject: [PATCH 36/89] Fix OTA action handling in appmain.c Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 63f95740c..ae8b890f6 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -3941,7 +3941,11 @@ static void PacketReceived(PacketCommandNG *packet) { uint32_t total_size = 0; if (packet->length >= 5) { memcpy(&total_size, packet->data.asBytes + 1, sizeof(total_size)); - } + } else if (action == BWM_OTA_ACTION_END) { + res = bwm_esp_ota_end(); + } else if (action == BWM_OTA_ACTION_ABORT) { + res = bwm_esp_ota_abort(); + } res = bwm_esp_ota_begin(total_size); break; } From 9f265e81eaa7184f5b5edb6ad09a2d95c0abc8af Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 11:07:18 +0200 Subject: [PATCH 37/89] Define BWM_OTA_ACTION_ABORT for OTA commands Add BWM_OTA_ACTION_ABORT constant for OTA actions. Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 15a08bed3..71083ffcd 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -887,6 +887,7 @@ typedef struct { #define BWM_OTA_ACTION_BEGIN 0x00 #define BWM_OTA_ACTION_WRITE 0x01 #define BWM_OTA_ACTION_END 0x02 +#define BWM_OTA_ACTION_ABORT 0x03 // Max firmware bytes per WRITE action. Bounded by the BWM app_com UART link's // internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is // far smaller than the USB link's max_cmd_data_size - do not derive this from From 1c184f4028b969436589161585f68e0217db449c Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 11:49:39 +0200 Subject: [PATCH 38/89] Decrease BWM_OTA_CHUNK_MAX from 256 to 240 Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 71083ffcd..fc6f1c295 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -892,7 +892,7 @@ typedef struct { // internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is // far smaller than the USB link's max_cmd_data_size - do not derive this from // g_conn.max_cmd_data_size. -#define BWM_OTA_CHUNK_MAX 256 +#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 From 266033d7a825c160eb7b8cea23abe16ad7dd3ead Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 12:18:15 +0200 Subject: [PATCH 39/89] Add newline at end of bwm_uart_at32.c Fix formatting issue by adding a newline at the end of the file. Signed-off-by: Niel Nielsen From 97d6dcd274d98edc15bbda583a62b01eb0d59b7b Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 12:52:21 +0200 Subject: [PATCH 40/89] Add version info command and function declaration Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index c03c87d93..23f861adb 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -79,6 +79,8 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); #ifndef BWM_OTA_BAUD #define BWM_OTA_BAUD 460800 #endif +#define BWM_CMD_GET_VERSION_INFO 1000 // resp: running firmware version string +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); From 8237ac2cb832e7514f9517a7360509f82a327dd0 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 12:52:54 +0200 Subject: [PATCH 41/89] Add function to get ESP firmware version Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index c25e8b5ce..15182da76 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -305,6 +305,11 @@ int bwm_wifi_forward_down(void) { // 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 From c98e5fe69f382620bd6e7abc2188c556dacf633f Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 12:53:46 +0200 Subject: [PATCH 42/89] Refactor OTA finalize and version check logic Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 108 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index a31a20589..334624afb 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2347,10 +2347,33 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { uint8_t end[1] = { BWM_OTA_ACTION_END }; clearCommandBuffer(); SendCommandNG(CMD_PM5_BWM_ESP_OTA, end, sizeof(end)); - if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 30000) == false) || (resp.status != PM3_SUCCESS)) { - PrintAndLogEx(WARNING, "OTA finalize failed"); + bool got_end = WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 30000); + if (got_end && (resp.status == PM3_SUCCESS)) { + return PM3_SUCCESS; + } + if (got_end == false) { + // All data was sent and OTA_END was issued. esp_ota_end + set_boot_partition + // is slow, so its ack is easily lost even though the flash completed - this + // is the case that used to discard a finished image and restart. Signal + // "reached END, unconfirmed" so the caller verifies by version instead. + return PM3_ETIMEOUT; + } + PrintAndLogEx(WARNING, "OTA finalize rejected (status %d)", resp.status); + return PM3_EFAILED; +} + +// 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; } @@ -2384,35 +2407,76 @@ static int CmdBWMUpgrade(const char *Cmd) { return PM3_EFILE; } - // The BWM OTA has no resume (DEV.md 8.4): a dropped chunk must restart the - // whole transfer. The link can drop the odd frame over thousands of chunks, - // so retry the full upload a few times before giving up. + // 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 = 3; - int res = PM3_EFAILED; for (int attempt = 1; attempt <= max_attempts; attempt++) { if (attempt > 1) { PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); } - res = bwm_ota_once(fw, fwlen); - if (res == PM3_SUCCESS) { - break; + int res = bwm_ota_once(fw, fwlen); + + // 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..."); + } + uint8_t rb[1] = { BWM_OTA_ACTION_REBOOT }; + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_ESP_OTA, rb, sizeof(rb)); + PacketResponseNG rr; + (void)WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &rr, 5000); + + PrintAndLogEx(INFO, "Rebooting BWM into the new image (the BWM link drops briefly)..."); + msleep(7000); // 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); - if (res != PM3_SUCCESS) { - // Restore the fast link + logs that the OTA lowered at BEGIN. - 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 failed after %d attempts", max_attempts); - return res; - } - PrintAndLogEx(SUCCESS, "BWM firmware updated - the BWM will reboot into the new image"); - PrintAndLogEx(HINT, "Give it a few seconds, then re-check with " _YELLOW_("hw status")); - return PM3_SUCCESS; + // 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 command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, From 0245e99fde0eede016fbaf0bd0b68f45182db105 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 12:58:44 +0200 Subject: [PATCH 43/89] Add new OTA action definitions Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index fc6f1c295..92e7a78fd 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -888,6 +888,8 @@ typedef struct { #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. Bounded by the BWM app_com UART link's // internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is // far smaller than the USB link's max_cmd_data_size - do not derive this from From d9d26abeaacf8c2f2ef7236718d4d325540e0e4e Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 13:03:55 +0200 Subject: [PATCH 44/89] Handle OTA actions and improve response logic Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index ae8b890f6..399960177 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -3935,17 +3935,25 @@ static void PacketReceived(PacketCommandNG *packet) { // END: (no payload) finalize + set boot partition #if defined(WITH_BWM_FORWARD) uint8_t action = packet->data.asBytes[0]; - int res; + 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)); - } else if (action == BWM_OTA_ACTION_END) { - res = bwm_esp_ota_end(); - } else if (action == BWM_OTA_ACTION_ABORT) { - res = bwm_esp_ota_abort(); - } + } res = bwm_esp_ota_begin(total_size); break; } @@ -3970,11 +3978,16 @@ static void PacketReceived(PacketCommandNG *packet) { (void)bwm_esp_reboot(); } break; + case BWM_OTA_ACTION_ABORT: + res = bwm_esp_ota_abort(); + break; default: res = PM3_EINVARG; break; } - reply_ng(CMD_PM5_BWM_ESP_OTA, res, NULL, 0); + 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 From e05af0eea72ecf4c5f1b9f09c6a42ed4150abd69 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 13:04:59 +0200 Subject: [PATCH 45/89] Refactor OTA reboot handling in cmdhw.c Removed redundant reboot command and adjusted logging. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 334624afb..f8f0cbf40 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2434,14 +2434,11 @@ static int CmdBWMUpgrade(const char *Cmd) { if (res == PM3_ETIMEOUT) { PrintAndLogEx(INFO, "finalize ack not seen - all data was sent, confirming by version..."); } - uint8_t rb[1] = { BWM_OTA_ACTION_REBOOT }; - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_ESP_OTA, rb, sizeof(rb)); - PacketResponseNG rr; - (void)WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &rr, 5000); - - PrintAndLogEx(INFO, "Rebooting BWM into the new image (the BWM link drops briefly)..."); - msleep(7000); // reboot + re-negotiate baud + re-link + // 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); From 7c6153d42b2f9191f3509c65c379939e07e448b1 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 13:27:06 +0200 Subject: [PATCH 46/89] Add debug sleep for USB timeout in cmdhw.c Added a sleep function for USB timeout debugging. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index f8f0cbf40..00f4b673a 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2339,6 +2339,8 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { } sent += n; print_progress(sent, fwlen, STYLE_MIXED); + ///// DEBUG TEST FOR USB timeout + msleep(10); } free(buf); PrintAndLogEx(NORMAL, ""); From a911333c619f18c8fa24d55a31a6df19f0f41b91 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 13:37:48 +0200 Subject: [PATCH 47/89] Extend sleep time during device reboot process Increased sleep duration to allow for a longer reboot and re-negotiation period. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 00f4b673a..cf4c1b845 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2440,7 +2440,7 @@ static int CmdBWMUpgrade(const char *Cmd) { // (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 + msleep(10000); // reboot + re-negotiate baud + re-link char ver_after[64] = {0}; bool have_after = (bwm_get_version(ver_after, sizeof(ver_after)) == PM3_SUCCESS); @@ -2462,7 +2462,6 @@ static int CmdBWMUpgrade(const char *Cmd) { // 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; } From 1e2faf0e6cc273c86a3a499b64348d6e6333549c Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 13:59:14 +0200 Subject: [PATCH 48/89] Update cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index cf4c1b845..54e4e1528 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,6 +2294,16 @@ static int CmdPM5QCTest(const char *Cmd) { return PM3_SUCCESS; } +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. @@ -2338,7 +2348,8 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { return PM3_EFAILED; } sent += n; - print_progress(sent, fwlen, STYLE_MIXED); + progressbar(sent, fwlen, STYLE_MIXED); + printf("\n"); ///// DEBUG TEST FOR USB timeout msleep(10); } From 8cb8e6d7c0eae78d00db3ce6c44559313822d38f Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 14:01:17 +0200 Subject: [PATCH 49/89] Update cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 54e4e1528..7edab8c3b 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,13 +2294,9 @@ static int CmdPM5QCTest(const char *Cmd) { return PM3_SUCCESS; } -void progressbar(long sent, long total, int style) { +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); } From af0116bcfaccdf82b31f3a9541b9f3b3ced32a61 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 14:04:50 +0200 Subject: [PATCH 50/89] Update cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 7edab8c3b..2b47eab76 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2345,11 +2345,11 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { } sent += n; progressbar(sent, fwlen, STYLE_MIXED); - printf("\n"); ///// DEBUG TEST FOR USB timeout msleep(10); } free(buf); + printf("\n"); PrintAndLogEx(NORMAL, ""); // END: finalize + set the new boot partition From 21009dfc37a060cdf791a3643303fc5cab61a7cb Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 14:18:50 +0200 Subject: [PATCH 51/89] Increase sleep duration from 10ms to 20ms Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 2b47eab76..e52970763 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2346,7 +2346,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { sent += n; progressbar(sent, fwlen, STYLE_MIXED); ///// DEBUG TEST FOR USB timeout - msleep(10); + msleep(20); } free(buf); printf("\n"); From e2e0dabe3d6cc023675ca05043cfb301c5e1aee6 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 14:26:36 +0200 Subject: [PATCH 52/89] Update cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index e52970763..e479d2f53 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,12 +2294,6 @@ 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); - printf("\rProgress: [%d%%]", percent); - 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. @@ -2327,6 +2321,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { } size_t sent = 0; while (sent < fwlen) { + msleep(10); size_t n = MIN(maxchunk, fwlen - sent); buf[0] = BWM_OTA_ACTION_WRITE; memcpy(buf + 1, fw + sent, n); @@ -2344,9 +2339,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { return PM3_EFAILED; } sent += n; - progressbar(sent, fwlen, STYLE_MIXED); - ///// DEBUG TEST FOR USB timeout - msleep(20); + print_progress(sent, fwlen, STYLE_MIXED); ///// DEBUG TEST FOR USB timeout } free(buf); printf("\n"); From b8d31f0a81aea0309c1fe308c87fceb6b2eec523 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 14:43:38 +0200 Subject: [PATCH 53/89] Update cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index e479d2f53..089af1f01 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2321,11 +2321,11 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { } size_t sent = 0; while (sent < fwlen) { - msleep(10); + // msleep(10); size_t n = MIN(maxchunk, fwlen - sent); buf[0] = BWM_OTA_ACTION_WRITE; memcpy(buf + 1, fw + sent, n); - clearCommandBuffer(); + //clearCommandBuffer(); SendCommandNG(CMD_PM5_BWM_ESP_OTA, buf, (uint16_t)(n + 1)); bool got = WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 15000); if (!got || resp.status != PM3_SUCCESS) { From c4d4bdc99e0f76b4437a4561e2c83582fb2f401a Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 15:08:06 +0200 Subject: [PATCH 54/89] Refactor OTA command handling and adjust sleep duration Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 089af1f01..f8f0cbf40 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2321,11 +2321,10 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { } size_t sent = 0; while (sent < fwlen) { - // msleep(10); size_t n = MIN(maxchunk, fwlen - sent); buf[0] = BWM_OTA_ACTION_WRITE; memcpy(buf + 1, fw + sent, n); - //clearCommandBuffer(); + clearCommandBuffer(); SendCommandNG(CMD_PM5_BWM_ESP_OTA, buf, (uint16_t)(n + 1)); bool got = WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 15000); if (!got || resp.status != PM3_SUCCESS) { @@ -2339,10 +2338,9 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { return PM3_EFAILED; } sent += n; - print_progress(sent, fwlen, STYLE_MIXED); ///// DEBUG TEST FOR USB timeout + print_progress(sent, fwlen, STYLE_MIXED); } free(buf); - printf("\n"); PrintAndLogEx(NORMAL, ""); // END: finalize + set the new boot partition @@ -2440,7 +2438,7 @@ static int CmdBWMUpgrade(const char *Cmd) { // (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(10000); // reboot + re-negotiate baud + re-link + 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); @@ -2462,6 +2460,7 @@ static int CmdBWMUpgrade(const char *Cmd) { // 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; } From 76e88f3d5d87c3d83dc42917343fe681e5b166d4 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 15:14:43 +0200 Subject: [PATCH 55/89] Add write delay parameter to bwm_ota_once function Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index f8f0cbf40..62c6db1d5 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2297,7 +2297,7 @@ static int CmdPM5QCTest(const char *Cmd) { // 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) { +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 (it erases the target partition) @@ -2339,6 +2339,15 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { } sent += n; print_progress(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, ""); @@ -2351,15 +2360,19 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { if (got_end && (resp.status == PM3_SUCCESS)) { return PM3_SUCCESS; } - if (got_end == false) { - // All data was sent and OTA_END was issued. esp_ota_end + set_boot_partition - // is slow, so its ack is easily lost even though the flash completed - this - // is the case that used to discard a finished image and restart. Signal - // "reached END, unconfirmed" so the caller verifies by version instead. - return PM3_ETIMEOUT; + 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 bwmupgrade -f --delay 10")); + return PM3_EFAILED; } - PrintAndLogEx(WARNING, "OTA finalize rejected (status %d)", resp.status); - 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). @@ -2387,12 +2400,14 @@ static int CmdBWMUpgrade(const char *Cmd) { 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 10)"), 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, 10); CLIParserFree(ctx); if (fnlen == 0) { @@ -2422,7 +2437,7 @@ static int CmdBWMUpgrade(const char *Cmd) { if (attempt > 1) { PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); } - int res = bwm_ota_once(fw, fwlen); + 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)) { From 1bcae500fbc82805733b622aadafa6dbe117afcb Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 15:43:39 +0200 Subject: [PATCH 56/89] Update cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 62c6db1d5..6ae5be0d3 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,6 +2294,16 @@ static int CmdPM5QCTest(const char *Cmd) { return PM3_SUCCESS; } +void printprogress(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. @@ -2338,7 +2348,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms return PM3_EFAILED; } sent += n; - print_progress(sent, fwlen, STYLE_MIXED); + printprogress(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 From 23eba9491c4cb0f4cf0aa4447731e60d09f37aad Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 15:48:36 +0200 Subject: [PATCH 57/89] Change printprogress to static and rename function Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 6ae5be0d3..d8f62144e 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,7 +2294,7 @@ static int CmdPM5QCTest(const char *Cmd) { return PM3_SUCCESS; } -void printprogress(long sent, long total, int style) { +static void printprogress(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 @@ -2348,7 +2348,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms return PM3_EFAILED; } sent += n; - printprogress(sent, fwlen, STYLE_MIXED); + print_progress(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 From c20d1491f55887a58c17760513491cf38d6328ab Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 15:50:55 +0200 Subject: [PATCH 58/89] Rename printprogress to progressbar Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index d8f62144e..476f5e115 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,7 +2294,7 @@ static int CmdPM5QCTest(const char *Cmd) { return PM3_SUCCESS; } -static void printprogress(long sent, long total, int style) { +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 @@ -2348,7 +2348,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms return PM3_EFAILED; } sent += n; - print_progress(sent, fwlen, STYLE_MIXED); + 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 From e65238dbd443f4d517acdc38183945813256171f Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 17:17:22 +0200 Subject: [PATCH 59/89] Implement BWM firmware version retrieval Added functionality to read and print the BWM firmware version. Signed-off-by: Niel Nielsen --- armsrc/appmain.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 399960177..742922055 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")); From 6b41421a4ff6604cc61206c6ae92bbaa0c2dc054 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 21:14:42 +0200 Subject: [PATCH 60/89] Implement CmdBWMUpgrade function for firmware updates Refactor BWM firmware upgrade process and improve error handling. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 251 +++++++++++++++------------------------------ 1 file changed, 82 insertions(+), 169 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 476f5e115..a802f4908 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,112 +2294,6 @@ 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 (it erases the target partition) - 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { - PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); - return PM3_EFAILED; - } - 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, 15000); - if (!got || resp.status != PM3_SUCCESS) { - PrintAndLogEx(NORMAL, ""); - if (!got) { - PrintAndLogEx(WARNING, "OTA write stalled at offset %zu (no response)", 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 bwmupgrade -f --delay 10")); - 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 bwmupgrade", @@ -2410,14 +2304,12 @@ static int CmdBWMUpgrade(const char *Cmd) { 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 10)"), 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, 10); CLIParserFree(ctx); if (fnlen == 0) { @@ -2432,74 +2324,95 @@ static int CmdBWMUpgrade(const char *Cmd) { 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); + // 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 and need the + // 5-pin header + esptool to recover. + // [0x00] == 0xE9 -> ESP image magic (esp_image_header_t.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]); + PrintAndLogEx(HINT, "pass the app image (starts with 0xE9), not a merged/combined or unrelated file"); + 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); + PrintAndLogEx(HINT, "the BWM uses an ESP32-C2 - flashing another chip's image would brick it"); + free(fw); + return PM3_EFILE; } - // No resume (DEV.md 8.4): a chunk lost mid-transfer restarts the whole upload. - const int max_attempts = 3; - for (int attempt = 1; attempt <= max_attempts; attempt++) { - if (attempt > 1) { - PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); - } - int res = bwm_ota_once(fw, fwlen, write_delay_ms); + PacketResponseNG resp; - // 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."); + // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) + 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); free(fw); - return PM3_SUCCESS; + return PM3_EFAILED; + } + PrintAndLogEx(INFO, "Uploading " _YELLOW_("%zu") " bytes of ESP firmware over the BWM link...", fwlen); + + // WRITE chunks (one action byte + as much firmware as fits the negotiated frame). + // Bounded by BWM_OTA_CHUNK_MAX, not just the USB link's max_cmd_data_size: the + // firmware forwards each WRITE over the BWM app_com UART link, which has its + // own much smaller frame buffer (see bwm_wifi.c: bwm_cmd()). + 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) { + free(fw); + 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, 12000); + if (!got || resp.status != PM3_SUCCESS) { + PrintAndLogEx(NORMAL, ""); + if (!got) { + PrintAndLogEx(FAILED, "OTA write failed at offset %zu (no response - link/BWM unresponsive)", sent); + } else { + PrintAndLogEx(FAILED, "OTA write failed at offset %zu (status %d)", sent, resp.status); + } + free(buf); + free(fw); + return PM3_EFAILED; + } + sent += n; + print_progress(sent, fwlen, STYLE_MIXED); + } + free(buf); + PrintAndLogEx(NORMAL, ""); + + // END: finalize + set the new boot partition; the BWM reboots into it + uint8_t end[1] = { BWM_OTA_ACTION_END }; + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_ESP_OTA, end, sizeof(end)); + if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 30000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(FAILED, "OTA finalize failed"); + free(fw); + return PM3_EFAILED; } 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; + PrintAndLogEx(SUCCESS, "BWM firmware updated - the BWM will reboot into the new image"); + PrintAndLogEx(HINT, "Give it a few seconds, then re-check with " _YELLOW_("hw status")); + return PM3_SUCCESS; } + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"}, From 9944e4d161c64641c9977b041274428d31f76fa5 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Thu, 3 Sep 2026 21:23:26 +0200 Subject: [PATCH 61/89] Implement progress bar for OTA updates Add progress bar and enhance OTA update handling Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 234 +++++++++++++++++++++++++++++++++------------ 1 file changed, 171 insertions(+), 63 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index a802f4908..5883d02c1 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,6 +2294,112 @@ 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 (it erases the target partition) + 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); + return PM3_EFAILED; + } + 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, 15000); + if (!got || resp.status != PM3_SUCCESS) { + PrintAndLogEx(NORMAL, ""); + if (!got) { + PrintAndLogEx(WARNING, "OTA write stalled at offset %zu (no response)", 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 bwmupgrade -f --delay 10")); + 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 bwmupgrade", @@ -2304,12 +2410,14 @@ static int CmdBWMUpgrade(const char *Cmd) { 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 10)"), 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, 10); CLIParserFree(ctx); if (fnlen == 0) { @@ -2325,9 +2433,8 @@ static int CmdBWMUpgrade(const char *Cmd) { } // 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 and need the - // 5-pin header + esptool to recover. - // [0x00] == 0xE9 -> ESP image magic (esp_image_header_t.magic) + // 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); @@ -2336,83 +2443,84 @@ static int CmdBWMUpgrade(const char *Cmd) { } if (fw[0] != 0xE9) { PrintAndLogEx(FAILED, "refusing to flash: not an ESP image (magic " _YELLOW_("0x%02X") ", expected 0xE9)", fw[0]); - PrintAndLogEx(HINT, "pass the app image (starts with 0xE9), not a merged/combined or unrelated file"); 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); - PrintAndLogEx(HINT, "the BWM uses an ESP32-C2 - flashing another chip's image would brick it"); free(fw); return PM3_EFILE; } - - PacketResponseNG resp; - - // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) - 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { - PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); - free(fw); - return PM3_EFAILED; + + // 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); } - PrintAndLogEx(INFO, "Uploading " _YELLOW_("%zu") " bytes of ESP firmware over the BWM link...", fwlen); - // WRITE chunks (one action byte + as much firmware as fits the negotiated frame). - // Bounded by BWM_OTA_CHUNK_MAX, not just the USB link's max_cmd_data_size: the - // firmware forwards each WRITE over the BWM app_com UART link, which has its - // own much smaller frame buffer (see bwm_wifi.c: bwm_cmd()). - 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) { - free(fw); - 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, 12000); - if (!got || resp.status != PM3_SUCCESS) { - PrintAndLogEx(NORMAL, ""); - if (!got) { - PrintAndLogEx(FAILED, "OTA write failed at offset %zu (no response - link/BWM unresponsive)", sent); - } else { - PrintAndLogEx(FAILED, "OTA write failed at offset %zu (status %d)", sent, resp.status); - } - free(buf); - free(fw); - return PM3_EFAILED; + // No resume (DEV.md 8.4): a chunk lost mid-transfer restarts the whole upload. + const int max_attempts = 3; + for (int attempt = 1; attempt <= max_attempts; attempt++) { + if (attempt > 1) { + PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); } - sent += n; - print_progress(sent, fwlen, STYLE_MIXED); - } - free(buf); - PrintAndLogEx(NORMAL, ""); + int res = bwm_ota_once(fw, fwlen, write_delay_ms); - // END: finalize + set the new boot partition; the BWM reboots into it - uint8_t end[1] = { BWM_OTA_ACTION_END }; - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_ESP_OTA, end, sizeof(end)); - if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 30000) == false) || (resp.status != PM3_SUCCESS)) { - PrintAndLogEx(FAILED, "OTA finalize failed"); + // 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_EFAILED; + return PM3_SUCCESS; } free(fw); - PrintAndLogEx(SUCCESS, "BWM firmware updated - the BWM will reboot into the new image"); - PrintAndLogEx(HINT, "Give it a few seconds, then re-check with " _YELLOW_("hw status")); - return PM3_SUCCESS; -} + // 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 command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"}, From 73a3c85f2c2e3b6fc194d795d4db8096fe037ac6 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 06:21:20 +0200 Subject: [PATCH 62/89] Fix: ROERR handling On the AT32, an unhandled UART overrun (ROERR) latches: one overrun stops the USART feeding the DMA, every ack after it is lost, and only a re-init clears it. Signed-off-by: Niel Nielsen --- armsrc/bwm_uart_at32.c | 10 ++++++++++ 1 file changed, 10 insertions(+) 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)); } From dec0185b5a9633585463326a101e611dda747052 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 08:18:39 +0200 Subject: [PATCH 63/89] Increase BWM_OTA_CHUNK_MAX from 240 to 264 Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 92e7a78fd..dfbf66336 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -894,7 +894,7 @@ typedef struct { // internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is // far smaller than the USB link's max_cmd_data_size - do not derive this from // g_conn.max_cmd_data_size. -#define BWM_OTA_CHUNK_MAX 240 +#define BWM_OTA_CHUNK_MAX 264 // 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 From 857a85557a59c42bd76d01c0631b6bfb9c4fe540 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 08:22:58 +0200 Subject: [PATCH 64/89] Reduce BWM_OTA_CHUNK_MAX from 264 to 248 Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index dfbf66336..e0487108b 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -894,7 +894,7 @@ typedef struct { // internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is // far smaller than the USB link's max_cmd_data_size - do not derive this from // g_conn.max_cmd_data_size. -#define BWM_OTA_CHUNK_MAX 264 +#define BWM_OTA_CHUNK_MAX 248 // 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 From c4cc31e06bbbada91630431465e18b24fa1226db Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 08:27:17 +0200 Subject: [PATCH 65/89] Reduce BWM_OTA_CHUNK_MAX from 248 to 236 Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index e0487108b..5d2fa3e01 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -894,7 +894,7 @@ typedef struct { // internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is // far smaller than the USB link's max_cmd_data_size - do not derive this from // g_conn.max_cmd_data_size. -#define BWM_OTA_CHUNK_MAX 248 +#define BWM_OTA_CHUNK_MAX 236 // 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 From f5029c265f75c22aeb83c37dff28763ef0efbc3f Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 08:31:54 +0200 Subject: [PATCH 66/89] Increase BWM_OTA_CHUNK_MAX from 236 to 240 Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 5d2fa3e01..92e7a78fd 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -894,7 +894,7 @@ typedef struct { // internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is // far smaller than the USB link's max_cmd_data_size - do not derive this from // g_conn.max_cmd_data_size. -#define BWM_OTA_CHUNK_MAX 236 +#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 From 803856343cfb67caf56ddbe23f32827802fd4308 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 08:41:30 +0200 Subject: [PATCH 67/89] Clean up comments in bwm_wifi.c Removed comments regarding CMD_ERROR handling and ESP log message processing. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.c | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 15182da76..9b580c56e 100644 --- a/armsrc/bwm_wifi.c +++ b/armsrc/bwm_wifi.c @@ -130,12 +130,6 @@ int bwm_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len, } return PM3_SUCCESS; } - // CMD_ERROR broadcasts are type 8091 regardless of which - // command failed - the failing command is only identified - // by the first 2 bytes of the payload (DEV.md 4.6). Other - // subsystems (WiFi/BLE/SNTP, etc.) can raise CMD_ERROR for - // their own commands while we're waiting here; only treat - // this as our failure if the embedded cmd actually matches. 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) { @@ -147,15 +141,6 @@ int bwm_cmd(uint16_t cmd, const uint8_t *req, uint16_t req_len, Dbprintf("[bwm-wifi] cmd 0x%04x failed, esp_err=0x%08x", (unsigned)cmd, (unsigned)esp_err); return PM3_EFAILED; } - // unrelated command's error - ignore, keep waiting - } - // otherwise: some other frame - surface ESP log lines so a - // crash/reset shows its cause instead of just going silent; - // anything else (e.g. a stray unrelated broadcast) is ignored. - if (!is_resp && rcmd == BWM_CMD_LOG_MESSAGE && rlen > 0) { - uint16_t plen = MIN(rlen, (uint16_t)(sizeof(pbuf) - 1)); - pbuf[plen] = 0; - Dbprintf("[esp-log] %s", (const char *)pbuf); } } st = W_H1; From 1c2a42321abb48e47ada40297bde9025834626b9 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 08:48:14 +0200 Subject: [PATCH 68/89] Removed possible unnessecary clearCommandBuffer(); Removed redundant clearCommandBuffer() calls during OTA process. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 5883d02c1..d3b034d32 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2308,13 +2308,13 @@ static void progressbar(long sent, long total, int style) { // (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) { + clearCommandBuffer(); PacketResponseNG resp; // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); @@ -2334,7 +2334,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms size_t n = MIN(maxchunk, fwlen - sent); buf[0] = BWM_OTA_ACTION_WRITE; memcpy(buf + 1, fw + sent, n); - clearCommandBuffer(); + // clearCommandBuffer(); SendCommandNG(CMD_PM5_BWM_ESP_OTA, buf, (uint16_t)(n + 1)); bool got = WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 15000); if (!got || resp.status != PM3_SUCCESS) { From dd35afc886ba21e5f40c9e7bb278038ba5333ac1 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 08:54:57 +0200 Subject: [PATCH 69/89] Update cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index d3b034d32..a77e0f9cd 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2308,7 +2308,6 @@ static void progressbar(long sent, long total, int style) { // (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) { - clearCommandBuffer(); PacketResponseNG resp; // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) @@ -2330,6 +2329,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms return PM3_EMALLOC; } size_t sent = 0; + clearCommandBuffer(); while (sent < fwlen) { size_t n = MIN(maxchunk, fwlen - sent); buf[0] = BWM_OTA_ACTION_WRITE; From 36d2ab11980695b8b350d3b1419dacad5a417d75 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:02:09 +0200 Subject: [PATCH 70/89] Refactor OTA handling and improve timeout duration Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index a77e0f9cd..08995c69d 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2309,7 +2309,7 @@ static void progressbar(long sent, long total, int style) { // 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; - + clearCommandBuffer(); // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) uint8_t beg[5] = { BWM_OTA_ACTION_BEGIN, (uint8_t)(fwlen & 0xFF), (uint8_t)((fwlen >> 8) & 0xFF), @@ -2329,7 +2329,6 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms return PM3_EMALLOC; } size_t sent = 0; - clearCommandBuffer(); while (sent < fwlen) { size_t n = MIN(maxchunk, fwlen - sent); buf[0] = BWM_OTA_ACTION_WRITE; @@ -2371,17 +2370,12 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms 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. + // The BWM answered END with an error. PrintAndLogEx(WARNING, "OTA finalize rejected (status %d) - data was lost in transit", resp.status); PrintAndLogEx(HINT, "Try a per-write delay: " _YELLOW_("hw bwmupgrade -f --delay 10")); 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. + // No answer at all. return PM3_ETIMEOUT; } @@ -2391,7 +2385,7 @@ static int bwm_get_version(char *out, size_t outlen) { 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)) { + if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &r, 500) == false) || (r.status != PM3_SUCCESS)) { return PM3_EFAILED; } uint16_t n = (r.length < (uint16_t)(outlen - 1)) ? r.length : (uint16_t)(outlen - 1); @@ -2480,11 +2474,8 @@ static int CmdBWMUpgrade(const char *Cmd) { 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 + msleep(500); // reboot + re-negotiate baud + re-link char ver_after[64] = {0}; bool have_after = (bwm_get_version(ver_after, sizeof(ver_after)) == PM3_SUCCESS); From c1d47f2be9f95301971471ab1f6305723068aebb Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:10:45 +0200 Subject: [PATCH 71/89] Reduce BWM_OTA_CHUNK_MAX from 240 to 196 Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 92e7a78fd..b274efe7b 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -894,7 +894,7 @@ typedef struct { // internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is // far smaller than the USB link's max_cmd_data_size - do not derive this from // g_conn.max_cmd_data_size. -#define BWM_OTA_CHUNK_MAX 240 +#define BWM_OTA_CHUNK_MAX 196 // 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 From ef09ab673b306a224ae8851b09cd995f73ffdc05 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:15:11 +0200 Subject: [PATCH 72/89] Update BWM_OTA_CHUNK_MAX to allow larger firmware writes Increased the maximum firmware bytes per WRITE action from 196 to 2048. Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index b274efe7b..ef5b5bd83 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -890,11 +890,8 @@ typedef struct { #define BWM_OTA_ACTION_ABORT 0x03 #define BWM_OTA_ACTION_VERSION 0x04 #define BWM_OTA_ACTION_REBOOT 0x05 -// Max firmware bytes per WRITE action. Bounded by the BWM app_com UART link's -// internal frame buffer (bwm_wifi.c: bwm_cmd()'s `frame[8 + 256]`), which is -// far smaller than the USB link's max_cmd_data_size - do not derive this from -// g_conn.max_cmd_data_size. -#define BWM_OTA_CHUNK_MAX 196 +// Max firmware bytes per WRITE action. +#define BWM_OTA_CHUNK_MAX 2048 // 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 From fa7aa4e594ef6fb82539d35ab3aa1326dccb6b9c Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:17:49 +0200 Subject: [PATCH 73/89] Increase PM3_FPC_MAX_DATA from 2048 to 240 240 seems the best choise Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index ef5b5bd83..68d021f6b 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -38,7 +38,7 @@ // Over the BWM/FPC link the forward buffers (AT32 DMA ring, ESP UART RX) are // small, so a full PM3_CMD_DATA_SIZE frame overruns them. Cap the payload the // device advertises and sends when replying via FPC. USB is unaffected. -#define PM3_FPC_MAX_DATA 2048 +#define PM3_FPC_MAX_DATA 240 typedef struct { uint64_t cmd; From 1f6e4eed00b31c1f4c0f603c8ef95f25a73f670c Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:22:41 +0200 Subject: [PATCH 74/89] Uncomment clearCommandBuffer() for OTA command Uncommented the clearCommandBuffer() function to ensure the command buffer is cleared before sending OTA commands. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 08995c69d..d08a53d3a 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2333,7 +2333,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms size_t n = MIN(maxchunk, fwlen - sent); buf[0] = BWM_OTA_ACTION_WRITE; memcpy(buf + 1, fw + sent, n); - // clearCommandBuffer(); + clearCommandBuffer(); SendCommandNG(CMD_PM5_BWM_ESP_OTA, buf, (uint16_t)(n + 1)); bool got = WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 15000); if (!got || resp.status != PM3_SUCCESS) { @@ -2350,10 +2350,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms 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.) + // AT32->ESP UART) if (write_delay_ms) { msleep(write_delay_ms); } From fa9691d8d075c4fb9213ed7e3ca4148e352743e1 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:25:33 +0200 Subject: [PATCH 75/89] Update cmdhw.c Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2990 ++------------------------------------------ 1 file changed, 139 insertions(+), 2851 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index d08a53d3a..e796f1945 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -6,2881 +6,169 @@ // 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. //----------------------------------------------------------------------------- -// Hardware commands -// low-level hardware control +// AT32F435 UART4 driver for the Proxmark5 BWM link - see bwm_uart_at32.h. +// +// RX is serviced by circular DMA (DMA1 channel 2) rather than a per-byte +// RX interrupt. The old RDBF ISR dropped bytes on overrun as soon as the +// main loop was busy (FPGA / USB) - fine at 460800, fatal at higher bauds, +// where the dropped byte becomes an app_com CRC-16 failure and a stall. +// Circular DMA lets the controller sink every byte independent of CPU load, +// so BWM_UART_BAUD can be raised to lift BLE/WiFi transfer rates. //----------------------------------------------------------------------------- -#include -#include -#include -#include +#include "bwm_uart_at32.h" -#ifdef HAVE_PYTHON -#ifdef _POSIX_C_SOURCE -#undef _POSIX_C_SOURCE -#endif -#include -#endif - -#include "cmdparser.h" // command_t -#include "cliparser.h" -#include "comms.h" -#include "usart_defs.h" -#include "ui.h" -#include "fpga.h" -#include "cmdhw.h" -#include "cmdfpga.h" -#include "cmddata.h" -#include "commonutil.h" -#include "preferences.h" #include "pm3_cmd.h" -#include "pmflash.h" // rdv40validation_t -#include "cmdflashmem.h" // get_signature.. -#include "uart/uart.h" // configure timeout -#include "util_posix.h" -#include "flash.h" // reboot to bootloader mode -#include "proxgui.h" -#include "graph.h" // for graph data +#include "at32f435_437.h" +#include "at32f435_437_crm.h" +#include "at32f435_437_gpio.h" +#include "at32f435_437_usart.h" +#include "at32f435_437_dma.h" +#include "at32f435_437_misc.h" -#include "lua.h" +#define BWM_UART UART4 +#define BWM_UART_GPIO GPIOA +#define BWM_UART_TX_PIN GPIO_PINS_0 +#define BWM_UART_RX_PIN GPIO_PINS_1 +#define BWM_UART_TX_SRC GPIO_PINS_SOURCE0 +#define BWM_UART_RX_SRC GPIO_PINS_SOURCE1 +#define BWM_UART_MUX GPIO_MUX_8 -static int CmdHelp(const char *Cmd); +// SSC already owns DMA1 channel 1 (see fpga_hw_at32.c); UART4 RX uses channel 2. +#define BWM_DMA_CHANNEL DMA1_CHANNEL2 +#define BWM_DMA_MUX_CHANNEL DMA1MUX_CHANNEL2 -static void lookup_chipid_short(uint32_t iChipID, uint32_t mem_used, uint32_t flash_size) { - // AT32 (PM5): the chip id is an ARM DBGMCU IDCODE, not an Atmel CIDR, so the - // AT91 decode below does not apply (it would print "Unknown" and a bogus flash - // size). Report the MCU and use the real flash size the device reported. - if (IfPm5()) { - PrintAndLogEx(NORMAL, " MCU....... " _YELLOW_("%s"), "AT32F437"); - uint32_t mem_kb = flash_size / 1024; - PrintAndLogEx(NORMAL, " Memory.... " _YELLOW_("%u") " KB ( " _YELLOW_("%2.0f%%") " used )" - , mem_kb - , mem_kb == 0 ? 0.0f : (float)mem_used / (mem_kb * 1024) * 100 - ); +// Power-of-two so head/tail wrap with a mask. DMA target buffer. +// Must comfortably exceed one full forward frame or the DMA laps the reader. +// A frame is app_com(6) + NG(10) + up to PM3_CMD_DATA_SIZE data + CRC(2); at +// PM3_CMD_DATA_SIZE=4064 that is ~4082 bytes, so 4096 leaves ~14 bytes of slack +// and overruns the instant the consumer lags. 4x headroom on a 512K part. +#define BWM_RX_RING_SZ 16384 +static volatile uint8_t s_rx_ring[BWM_RX_RING_SZ]; +static volatile uint16_t s_rx_tail = 0; // software read cursor; head comes from DMA + +static volatile bool s_inited = false; +static volatile uint32_t s_cur_baud = BWM_UART_BAUD; + +// 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]. +static inline uint16_t bwm_uart_rx_head(void) { + return (uint16_t)((BWM_RX_RING_SZ - dma_data_number_get(BWM_DMA_CHANNEL)) + & (BWM_RX_RING_SZ - 1)); +} + +// Bring UART4 + circular RX DMA up at `baud`. Safe to call repeatedly: on a +// re-config it tears the channel down first, so the ring restarts empty (which +// also discards bytes straddling a baud switch). Mirrors the SSC RX setup in +// fpga_hw_at32.c. +static void bwm_uart_configure(uint32_t baud) { + crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE); + crm_periph_clock_enable(CRM_UART4_PERIPH_CLOCK, TRUE); + crm_periph_clock_enable(CRM_DMA1_PERIPH_CLOCK, TRUE); + + gpio_init_type gpio_init_struct; + gpio_default_para_init(&gpio_init_struct); + gpio_init_struct.gpio_mode = GPIO_MODE_MUX; + gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL; + gpio_init_struct.gpio_pull = GPIO_PULL_NONE; + gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER; + gpio_init_struct.gpio_pins = BWM_UART_TX_PIN | BWM_UART_RX_PIN; + gpio_init(BWM_UART_GPIO, &gpio_init_struct); + gpio_pin_mux_config(BWM_UART_GPIO, BWM_UART_TX_SRC, BWM_UART_MUX); + gpio_pin_mux_config(BWM_UART_GPIO, BWM_UART_RX_SRC, BWM_UART_MUX); + + // Quiesce before reconfiguring (matters on the re-config path). + dma_channel_enable(BWM_DMA_CHANNEL, FALSE); + usart_enable(BWM_UART, FALSE); + + usart_init(BWM_UART, baud, USART_DATA_8BITS, USART_STOP_1_BIT); + usart_parity_selection_config(BWM_UART, USART_PARITY_NONE); + usart_transmitter_enable(BWM_UART, TRUE); + usart_receiver_enable(BWM_UART, TRUE); + + // --- RX via circular DMA --- + s_rx_tail = 0; + + dma_reset(BWM_DMA_CHANNEL); + + dma_init_type dma_init_struct; + dma_default_para_init(&dma_init_struct); + dma_init_struct.buffer_size = BWM_RX_RING_SZ; + dma_init_struct.direction = DMA_DIR_PERIPHERAL_TO_MEMORY; + dma_init_struct.peripheral_base_addr = (uint32_t) & (BWM_UART->dt); + dma_init_struct.peripheral_inc_enable = FALSE; + dma_init_struct.memory_base_addr = (uint32_t)s_rx_ring; + dma_init_struct.memory_inc_enable = TRUE; + dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_BYTE; + dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_BYTE; + dma_init_struct.loop_mode_enable = TRUE; // circular: reloads at count 0 + // MEDIUM: single-byte requests, must not starve the SSC bulk channel (HIGH). + dma_init_struct.priority = DMA_PRIORITY_MEDIUM; + dma_init(BWM_DMA_CHANNEL, &dma_init_struct); + + dmamux_enable(DMA1, TRUE); + dmamux_init(BWM_DMA_MUX_CHANNEL, DMAMUX_DMAREQ_ID_UART4_RX); + + usart_dma_receiver_enable(BWM_UART, TRUE); + dma_channel_enable(BWM_DMA_CHANNEL, TRUE); + + usart_enable(BWM_UART, TRUE); + s_cur_baud = baud; +} + +void bwm_uart_init(void) { + if (s_inited) { return; } - - const char *asBuff; - switch (iChipID) { - case 0x270B0A40: - asBuff = "AT91SAM7S512 Rev A"; - break; - case 0x270B0A4E: - case 0x270B0A4F: - asBuff = "AT91SAM7S512 Rev B"; - break; - case 0x270D0940: - asBuff = "AT91SAM7S256 Rev A"; - break; - case 0x270B0941: - asBuff = "AT91SAM7S256 Rev B"; - break; - case 0x270B0942: - asBuff = "AT91SAM7S256 Rev C"; - break; - case 0x270B0943: - asBuff = "AT91SAM7S256 Rev D"; - break; - case 0x270C0740: - asBuff = "AT91SAM7S128 Rev A"; - break; - case 0x270A0741: - asBuff = "AT91SAM7S128 Rev B"; - break; - case 0x270A0742: - asBuff = "AT91SAM7S128 Rev C"; - break; - case 0x270A0743: - asBuff = "AT91SAM7S128 Rev D"; - break; - case 0x27090540: - asBuff = "AT91SAM7S64 Rev A"; - break; - case 0x27090543: - asBuff = "AT91SAM7S64 Rev B"; - break; - case 0x27090544: - asBuff = "AT91SAM7S64 Rev C"; - break; - case 0x27080342: - asBuff = "AT91SAM7S321 Rev A"; - break; - case 0x27080340: - asBuff = "AT91SAM7S32 Rev A"; - break; - case 0x27080341: - asBuff = "AT91SAM7S32 Rev B"; - break; - case 0x27050241: - asBuff = "AT9SAM7S161 Rev A"; - break; - case 0x27050240: - asBuff = "AT91SAM7S16 Rev A"; - break; - default: - asBuff = "Unknown"; - break; - } - PrintAndLogEx(NORMAL, " MCU....... " _YELLOW_("%s"), asBuff); - - uint32_t mem_avail = 0; - switch ((iChipID & 0xF00) >> 8) { - case 0: - mem_avail = 0; - break; - case 1: - mem_avail = 8; - break; - case 2: - mem_avail = 16; - break; - case 3: - mem_avail = 32; - break; - case 5: - mem_avail = 64; - break; - case 7: - mem_avail = 128; - break; - case 9: - mem_avail = 256; - break; - case 10: - mem_avail = 512; - break; - case 12: - mem_avail = 1024; - break; - case 14: - mem_avail = 2048; - break; - } - - PrintAndLogEx(NORMAL, " Memory.... " _YELLOW_("%u") " KB ( " _YELLOW_("%2.0f%%") " used )" - , mem_avail - , mem_avail == 0 ? 0.0f : (float)mem_used / (mem_avail * 1024) * 100 - ); + bwm_uart_configure(BWM_UART_BAUD); + s_inited = true; } -static void lookupChipID(uint32_t iChipID, uint32_t mem_used, uint32_t flash_size) { - const char *asBuff; - uint32_t mem_avail = 0; - PrintAndLogEx(NORMAL, "\n [ " _YELLOW_("Hardware") " ]"); - - // AT32 (PM5): the chip id is an ARM DBGMCU IDCODE, not an Atmel CIDR, so the - // verbose AT91 decode below does not apply. Print a short AT32 summary instead. - if (IfPm5()) { - PrintAndLogEx(NORMAL, " --= uC: AT32F437"); - uint32_t mem_kb = flash_size / 1024; - PrintAndLogEx(NORMAL, " --= Nonvolatile Program Memory Size: %u KB, Used: %u bytes (%2.0f%%)" - , mem_kb - , mem_used - , mem_kb == 0 ? 0.0f : (float)mem_used / (mem_kb * 1024) * 100 - ); +void bwm_uart_set_baud(uint32_t baud) { + if (baud == 0 || baud == s_cur_baud) { return; } - - switch (iChipID) { - case 0x270B0A40: - asBuff = "AT91SAM7S512 Rev A"; - break; - case 0x270B0A4E: - case 0x270B0A4F: - asBuff = "AT91SAM7S512 Rev B"; - break; - case 0x270D0940: - asBuff = "AT91SAM7S256 Rev A"; - break; - case 0x270B0941: - asBuff = "AT91SAM7S256 Rev B"; - break; - case 0x270B0942: - asBuff = "AT91SAM7S256 Rev C"; - break; - case 0x270B0943: - asBuff = "AT91SAM7S256 Rev D"; - break; - case 0x270C0740: - asBuff = "AT91SAM7S128 Rev A"; - break; - case 0x270A0741: - asBuff = "AT91SAM7S128 Rev B"; - break; - case 0x270A0742: - asBuff = "AT91SAM7S128 Rev C"; - break; - case 0x270A0743: - asBuff = "AT91SAM7S128 Rev D"; - break; - case 0x27090540: - asBuff = "AT91SAM7S64 Rev A"; - break; - case 0x27090543: - asBuff = "AT91SAM7S64 Rev B"; - break; - case 0x27090544: - asBuff = "AT91SAM7S64 Rev C"; - break; - case 0x27080342: - asBuff = "AT91SAM7S321 Rev A"; - break; - case 0x27080340: - asBuff = "AT91SAM7S32 Rev A"; - break; - case 0x27080341: - asBuff = "AT91SAM7S32 Rev B"; - break; - case 0x27050241: - asBuff = "AT9SAM7S161 Rev A"; - break; - case 0x27050240: - asBuff = "AT91SAM7S16 Rev A"; - break; - default: - asBuff = "Unknown"; - break; - } - PrintAndLogEx(NORMAL, " --= uC: " _YELLOW_("%s"), asBuff); - - switch ((iChipID & 0xE0) >> 5) { - case 1: - asBuff = "ARM946ES"; - break; - case 2: - asBuff = "ARM7TDMI"; - break; - case 4: - asBuff = "ARM920T"; - break; - case 5: - asBuff = "ARM926EJS"; - break; - default: - asBuff = "Unknown"; - break; - } - PrintAndLogEx(NORMAL, " --= Embedded Processor: %s", asBuff); - - switch ((iChipID & 0xF0000) >> 16) { - case 1: - asBuff = "1K bytes"; - break; - case 2: - asBuff = "2K bytes"; - break; - case 3: - asBuff = "6K bytes"; - break; - case 4: - asBuff = "112K bytes"; - break; - case 5: - asBuff = "4K bytes"; - break; - case 6: - asBuff = "80K bytes"; - break; - case 7: - asBuff = "160K bytes"; - break; - case 8: - asBuff = "8K bytes"; - break; - case 9: - asBuff = "16K bytes"; - break; - case 10: - asBuff = "32K bytes"; - break; - case 11: - asBuff = "64K bytes"; - break; - case 12: - asBuff = "128K bytes"; - break; - case 13: - asBuff = "256K bytes"; - break; - case 14: - asBuff = "96K bytes"; - break; - case 15: - asBuff = "512K bytes"; - break; - default: - asBuff = "Unknown"; - break; - } - PrintAndLogEx(NORMAL, " --= Internal SRAM size: %s", asBuff); - - switch ((iChipID & 0xFF00000) >> 20) { - case 0x19: - asBuff = "AT91SAM9xx Series"; - break; - case 0x29: - asBuff = "AT91SAM9XExx Series"; - break; - case 0x34: - asBuff = "AT91x34 Series"; - break; - case 0x37: - asBuff = "CAP7 Series"; - break; - case 0x39: - asBuff = "CAP9 Series"; - break; - case 0x3B: - asBuff = "CAP11 Series"; - break; - case 0x40: - asBuff = "AT91x40 Series"; - break; - case 0x42: - asBuff = "AT91x42 Series"; - break; - case 0x55: - asBuff = "AT91x55 Series"; - break; - case 0x60: - asBuff = "AT91SAM7Axx Series"; - break; - case 0x61: - asBuff = "AT91SAM7AQxx Series"; - break; - case 0x63: - asBuff = "AT91x63 Series"; - break; - case 0x70: - asBuff = "AT91SAM7Sxx Series"; - break; - case 0x71: - asBuff = "AT91SAM7XCxx Series"; - break; - case 0x72: - asBuff = "AT91SAM7SExx Series"; - break; - case 0x73: - asBuff = "AT91SAM7Lxx Series"; - break; - case 0x75: - asBuff = "AT91SAM7Xxx Series"; - break; - case 0x92: - asBuff = "AT91x92 Series"; - break; - case 0xF0: - asBuff = "AT75Cxx Series"; - break; - default: - asBuff = "Unknown"; - break; - } - PrintAndLogEx(NORMAL, " --= Architecture identifier: %s", asBuff); - - switch ((iChipID & 0x70000000) >> 28) { - case 0: - asBuff = "ROM"; - break; - case 1: - asBuff = "ROMless or on-chip Flash"; - break; - case 2: - asBuff = "Embedded flash memory"; - break; - case 3: - asBuff = "ROM and Embedded flash memory\nNVPSIZ is ROM size\nNVPSIZ2 is Flash size"; - break; - case 4: - asBuff = "SRAM emulating ROM"; - break; - default: - asBuff = "Unknown"; - break; - } - switch ((iChipID & 0xF00) >> 8) { - case 0: - mem_avail = 0; - break; - case 1: - mem_avail = 8; - break; - case 2: - mem_avail = 16; - break; - case 3: - mem_avail = 32; - break; - case 5: - mem_avail = 64; - break; - case 7: - mem_avail = 128; - break; - case 9: - mem_avail = 256; - break; - case 10: - mem_avail = 512; - break; - case 12: - mem_avail = 1024; - break; - case 14: - mem_avail = 2048; - break; - } - - PrintAndLogEx(NORMAL, " --= %s " _YELLOW_("%uK") " bytes ( " _YELLOW_("%2.0f%%") " used )" - , asBuff - , mem_avail - , mem_avail == 0 ? 0.0f : (float)mem_used / (mem_avail * 1024) * 100 - ); - - /* - switch ((iChipID & 0xF000) >> 12) { - case 0: - asBuff = "None"); - break; - case 1: - asBuff = "8K bytes"); - break; - case 2: - asBuff = "16K bytes"); - break; - case 3: - asBuff = "32K bytes"); - break; - case 5: - asBuff = "64K bytes"); - break; - case 7: - asBuff = "128K bytes"); - break; - case 9: - asBuff = "256K bytes"); - break; - case 10: - asBuff = "512K bytes"); - break; - case 12: - asBuff = "1024K bytes"); - break; - case 14: - asBuff = "2048K bytes"); - break; - } - PrintAndLogEx(NORMAL, " --= Second nonvolatile program memory size: %s", asBuff); - */ + bwm_uart_configure(baud); } -static int CmdDbg(const char *Cmd) { +uint32_t bwm_uart_get_baud(void) { + return s_cur_baud; +} - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw dbg", - "Set device side debug level output.\n" - "Note: option `-4`, this option may cause malfunction itself by\n" - "introducing delays in time critical functions like simulation or sniffing", - "hw dbg --> get current log level\n" - "hw dbg -1 --> set log level to _error_\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_lit0("0", NULL, "no debug messages"), - arg_lit0("1", NULL, "error messages"), - arg_lit0("2", NULL, "plus information messages"), - arg_lit0("3", NULL, "plus debug messages"), - arg_lit0("4", NULL, "print even debug messages in timing critical functions"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - bool lv0 = arg_get_lit(ctx, 1); - bool lv1 = arg_get_lit(ctx, 2); - bool lv2 = arg_get_lit(ctx, 3); - bool lv3 = arg_get_lit(ctx, 4); - bool lv4 = arg_get_lit(ctx, 5); - CLIParserFree(ctx); - - if ((lv0 + lv1 + lv2 + lv3 + lv4) > 1) { - PrintAndLogEx(INFO, "Can only set one debug level"); - return PM3_EINVARG; +int bwm_uart_write(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) { + } + usart_data_transmit(BWM_UART, data[i]); } - - uint8_t curr = DBG_NONE; - if (getDeviceDebugLevel(&curr) != PM3_SUCCESS) - return PM3_EFAILED; - - const char *dbglvlstr; - switch (curr) { - case DBG_NONE: - dbglvlstr = "none"; - break; - case DBG_ERROR: - dbglvlstr = "error"; - break; - case DBG_INFO: - dbglvlstr = "info"; - break; - case DBG_DEBUG: - dbglvlstr = "debug"; - break; - case DBG_EXTENDED: - dbglvlstr = "extended"; - break; - default: - dbglvlstr = "unknown"; - break; - } - PrintAndLogEx(INFO, " Current debug log level..... %d ( " _YELLOW_("%s") " )", curr, dbglvlstr); - - if ((lv0 + lv1 + lv2 + lv3 + lv4) == 1) { - uint8_t dbg = 0; - if (lv0) - dbg = 0; - else if (lv1) - dbg = 1; - else if (lv2) - dbg = 2; - else if (lv3) - dbg = 3; - else if (lv4) - dbg = 4; - - if (setDeviceDebugLevel(dbg, true) != PM3_SUCCESS) - return PM3_EFAILED; + while (usart_flag_get(BWM_UART, USART_TDC_FLAG) == RESET) { } return PM3_SUCCESS; } -static int CmdDetectReader(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw detectreader", - "Start to detect presences of reader field", - "hw detectreader\n" - "hw detectreader -L\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_lit0("L", "LF", "only detect low frequency 125/134 kHz"), - arg_lit0("H", "HF", "only detect high frequency 13.56 MHZ"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - bool lf = arg_get_lit(ctx, 1); - bool hf = arg_get_lit(ctx, 2); - CLIParserFree(ctx); - - // 0: Detect both frequency in mode 1 - // 1: LF_ONLY - // 2: HF_ONLY - uint8_t arg = 0; - if (lf == true && hf == false) { - arg = 1; - } else if (hf == true && lf == false) { - arg = 2; +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; } - - clearCommandBuffer(); - SendCommandNG(CMD_LISTEN_READER_FIELD, (uint8_t *)&arg, sizeof(arg)); - PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " or " _GREEN_("") " to change modes and exit"); - - for (;;) { - if (kbd_enter_pressed()) { - SendCommandNG(CMD_BREAK_LOOP, NULL, 0); - PrintAndLogEx(DEBUG, _GREEN_("") " pressed"); - } - - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_LISTEN_READER_FIELD, &resp, 1000)) { - if (resp.status != PM3_EOPABORTED) { - PrintAndLogEx(ERR, "Unexpected response: %d", resp.status); - } - break; - } - } - PrintAndLogEx(INFO, "Done!"); - return PM3_SUCCESS; + return (uint16_t)((bwm_uart_rx_head() - s_rx_tail) & (BWM_RX_RING_SZ - 1)); } -// ## FPGA Control -static int CmdFPGAOff(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw fpgaoff", - "Turn of fpga and antenna field", - "hw fpgaoff\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - - clearCommandBuffer(); - SendCommandNG(CMD_FPGA_MAJOR_MODE_OFF, NULL, 0); - return PM3_SUCCESS; -} - -static int CmdLCD(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw lcd", - "Send command/data to LCD", - "hw lcd -r AA -c 03 -> sends 0xAA three times" - ); - - void *argtable[] = { - arg_param_begin, - arg_int1("r", "raw", "", "data "), - arg_int1("c", "cnt", "", "number of times to send"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - - int r_len = 0; - uint8_t raw[1] = {0}; - CLIGetHexWithReturn(ctx, 1, raw, &r_len); - int j = arg_get_int_def(ctx, 2, 1); - CLIParserFree(ctx); - if (j < 1) { - PrintAndLogEx(WARNING, "Count must be larger than zero"); - return PM3_EINVARG; - } - - while (j--) { - clearCommandBuffer(); - lcd_cmd_t payload = { .cmd = raw[0] }; - SendCommandNG(CMD_LCD, (uint8_t *)&payload, sizeof(payload)); - } - return PM3_SUCCESS; -} - -static int CmdLCDReset(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw lcdreset", - "Hardware reset LCD", - "hw lcdreset\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - clearCommandBuffer(); - SendCommandNG(CMD_LCD_RESET, NULL, 0); - return PM3_SUCCESS; -} - -static int CmdReadmem(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw readmem", - "Reads processor flash memory into a file or views on console", - "hw readmem -f myfile -> save 512KB processor flash memory to file\n" - "hw readmem -a 8192 -l 512 -> display 512 bytes from offset 8192\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_u64_0("a", "adr", "", "flash address to start reading from"), - arg_u64_0("l", "len", "", "length (default 32 or 512KB)"), - arg_str0("f", "file", "", "save to file"), - arg_u64_0("c", "cols", "", "column breaks"), - arg_lit0("r", "raw", "use raw address mode: read from anywhere, not just flash"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, false); - - // check for -file option first to determine the output mode - int fnlen = 0; - char filename[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); - bool save_to_file = fnlen > 0; - - // default len to 512KB when saving to file, to 32 bytes when viewing on the console. - uint32_t default_len = save_to_file ? 512 * 1024 : 32; - - uint32_t address = arg_get_u32_def(ctx, 1, 0); - uint32_t len = arg_get_u32_def(ctx, 2, default_len); - int breaks = arg_get_int_def(ctx, 4, 32); - bool raw = arg_get_lit(ctx, 5); - CLIParserFree(ctx); - - uint8_t *buffer = calloc(len, sizeof(uint8_t)); - if (buffer == NULL) { - PrintAndLogEx(WARNING, "Failed to allocate memory"); - return PM3_EMALLOC; - } - - const char *flash_str = raw ? "" : " flash"; - PrintAndLogEx(INFO, "reading " _YELLOW_("%u") " bytes from processor%s memory", - len, flash_str); - - DeviceMemType_t type = raw ? MCU_MEM : MCU_FLASH; - if (!GetFromDevice(type, buffer, len, address, NULL, 0, NULL, -1, true)) { - PrintAndLogEx(FAILED, "ERROR; reading from MCU flash memory"); - free(buffer); - return PM3_EFLASH; - } - - if (save_to_file) { - saveFile(filename, ".bin", buffer, len); - } else { - PrintAndLogEx(INFO, "---- " _CYAN_("processor%s memory") " ----", flash_str); - print_hex_break(buffer, len, breaks); - } - - free(buffer); - return PM3_SUCCESS; -} - -static int CmdReset(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw reset", - "Reset the Proxmark3 device.", - "hw reset" - ); - - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - clearCommandBuffer(); - SendCommandNG(CMD_HARDWARE_RESET, NULL, 0); - PrintAndLogEx(INFO, "Proxmark3 has been reset."); - return PM3_SUCCESS; -} - -/* - * Sets the divisor for LF frequency clock: lets the user choose any LF frequency below - * 600kHz. - */ -static int CmdSetDivisor(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw setlfdivisor", - "Drive LF antenna at 12 MHz / (divisor + 1).", - "hw setlfdivisor -d 88" - ); - - void *argtable[] = { - arg_param_begin, - arg_u64_1("d", "div", "", "19 - 255 divisor value (def 95)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - uint8_t arg = arg_get_u32_def(ctx, 1, 95); - CLIParserFree(ctx); - - if (arg < 19) { - PrintAndLogEx(ERR, "Divisor must be between " _YELLOW_("19") " and " _YELLOW_("255")); - return PM3_EINVARG; - } - // 12 000 000 (12MHz) - clearCommandBuffer(); - SendCommandNG(CMD_LF_SET_DIVISOR, (uint8_t *)&arg, sizeof(arg)); - PrintAndLogEx(SUCCESS, "Divisor set, expected " _YELLOW_("%.1f") " kHz", ((double)12000 / (arg + 1))); - return PM3_SUCCESS; -} - -static int CmdSetHFThreshold(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw sethfthresh", - "Set thresholds in HF/14a and Legic mode.", - "hw sethfthresh -t 7 -i 20 -l 8" - ); - - void *argtable[] = { - arg_param_begin, - arg_int0("t", "thresh", "", "threshold, used in 14a reader mode (def 7)"), - arg_int0("i", "high", "", "high threshold, used in 14a sniff mode (def 20)"), - arg_int0("l", "legic", "", "threshold used in Legic mode (def 8)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - - struct { - uint8_t threshold; - uint8_t threshold_high; - uint8_t legic_threshold; - } PACKED params; - - params.threshold = arg_get_int_def(ctx, 1, 7); - params.threshold_high = arg_get_int_def(ctx, 2, 20); - params.legic_threshold = arg_get_int_def(ctx, 3, 8); - CLIParserFree(ctx); - - if ((params.threshold < 1) || (params.threshold > 63) || (params.threshold_high < 1) || (params.threshold_high > 63)) { - PrintAndLogEx(ERR, "Thresholds must be between " _YELLOW_("1") " and " _YELLOW_("63")); - return PM3_EINVARG; - } - - clearCommandBuffer(); - SendCommandNG(CMD_HF_ISO14443A_SET_THRESHOLDS, (uint8_t *)¶ms, sizeof(params)); - PrintAndLogEx(SUCCESS, "Thresholds set."); - return PM3_SUCCESS; -} - -static int CmdSetMux(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw setmux", - "Set the ADC mux to a specific value", - "hw setmux --hipkd -> set HIGH PEAK\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_lit0(NULL, "lopkd", "low peak"), - arg_lit0(NULL, "loraw", "low raw"), - arg_lit0(NULL, "hipkd", "high peak"), - arg_lit0(NULL, "hiraw", "high raw"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - bool lopkd = arg_get_lit(ctx, 1); - bool loraw = arg_get_lit(ctx, 2); - bool hipkd = arg_get_lit(ctx, 3); - bool hiraw = arg_get_lit(ctx, 4); - CLIParserFree(ctx); - - if ((lopkd + loraw + hipkd + hiraw) > 1) { - PrintAndLogEx(INFO, "Can only set one mux"); - return PM3_EINVARG; - } - -#ifdef WITH_FPC_USART - if (loraw || hiraw) { - PrintAndLogEx(INFO, "this ADC mux option is unavailable on RDV4 compiled with FPC USART"); - return PM3_EINVARG; - } -#endif - - uint8_t arg = 0; - if (lopkd) - arg = 0; - else if (loraw) - arg = 1; - else if (hipkd) - arg = 2; - else if (hiraw) - arg = 3; - - clearCommandBuffer(); - SendCommandNG(CMD_SET_ADC_MUX, (uint8_t *)&arg, sizeof(arg)); - return PM3_SUCCESS; -} - -static int CmdStandalone(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw standalone", - "Start standalone mode", - "hw standalone -> start \n" - "hw standalone -a 1 -> start and send arg 1" - ); - - void *argtable[] = { - arg_param_begin, - arg_u64_0("a", "arg", "", "argument byte"), - arg_str0("b", NULL, "", "UniSniff arg: 14a, 14b, 15, iclass"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - - struct p { - uint8_t arg; - uint8_t mlen; - uint8_t mode[10]; - } PACKED packet; - - packet.arg = arg_get_u32_def(ctx, 1, 1); - int mlen = 0; - CLIParamStrToBuf(arg_get_str(ctx, 2), packet.mode, sizeof(packet.mode), &mlen); - if (mlen) { - packet.mlen = mlen; - } - CLIParserFree(ctx); - clearCommandBuffer(); - SendCommandNG(CMD_STANDALONE, (uint8_t *)&packet, sizeof(struct p)); - return PM3_SUCCESS; -} - -static int CmdDecay(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw decay", - "Measure HF antenna decay after field-off.\n" - "Captures how quickly the peak-detect capacitor voltage drops\n" - "after the 13.56 MHz field is turned off. Different antenna loading\n" - "(unloaded, booster board, damaged) produces different decay profiles.", - "hw decay\n" - "hw decay --ms 100 --> stabilize for 100ms before measurement\n" - "hw decay --us 5000 --> measure 5ms decay window\n"); - - void *argtable[] = { - arg_param_begin, - arg_int0(NULL, "ms", "", "Field stabilization time in ms (default: 50)"), - arg_int0(NULL, "us", "", "Measurement window in us (default: 2000)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - - uint16_t stabilize_ms = arg_get_int_def(ctx, 1, 50); - uint16_t measure_us = arg_get_int_def(ctx, 2, 2000); - CLIParserFree(ctx); - - // Build parameter packet - hf_decay_params_t decay_params = { - .stabilize_ms = stabilize_ms, - .measure_us = measure_us, - }; - - PrintAndLogEx(INFO, "Measuring HF antenna decay..."); - PrintAndLogEx(INFO, " Field stabilization: " _YELLOW_("%d") " ms", stabilize_ms); - PrintAndLogEx(INFO, " Measurement window: " _YELLOW_("%d") " us", measure_us); - - clearCommandBuffer(); - SendCommandNG(CMD_HF_DECAY, (uint8_t *)&decay_params, sizeof(decay_params)); - - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_HF_DECAY, &resp, 5000) == false) { - PrintAndLogEx(WARNING, "Timeout waiting for decay measurement"); - return PM3_ETIMEOUT; - } - - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "Decay measurement failed"); - return PM3_ESOFT; - } - - // Parse response header - hf_decay_response_t *decay_resp = (hf_decay_response_t *)resp.data.asBytes; - uint16_t baseline_mv = decay_resp->baseline_mv; - uint16_t num_samples = decay_resp->num_samples; - uint16_t sample_interval_us = decay_resp->sample_interval_us; - uint16_t measure_window_us = decay_resp->measure_window_us; - uint16_t samples[num_samples]; - memcpy(samples, decay_resp->samples_mv, num_samples * sizeof(uint16_t)); - - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "-------- " _CYAN_("HF Decay Measurement") " ----------"); - PrintAndLogEx(SUCCESS, "Baseline (field on).... " _YELLOW_("%d") " mV (%.2f V)", - baseline_mv, baseline_mv / 1000.0); - PrintAndLogEx(SUCCESS, "Samples captured....... %d", num_samples); - PrintAndLogEx(SUCCESS, "Sample interval........ ~%d us", sample_interval_us); - PrintAndLogEx(SUCCESS, "Total window........... %d us", measure_window_us); - - if (num_samples == 0) { - PrintAndLogEx(WARNING, "No samples captured"); - return PM3_ESOFT; - } - - // Decay samples use fast ADC (reduced S&H) for ~5us/sample resolution. - // Absolute mV values are ~11% of truth due to RC charging limitation, - // but relative decay shape is accurate. Use first sample as 100% reference. - uint16_t ref_mv = samples[0]; - - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, " idx | time (us) | raw | %% of peak"); - PrintAndLogEx(INFO, "-----+-----------+-------+-------------"); - - for (uint16_t i = 0; i < num_samples; i++) { - uint32_t time_us = (num_samples > 1) - ? (uint32_t)i * measure_window_us / (num_samples - 1) - : 0; - double pct = (ref_mv > 0) - ? 100.0 * samples[i] / ref_mv - : 0; - PrintAndLogEx(INFO, " %3d | %7d | %5d | %.1f%%", - i, time_us, samples[i], pct); - } - - // Find time to 50% decay (relative to first sample) - uint16_t half_ref = ref_mv / 2; - int t_half_idx = -1; - for (uint16_t i = 0; i < num_samples; i++) { - if (samples[i] <= half_ref) { - t_half_idx = i; - break; - } - } - - PrintAndLogEx(NORMAL, ""); - if (t_half_idx >= 0) { - uint32_t t_half_us = (num_samples > 1) - ? (uint32_t)t_half_idx * measure_window_us / (num_samples - 1) - : 0; - PrintAndLogEx(SUCCESS, "Time to 50%% decay..... ~" _YELLOW_("%d") " us (sample %d)", t_half_us, t_half_idx); - } else { - PrintAndLogEx(INFO, "Voltage did not reach 50%% decay within measurement window"); - } - - uint16_t final_mv = samples[num_samples - 1]; - double final_pct = (ref_mv > 0) ? 100.0 * final_mv / ref_mv : 0; - PrintAndLogEx(SUCCESS, "Final voltage.......... %d raw (%.1f%% of peak)", final_mv, final_pct); - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "Note: decay samples use fast ADC (~5us/sample, relative values)"); - - // Load into graph window - for (uint16_t i = 0; i < num_samples; i++) { - g_GraphBuffer[i] = (int)samples[i]; - } - g_GraphTraceLen = num_samples; - ShowGraphWindow(); - RepaintGraphWindow(); - - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "Decay curve loaded into graph window (mV vs sample index)"); - PrintAndLogEx(NORMAL, ""); - - return PM3_SUCCESS; -} - -static int CmdTune(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw tune", - "Measure tuning of device antenna. Results shown in graph window.\n" - "This command doesn't actively tune your antennas, \n" - "it's only informative by measuring voltage that the antennas will generate", - "hw tune" - ); - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - -#define NON_VOLTAGE 1000 -#define LF_UNUSABLE_V 2000 -#define LF_MARGINAL_V 10000 -#define HF_UNUSABLE_V 3000 -#define HF_MARGINAL_V 5000 -#define ANTENNA_ERROR 1.00 // current algo has 3% error margin. - - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "-------- " _CYAN_("Reminder") " ----------------------------"); - PrintAndLogEx(INFO, "`" _YELLOW_("hw tune") "` doesn't actively tune your antennas."); - PrintAndLogEx(INFO, "It's only informative."); - PrintAndLogEx(INFO, "Measuring antenna characteristics..."); - - // hide demod plot line - g_DemodBufferLen = 0; - setClockGrid(0, 0); - RepaintGraphWindow(); - int timeout = 0; - int timeout_max = 20; - - clearCommandBuffer(); - SendCommandNG(CMD_MEASURE_ANTENNA_TUNING, NULL, 0); - PacketResponseNG resp; - PrintAndLogEx(INPLACE, "% 3i", timeout_max - timeout); - - while (WaitForResponseTimeout(CMD_MEASURE_ANTENNA_TUNING, &resp, 500) == false) { - - fflush(stdout); - if (timeout >= timeout_max) { - PrintAndLogEx(WARNING, "\nNo response from Proxmark3. Aborting..."); - return PM3_ETIMEOUT; - } - - timeout++; - PrintAndLogEx(INPLACE, "% 3i", timeout_max - timeout); - } - - PrintAndLogEx(NORMAL, ""); - - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(WARNING, "Antenna tuning failed"); - return PM3_ESOFT; - } - - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "-------- " _CYAN_("LF Antenna") " ----------"); - // in mVolt - struct p { - uint32_t v_lf134; - uint32_t v_lf125; - uint32_t v_lfconf; - uint32_t v_hf; - uint32_t peak_v; - uint32_t peak_f; - int divisor; - uint8_t results[256]; - } PACKED; - - struct p *package = (struct p *)resp.data.asBytes; - - if (package->v_lf125 > NON_VOLTAGE) - PrintAndLogEx(SUCCESS, "%.2f kHz ........... " _YELLOW_("%5.2f") " V", LF_DIV2FREQ(LF_DIVISOR_125), (package->v_lf125 * ANTENNA_ERROR) / 1000.0); - - if (package->v_lf134 > NON_VOLTAGE) - PrintAndLogEx(SUCCESS, "%.2f kHz ........... " _YELLOW_("%5.2f") " V", LF_DIV2FREQ(LF_DIVISOR_134), (package->v_lf134 * ANTENNA_ERROR) / 1000.0); - - if (package->v_lfconf > NON_VOLTAGE && package->divisor > 0 && package->divisor != LF_DIVISOR_125 && package->divisor != LF_DIVISOR_134) - PrintAndLogEx(SUCCESS, "%.2f kHz ........... " _YELLOW_("%5.2f") " V", LF_DIV2FREQ(package->divisor), (package->v_lfconf * ANTENNA_ERROR) / 1000.0); - - if (package->peak_v > NON_VOLTAGE && package->peak_f > 0) - PrintAndLogEx(SUCCESS, "%.2f kHz optimal.... " _BACK_GREEN_("%5.2f") " V", LF_DIV2FREQ(package->peak_f), (package->peak_v * ANTENNA_ERROR) / 1000.0); - - // Empirical measures in mV - const double vdd_rdv4 = 9000; - const double vdd_other = 5400; - double vdd = IfPm3Rdv4Fw() ? vdd_rdv4 : vdd_other; - - if (package->peak_v > NON_VOLTAGE && package->peak_f > 0) { - - // Q measure with Q=f/delta_f - double v_3db_scaled = (double)(package->peak_v * 0.707) / 512; // /512 == >>9 - uint32_t s2 = 0, s4 = 0; - for (int i = 1; i < 256; i++) { - if ((s2 == 0) && (package->results[i] > v_3db_scaled)) { - s2 = i; - } - if ((s2 != 0) && (package->results[i] < v_3db_scaled)) { - s4 = i; - break; - } - } - - PrintAndLogEx(SUCCESS, ""); - PrintAndLogEx(SUCCESS, "Approx. Q factor measurement"); - double lfq1 = 0; - if (s4 != 0) { - // we got all our points of interest - double a = package->results[s2 - 1]; - double b = package->results[s2]; - double f1 = LF_DIV2FREQ(s2 - 1 + (v_3db_scaled - a) / (b - a)); - double c = package->results[s4 - 1]; - double d = package->results[s4]; - double f2 = LF_DIV2FREQ(s4 - 1 + (c - v_3db_scaled) / (c - d)); - lfq1 = LF_DIV2FREQ(package->peak_f) / (f1 - f2); - PrintAndLogEx(SUCCESS, "Frequency bandwidth... " _YELLOW_("%.1lf"), lfq1); - } - - // Q measure with Vlr=Q*(2*Vdd/pi) - double lfq2 = (double)package->peak_v * 3.14 / 2 / vdd; - PrintAndLogEx(SUCCESS, "Peak voltage.......... " _YELLOW_("%.1lf"), lfq2); - // cross-check results - // TODO DXL pm5 to be covered - if (IfPm5() == false) { - if (lfq1 > 3) { - double approx_vdd = (double)package->peak_v * 3.14 / 2 / lfq1; - // Got 8858 on a RDV4 with large antenna 134/14 - // Got 8761 on a non-RDV4 - const double approx_vdd_other_max = 8840; - - // 1% over threshold and supposedly non-RDV4 - if ((approx_vdd > approx_vdd_other_max * 1.01) && (!IfPm3Rdv4Fw())) { - PrintAndLogEx(WARNING, "Contradicting measures seem to indicate you're running a " _YELLOW_("PM3GENERIC firmware on a RDV4")); - PrintAndLogEx(WARNING, "False positives is possible but please check your setup"); - } - // 1% below threshold and supposedly RDV4 - if ((approx_vdd < approx_vdd_other_max * 0.99) && (IfPm3Rdv4Fw())) { - PrintAndLogEx(WARNING, "Contradicting measures seem to indicate you're running a " _YELLOW_("PM3_RDV4 firmware on a generic device")); - PrintAndLogEx(WARNING, "False positives is possible but please check your setup"); - } - } - } - } - - char judgement[20]; - memset(judgement, 0, sizeof(judgement)); - // LF evaluation - if (package->peak_v < LF_UNUSABLE_V) - snprintf(judgement, sizeof(judgement), _RED_("unusable")); - else if (package->peak_v < LF_MARGINAL_V) - snprintf(judgement, sizeof(judgement), _YELLOW_("marginal")); - else - snprintf(judgement, sizeof(judgement), _GREEN_("ok")); - - // PrintAndLogEx((package->peak_v < LF_UNUSABLE_V) ? WARNING : SUCCESS, "LF antenna ( %s )", judgement); - PrintAndLogEx((package->peak_v < LF_UNUSABLE_V) ? WARNING : SUCCESS, "LF antenna............ %s", judgement); - - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "-------- " _CYAN_("HF Antenna") " ----------"); - // HF evaluation - if (package->v_hf > NON_VOLTAGE) { - PrintAndLogEx(SUCCESS, "13.56 MHz............. " _BACK_GREEN_("%5.2f") " V", (package->v_hf * ANTENNA_ERROR) / 1000.0); - } - - memset(judgement, 0, sizeof(judgement)); - - // If HF is unusable or marginal, run a quick decay measurement to check - // for booster board. With a booster, the first fast-ADC decay sample reads - // 50-500 (rapid discharge). Without a booster, it reads >1000. - bool hf_booster_detected = false; - if (!IfPm3Rdv4Fw() && package->v_hf < HF_MARGINAL_V) { - hf_decay_params_t decay_params = { - .stabilize_ms = 50, - .measure_us = 50, - }; - - clearCommandBuffer(); - SendCommandNG(CMD_HF_DECAY, (uint8_t *)&decay_params, sizeof(decay_params)); - - if (WaitForResponseTimeout(CMD_HF_DECAY, &resp, 3000) && resp.status == PM3_SUCCESS) { - hf_decay_response_t *decay_resp = (hf_decay_response_t *)resp.data.asBytes; - if (decay_resp->num_samples > 0) { - uint16_t samples[1]; - memcpy(samples, decay_resp->samples_mv, sizeof(uint16_t)); - if (samples[0] >= 50 && samples[0] <= 500) { - hf_booster_detected = true; - } - } - } - } - - if (hf_booster_detected) { - PrintAndLogEx(SUCCESS, ""); - PrintAndLogEx(SUCCESS, "Your HF antenna measurement shows"); - PrintAndLogEx(SUCCESS, "low voltage that is consistent"); - PrintAndLogEx(SUCCESS, "with the installation of a booster"); - PrintAndLogEx(SUCCESS, "board. If you do not have a"); - PrintAndLogEx(SUCCESS, "booster board installed, either"); - PrintAndLogEx(SUCCESS, "your antenna is malfunctioning or"); - PrintAndLogEx(SUCCESS, "you have a tag on the HF antenna."); - } - - PrintAndLogEx(SUCCESS, ""); - PrintAndLogEx(SUCCESS, "Approx. Q factor measurement"); - - if (package->v_hf >= HF_UNUSABLE_V) { - // Q measure with Vlr=Q*(2*Vdd/pi) - double hfq = (double)package->v_hf * 3.14 / 2 / vdd; - PrintAndLogEx(SUCCESS, "Peak voltage.......... " _YELLOW_("%.1lf"), hfq); - } - - if (package->v_hf < HF_UNUSABLE_V) - snprintf(judgement, sizeof(judgement), _RED_("unusable")); - else if (package->v_hf < HF_MARGINAL_V) - snprintf(judgement, sizeof(judgement), _YELLOW_("marginal")); - else - snprintf(judgement, sizeof(judgement), _GREEN_("ok")); - - PrintAndLogEx((package->v_hf < HF_UNUSABLE_V) ? WARNING : SUCCESS, "HF antenna ( %s )", judgement); - - // If HF voltage is ok/marginal but below 13V, check for - // surface interference via decay measurement. - // Only on PM3 Easy — RDV4 has different voltage divider. - if (!IfPm3Rdv4Fw() && package->v_hf >= HF_MARGINAL_V && package->v_hf < 13000) { - hf_decay_params_t surface_params = { - .stabilize_ms = 50, - .measure_us = 50, - }; - - clearCommandBuffer(); - SendCommandNG(CMD_HF_DECAY, (uint8_t *)&surface_params, sizeof(surface_params)); - - if (WaitForResponseTimeout(CMD_HF_DECAY, &resp, 3000) && resp.status == PM3_SUCCESS) { - hf_decay_response_t *surface_resp = (hf_decay_response_t *)resp.data.asBytes; - if (surface_resp->num_samples > 0) { - uint16_t samples[1]; - memcpy(samples, surface_resp->samples_mv, sizeof(uint16_t)); - if (samples[0] >= 600 && samples[0] <= 900) { - PrintAndLogEx(SUCCESS, ""); - PrintAndLogEx(SUCCESS, "The surface your proxmark is on could"); - PrintAndLogEx(SUCCESS, "contain interfering materials. Try again"); - PrintAndLogEx(SUCCESS, "while holding the proxmark in free space."); - } - } - } - } - - // graph LF measurements - // even here, these values has 3% error. - uint16_t test1 = 0; - for (int i = 0; i < 256; i++) { - g_GraphBuffer[i] = package->results[i] - 128; - test1 += package->results[i]; - } - - if (test1 > 0) { - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "-------- " _CYAN_("LF tuning graph") " ------------"); - PrintAndLogEx(SUCCESS, "Orange line - divisor %d / %.2f kHz" - , LF_DIVISOR_125 - , LF_DIV2FREQ(LF_DIVISOR_125) - ); - PrintAndLogEx(SUCCESS, "Blue line - divisor %d / %.2f kHz\n\n" - , LF_DIVISOR_134 - , LF_DIV2FREQ(LF_DIVISOR_134) - ); - g_GraphTraceLen = 256; - g_MarkerC.pos = LF_DIVISOR_125; - g_MarkerD.pos = LF_DIVISOR_134; - ShowGraphWindow(); - RepaintGraphWindow(); - } else { - PrintAndLogEx(FAILED, "\nAll values are zero. Not showing LF tuning graph\n\n"); - } - - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(INFO, "Q factor must be measured without tag on the antenna"); - PrintAndLogEx(NORMAL, ""); - return PM3_SUCCESS; -} - -static int CmdVersion(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw version", - "Show version information about the client and the connected Proxmark3", - "hw version" - ); - - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - pm3_version(true, false); - return PM3_SUCCESS; -} - -static int CmdStatus(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw status", - "Show runtime status information about the connected Proxmark3", - "hw status\n" - "hw status --ms 1000 -> Test connection speed with 1000ms timeout\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_int0("m", "ms", "", "speed test timeout in micro seconds"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - int32_t speedTestTimeout = arg_get_int_def(ctx, 1, -1); - CLIParserFree(ctx); - - clearCommandBuffer(); - PacketResponseNG resp; - if (speedTestTimeout < 0) { - speedTestTimeout = 0; - SendCommandNG(CMD_STATUS, NULL, 0); - } else { - SendCommandNG(CMD_STATUS, (uint8_t *)&speedTestTimeout, sizeof(speedTestTimeout)); - } - - if (WaitForResponseTimeout(CMD_STATUS, &resp, 2000 + speedTestTimeout) == false) { - PrintAndLogEx(WARNING, "Status command timeout. Communication speed test timed out"); - return PM3_ETIMEOUT; - } - return PM3_SUCCESS; -} - -int handle_tearoff(tearoff_params_t *params, bool verbose) { - - if (params == NULL) - return PM3_EINVARG; - - clearCommandBuffer(); - SendCommandNG(CMD_SET_TEAROFF, (uint8_t *)params, sizeof(tearoff_params_t)); - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_SET_TEAROFF, &resp, 500) == false) { - PrintAndLogEx(WARNING, "Tear-off command timeout."); - return PM3_ETIMEOUT; - } - - if (resp.status == PM3_SUCCESS) { - if (params->delay_us > 0 && verbose) - PrintAndLogEx(INFO, "Tear-off hook configured with delay of " _GREEN_("%i us"), params->delay_us); - - if (params->skip > 0 && verbose) - PrintAndLogEx(INFO, "Tear-off hook will be skipped " _YELLOW_("%i times") " before being activated", params->skip); - if (params->skip == 0 && verbose) - PrintAndLogEx(INFO, "Tear-off hook skipping " _GREEN_("disabled")); - - if (params->on && verbose) - PrintAndLogEx(INFO, "Tear-off hook " _GREEN_("enabled")); - - if (params->off && verbose) - PrintAndLogEx(INFO, "Tear-off hook " _RED_("disabled")); - } else if (verbose) - PrintAndLogEx(WARNING, "Tear-off command failed."); - return resp.status; -} - -static int CmdTearoff(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw tearoff", - "Configure a tear-off hook for the next write command supporting tear-off\n" - "After having been triggered by a write command, the tear-off hook is deactivated\n" - "Delay (in us) must be between 1 and 65535 (65ms). Precision is about 1/3us.", - "hw tearoff --delay 1200 --> define delay of 1200us\n" - "hw tearoff --on --> (re)activate a previously defined delay\n" - "hw tearoff --off --> deactivate a previously activated but not yet triggered hook\n" - "hw tearoff --list --> list commands implementing tear-off hooks\n"); - - void *argtable[] = { - arg_param_begin, - arg_int0(NULL, "delay", "", "Delay in us before triggering tear-off, must be between 1 and 65535"), - arg_lit0(NULL, "on", "Activate tear-off hook"), - arg_lit0(NULL, "off", "Deactivate tear-off hook"), - arg_int0(NULL, "skip", "", "Skip N triggers before activating the hook"), - arg_lit0("s", "silent", "less verbose output"), - arg_lit0(NULL, "list", "List commands implementing tear-off hooks"), - arg_param_end - }; - - CLIExecWithReturn(ctx, Cmd, argtable, false); - tearoff_params_t params; - int delay = arg_get_int_def(ctx, 1, -1); - params.on = arg_get_lit(ctx, 2); - params.off = arg_get_lit(ctx, 3); - int skip = arg_get_int_def(ctx, 4, -1); - bool silent = arg_get_lit(ctx, 5); - bool list = arg_get_lit(ctx, 6); - CLIParserFree(ctx); - - if (list) { - PrintAndLogEx(INFO, "Commands implementing tear-off hooks:"); - PrintAndLogEx(INFO, " hf 14a raw"); - PrintAndLogEx(INFO, " hf 14b apdu"); - PrintAndLogEx(INFO, " hf 14b raw"); - PrintAndLogEx(INFO, " hf 15 raw"); - PrintAndLogEx(INFO, " hf iclass creditepurse"); - PrintAndLogEx(INFO, " hf iclass wrbl"); - PrintAndLogEx(INFO, " hf mf wrbl"); - PrintAndLogEx(INFO, " hf mfu wrbl (with --skip 3)"); - PrintAndLogEx(INFO, " hf topaz wrbl"); - PrintAndLogEx(INFO, " lf em 4x05 write"); - PrintAndLogEx(INFO, " lf em 4x50 wrbl"); - PrintAndLogEx(INFO, " lf em 4x50 wrpwd"); - PrintAndLogEx(INFO, " lf hitag wrbl"); - PrintAndLogEx(INFO, " lf hitag hts wrbl"); - PrintAndLogEx(INFO, ""); - PrintAndLogEx(INFO, "See also commands implementing tearing-off on their own:"); - PrintAndLogEx(INFO, " lf em 4x05_unlock"); - PrintAndLogEx(INFO, " lf t55xx dangerraw"); - PrintAndLogEx(INFO, " hf iclass tear"); - PrintAndLogEx(INFO, " hf iclass blacktears"); - PrintAndLogEx(INFO, " hf mfu otptear"); - PrintAndLogEx(INFO, " Standalone mode HF_ST25_TEAROFF"); - return PM3_SUCCESS; - } - - if (delay != -1) { - // 65535 is where tearoff_params_t.delay_us runs out, not where the - // timer does. The old 43000 was the point past which the PWM tick - // count wrapped into 16 bits and the delay silently came up short. - if ((delay < 1) || (delay > 65535)) { - PrintAndLogEx(WARNING, "You can't set delay out of 1..65535 range!"); - return PM3_EINVARG; - } - } else { - delay = 0; // will be ignored by ARM - } - - params.delay_us = delay; - - if (skip != -1) { - if ((skip < 0) || (skip > 127)) { - PrintAndLogEx(WARNING, "You can't set skip out of 0..127 range!"); - return PM3_EINVARG; - } - } - - params.skip = skip; - - if (params.on && params.off) { - PrintAndLogEx(WARNING, "You can't set both --on and --off!"); - return PM3_EINVARG; - } - - return handle_tearoff(¶ms, !silent); -} - -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"); - - 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"); - return PM3_EINVARG; - } - uint8_t payload = off ? 0 : 1; // default (neither flag) = enable - - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_AUTOOFF, &payload, sizeof(payload)); - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_PM5_BWM_AUTOOFF, &resp, 2500) == false) { - PrintAndLogEx(WARNING, "command timeout (is this a PM5?)"); - return PM3_ETIMEOUT; - } - if (resp.status == PM3_ENOTIMPL) { - PrintAndLogEx(WARNING, "firmware built without auto power-off support"); - return resp.status; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "failed to set auto power-off"); - return resp.status; - } - PrintAndLogEx(SUCCESS, "Auto power-off %s.", payload ? _GREEN_("enabled") : _YELLOW_("disabled")); - return PM3_SUCCESS; -} - -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"); - - 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) { - uint8_t q[1] = { BWM_WIFI_ACTION_STATUS }; - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_WIFI, q, sizeof(q)); - PacketResponseNG r; - if (WaitForResponseTimeout(CMD_PM5_BWM_WIFI, &r, 5000) == false) { - PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); - return PM3_ETIMEOUT; - } - if (r.status != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "could not query BWM WiFi status (BWM present?)"); - return r.status; - } - uint8_t state = (r.length >= 1) ? r.data.asBytes[0] : 0xFF; - uint32_t ip = 0; - if (r.length >= 5) { - ip = r.data.asBytes[1] | (r.data.asBytes[2] << 8) | (r.data.asBytes[3] << 16) | ((uint32_t)r.data.asBytes[4] << 24); - } - switch (state) { - case 0xFF: - PrintAndLogEx(INFO, "BWM WiFi disabled (BLE-only). Bring it up with " _YELLOW_("hw bwmwifi --ssid --pwd ")); - break; - case 2: // connected - if (ip) { - PrintAndLogEx(SUCCESS, "BWM WiFi connected, IP " _YELLOW_("%u.%u.%u.%u"), - ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); - PrintAndLogEx(HINT, "Connect with: " _YELLOW_("pm3 -p tcp:%u.%u.%u.%u:"), - ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); - } else { - PrintAndLogEx(INFO, "BWM WiFi associated, waiting for a DHCP lease..."); - } - break; - case 1: // connecting - PrintAndLogEx(INFO, "BWM WiFi connecting..."); - break; - case 3: // reconnect wait - PrintAndLogEx(INFO, "BWM WiFi reconnecting..."); - break; - case 4: // task stopped - PrintAndLogEx(INFO, "BWM WiFi connect task stopped"); - break; - case 0: // disconnected - default: - PrintAndLogEx(INFO, "BWM WiFi configured but not connected"); - break; - } - return PM3_SUCCESS; - } - - if (stop) { - uint8_t off[1] = { BWM_WIFI_ACTION_STOP }; - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_WIFI, off, sizeof(off)); - PacketResponseNG r; - if (WaitForResponseTimeout(CMD_PM5_BWM_WIFI, &r, 5000) == false) { - PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); - return PM3_ETIMEOUT; - } - if (r.status != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "failed to disable BWM WiFi"); - return r.status; - } - PrintAndLogEx(SUCCESS, "BWM WiFi disabled (back to BLE-only)"); - return PM3_SUCCESS; - } - - if (ssid_len == 0) { - PrintAndLogEx(FAILED, "an SSID is required (or use --stop to tear down)"); - return PM3_EINVARG; - } - if (port < 1 || port > 65535) { - PrintAndLogEx(FAILED, "port must be 1..65535"); - return PM3_EINVARG; - } - - // payload: [action:u8][port:u16 LE][ssid\0][pwd\0][hostname\0] - uint8_t data[200] = {0}; - int n = 0; - data[n++] = BWM_WIFI_ACTION_START; - data[n++] = (uint8_t)(port & 0xFF); - data[n++] = (uint8_t)((port >> 8) & 0xFF); - memcpy(&data[n], ssid, ssid_len); - n += ssid_len; - data[n++] = 0; - memcpy(&data[n], pwd, pwd_len); - n += pwd_len; - data[n++] = 0; - memcpy(&data[n], host, host_len); - n += host_len; - data[n++] = 0; - - PrintAndLogEx(INFO, "Bringing up BWM WiFi (SSID \"%s\", port %d)... this can take ~15s", ssid, port); - - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_WIFI, data, n); - PacketResponseNG resp; - // ARM blocks during join + DHCP wait, so allow a long client timeout - if (WaitForResponseTimeout(CMD_PM5_BWM_WIFI, &resp, 60000) == false) { - PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); - return PM3_ETIMEOUT; - } - 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")); - return resp.status; - } - - uint32_t ip = resp.data.asDwords[0]; - PrintAndLogEx(SUCCESS, "BWM on WiFi at %u.%u.%u.%u", - ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); - PrintAndLogEx(HINT, "Connect with: " _YELLOW_("pm3 -p tcp:%u.%u.%u.%u:%d"), - ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF, port); - PrintAndLogEx(HINT, "Or by name (router-dependent): " _YELLOW_("pm3 -p tcp:%s:%d"), host, port); - return PM3_SUCCESS; -} - -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"); - - 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"); - return PM3_EINVARG; - } - uint8_t payload = off ? 0 : 1; // default (neither flag) = enable - PrintAndLogEx(INFO, "%s BWM battery charging...", off ? "Disabling" : "Enabling"); - - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_CHARGE_EN, &payload, sizeof(payload)); - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_PM5_BWM_CHARGE_EN, &resp, 2500) == false) { - PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); - return PM3_ETIMEOUT; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "charger did not respond (check BWM present)"); - return resp.status; - } - PrintAndLogEx(SUCCESS, "Charging %s. Verify with " _YELLOW_("hw status") ".", - off ? "disabled" : "enabled"); - if (off == false) { - PrintAndLogEx(HINT, "Reverts on the charger watchdog (~160 s) if not serviced."); - } - return PM3_SUCCESS; -} - -static int CmdBwmVchg(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw bwmvchg", - "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"); - - void *argtable[] = { - arg_param_begin, - arg_int0(NULL, "mv", "", "charge voltage in mV (default 4100, clamped 3600..4200)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - int mv = arg_get_int_def(ctx, 1, 4100); - CLIParserFree(ctx); - - if (mv < 3600 || mv > 4200) { - PrintAndLogEx(WARNING, "charge voltage out of range (3600..4200 mV): %d", mv); - return PM3_EINVARG; - } - - uint8_t payload[2] = { (uint8_t)(mv & 0xFF), (uint8_t)((mv >> 8) & 0xFF) }; - PrintAndLogEx(INFO, "Setting BWM charge voltage to " _YELLOW_("%d mV") "...", mv); - - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_SET_VCHG, payload, sizeof(payload)); - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_PM5_BWM_SET_VCHG, &resp, 5000) == false) { - PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); - return PM3_ETIMEOUT; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "failed to set charge voltage - check BWM present"); - return resp.status; - } - uint16_t applied = (resp.length >= 2) ? (resp.data.asBytes[0] | (resp.data.asBytes[1] << 8)) : 0; - PrintAndLogEx(SUCCESS, "Charge voltage set to " _YELLOW_("%u.%03u V") " (nearest 15 mV step).", applied / 1000, applied % 1000); - return PM3_SUCCESS; -} - -static int CmdBwmSetCap(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw bwmsetcap", - "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"); - - void *argtable[] = { - arg_param_begin, - arg_int0(NULL, "cap", "", "design capacity in mAh (default 500)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - int cap = arg_get_int_def(ctx, 1, 500); - CLIParserFree(ctx); - - if (cap <= 0 || cap > 32000) { - PrintAndLogEx(WARNING, "capacity out of range: %d mAh", cap); - return PM3_EINVARG; - } - - uint8_t payload[2] = { (uint8_t)(cap & 0xFF), (uint8_t)((cap >> 8) & 0xFF) }; - PrintAndLogEx(INFO, "Programming BWM gauge design capacity to " _YELLOW_("%d mAh") "...", cap); - PrintAndLogEx(INFO, "Run this " _YELLOW_("once") "; then perform a full charge/discharge learning cycle."); - - clearCommandBuffer(); - SendCommandNG(CMD_PM5_BWM_SET_CAP, payload, sizeof(payload)); - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_PM5_BWM_SET_CAP, &resp, 5000) == false) { - PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); - return PM3_ETIMEOUT; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(FAILED, "gauge provisioning failed - check BWM present and gauge unsealed"); - return resp.status; - } - PrintAndLogEx(SUCCESS, "Design capacity programmed. `hw status` should now report sane capacity."); - return PM3_SUCCESS; -} - -static int CmdTia(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw tia", - "Trigger a Timing Interval Acquisition to re-adjust the RealTimeCounter divider", - "hw tia" - ); - - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - - PrintAndLogEx(INFO, "Triggering new Timing Interval Acquisition (TIA)..."); - clearCommandBuffer(); - SendCommandNG(CMD_TIA, NULL, 0); - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_TIA, &resp, 2000) == false) { - PrintAndLogEx(WARNING, "TIA command timeout. You probably need to unplug the Proxmark3."); - return PM3_ETIMEOUT; - } - PrintAndLogEx(INFO, "TIA done."); - return PM3_SUCCESS; -} - -static int CmdTimeout(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw timeout", - "Set the communication timeout on the client side", - "hw timeout --> Show current timeout\n" - "hw timeout -m 20 --> Set the timeout to 20ms\n" - "hw timeout --ms 500 --> Set the timeout to 500ms\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_int0("m", "ms", "", "timeout in micro seconds"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - int32_t arg = arg_get_int_def(ctx, 1, -1); - CLIParserFree(ctx); - - uint32_t oldTimeout = uart_get_timeouts(); - - // timeout is not given/invalid, just show the current timeout then return - if (arg < 0) { - PrintAndLogEx(INFO, "Current communication timeout... " _GREEN_("%u") " ms", oldTimeout); - return PM3_SUCCESS; - } - - uint32_t newTimeout = arg; - // UART_USB_CLIENT_RX_TIMEOUT_MS is considered as the minimum required timeout. - if (newTimeout < UART_USB_CLIENT_RX_TIMEOUT_MS) { - PrintAndLogEx(WARNING, "Timeout less than %u ms might cause errors.", UART_USB_CLIENT_RX_TIMEOUT_MS); - } else if (newTimeout > 5000) { - PrintAndLogEx(WARNING, "Timeout greater than 5000 ms makes the client unresponsive."); - } - uart_reconfigure_timeouts(newTimeout); - PrintAndLogEx(INFO, "Old communication timeout... %u ms", oldTimeout); - PrintAndLogEx(INFO, "New communication timeout... " _GREEN_("%u") " ms", newTimeout); - return PM3_SUCCESS; -} - -static int CmdPing(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw ping", - "Test if the Proxmark3 is responsive", - "hw ping\n" - "hw ping --len 32" - ); - - void *argtable[] = { - arg_param_begin, - arg_u64_0("l", "len", "", "length of payload to send"), - arg_param_end - }; - - CLIExecWithReturn(ctx, Cmd, argtable, true); - uint32_t len = arg_get_u32_def(ctx, 1, 32); - CLIParserFree(ctx); - - if (len > PM3_CMD_DATA_SIZE) - len = PM3_CMD_DATA_SIZE; - - if (len) { - PrintAndLogEx(INFO, "Ping sent with payload len... " _YELLOW_("%d"), len); - } else { - PrintAndLogEx(INFO, "Ping sent"); - } - - clearCommandBuffer(); - PacketResponseNG resp; - uint8_t data[PM3_CMD_DATA_SIZE] = {0}; - - for (uint16_t i = 0; i < len; i++) { - data[i] = i & 0xFF; - } - - uint64_t tms = msclock(); - SendCommandNG(CMD_PING, data, len); - if (WaitForResponseTimeout(CMD_PING, &resp, 1000)) { - tms = msclock() - tms; - if (len) { - bool error = (memcmp(data, resp.data.asBytes, len) != 0); - PrintAndLogEx((error) ? ERR : SUCCESS, "Ping response " _GREEN_("received") - " in " _YELLOW_("%" PRIu64) " ms and content ( %s )", - tms, error ? _RED_("fail") : _GREEN_("ok")); - } else { - PrintAndLogEx(SUCCESS, "Ping response " _GREEN_("received") - " in " _YELLOW_("%" PRIu64) " ms", tms); - } - } else - PrintAndLogEx(WARNING, "Ping response " _RED_("timeout")); - return PM3_SUCCESS; -} - -static int CmdConnect(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw connect", - "Connects to a Proxmark3 device via specified serial port.\n" - "Baudrate here is only for physical UART or UART-BT, NOT for USB-CDC or blue shark add-on", - "hw connect -p " SERIAL_PORT_EXAMPLE_H "\n" - "hw connect -p "SERIAL_PORT_EXAMPLE_H" -b 115200" - ); - - void *argtable[] = { - arg_param_begin, - arg_str0("p", "port", "", "Serial port to connect to, else retry the last used one"), - arg_u64_0("b", "baud", "", "Baudrate"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - - char port[FILE_PATH_SIZE] = {0}; - int p_len = sizeof(port) - 1; // CLIGetStrWithReturn does not guarantee string to be null-terminated; - CLIGetStrWithReturn(ctx, 1, (uint8_t *)port, &p_len); - uint32_t baudrate = arg_get_u32_def(ctx, 2, USART_BAUD_RATE); - CLIParserFree(ctx); - - if (baudrate == 0) { - PrintAndLogEx(WARNING, "Baudrate can't be zero"); - return PM3_EINVARG; - } - - // default back to previous used serial port - if (strlen(port) == 0) { - if (strlen(g_conn.serial_port_name) == 0) { - PrintAndLogEx(WARNING, "Must specify a serial port"); - return PM3_EINVARG; - } - memcpy(port, g_conn.serial_port_name, sizeof(port)); - } - - if (g_session.pm3_present) { - CloseProxmark(g_session.current_device); - } - - // 10 second timeout - OpenProxmark(&g_session.current_device, port, false, 10, false, baudrate); - - if (g_session.pm3_present && (TestProxmark(g_session.current_device) != PM3_SUCCESS)) { - PrintAndLogEx(ERR, _RED_("ERROR:") " cannot communicate with the Proxmark3\n"); - CloseProxmark(g_session.current_device); - return PM3_ENOTTY; - } - return PM3_SUCCESS; -} - -static int CmdBreak(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw break", - "send break loop package", - "hw break\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - clearCommandBuffer(); - SendCommandNG(CMD_BREAK_LOOP, NULL, 0); - return PM3_SUCCESS; -} - -static int CmdBootloader(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw bootloader", - "Reboot Proxmark3 into bootloader mode", - "hw bootloader\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - CLIParserFree(ctx); - clearCommandBuffer(); - flash_reboot_bootloader(g_conn.serial_port_name, false); - return PM3_SUCCESS; -} - -int set_fpga_mode(uint8_t mode) { - if (mode < FPGA_BITSTREAM_MIN || mode > FPGA_BITSTREAM_MAX) { - return PM3_EINVARG; - } - uint8_t d[] = {mode}; - clearCommandBuffer(); - SendCommandNG(CMD_SET_FPGAMODE, d, sizeof(d)); - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_SET_FPGAMODE, &resp, 1000) == false) { - PrintAndLogEx(WARNING, "command execution time out"); - return PM3_ETIMEOUT; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(ERR, "failed to set FPGA mode"); - } - return resp.status; -} - -static int CmdPM5Ant(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw ant_pm5", - "Control the antennal of pm5", - "hw ant_pm5 --set -> Write the data of IO data register\n" - "hw ant_pm5 -m --set -> Write the data of IO map register\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_lit0("m", "map", "Write the IO map register"), - arg_u64_0("s", "set", "", "Set PM5 antenna"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - bool rw_map = arg_get_lit(ctx, 1); - uint64_t io = arg_get_u64_def(ctx, 2, -1); - CLIParserFree(ctx); - - PacketResponseNG resp; - - // Read the current value of the register before writing - struct { - uint8_t reg_type; // 0 is io reg, 1 is map reg. - } PACKED payload_read = { - .reg_type = rw_map ? 1 : 0, - }; - clearCommandBuffer(); - SendCommandNG(CMD_ANT_CONTROL_READ, (uint8_t *)&payload_read, sizeof(payload_read)); - if (WaitForResponseTimeout(CMD_ANT_CONTROL_READ, &resp, 1000) == false) { - PrintAndLogEx(WARNING, "command execution time out"); - return PM3_ETIMEOUT; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(ERR, "failed to read PM5 antenna register"); - return resp.status; - } - PrintAndLogEx(INFO, "PM5 antenna register read: 0x%02X", resp.data.asBytes[0]); - - // Write the new value to the register(If need) - if (io != (uint64_t) -1) { - struct { - uint8_t data; - uint8_t reg_type; // 0 is io reg, 1 is map reg. - } PACKED payload_write = { - .reg_type = rw_map ? 1 : 0, - .data = io & 0xFF, - }; - clearCommandBuffer(); - SendCommandNG(CMD_ANT_CONTROL_WRITE, (uint8_t *)&payload_write, sizeof(payload_write)); - if (WaitForResponseTimeout(CMD_ANT_CONTROL_WRITE, &resp, 1000) == false) { - PrintAndLogEx(WARNING, "command execution time out"); - return PM3_ETIMEOUT; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(ERR, "failed to write PM5 antenna register"); - return resp.status; - } - PrintAndLogEx(INFO, "PM5 antenna register written: 0x%02X", payload_write.data); - } - - return PM3_SUCCESS; -} - -static int CmdDeviceFactoryData(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw factorydata", - "Get/Set the factory data for Device", - "hw factorydata --load -> Write the factory data to device from file\n" - "hw factorydata -> Read and parse the factory data from device\n" - ); - - void *argtable[] = { - arg_param_begin, - arg_str0(NULL, "load", "", "Load factory data from file to device"), - arg_param_end - }; - - CLIExecWithReturn(ctx, Cmd, argtable, true); - - int fnlen = 0; - char filename[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); - - CLIParserFree(ctx); - - // Read the factory data from device - clearCommandBuffer(); - SendCommandNG(CMD_EEPROM_FACTORY_INFO_READ, NULL, 0); - PacketResponseNG resp; - if (WaitForResponseTimeout(CMD_EEPROM_FACTORY_INFO_READ, &resp, 1000) == false) { - PrintAndLogEx(WARNING, "command execution time out"); - return PM3_ETIMEOUT; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(ERR, "failed to read factory data, maybe eeprom unavailable."); - return resp.status; - } - // Parse the factory data - if (resp.length) { - proxmark5_factory_info_v1_t *factory = (proxmark5_factory_info_v1_t *)resp.data.asBytes; - PrintAndLogEx(INFO, "Factory data read from device:"); - PrintAndLogEx(INFO, " - Data raw(hex) : 0x%s", sprint_hex_inrow(resp.data.asBytes, resp.length)); - PrintAndLogEx(INFO, " - Version : %d", factory->factory_info_version); - PrintAndLogEx(INFO, " - Signature : %s", - sprint_hex_inrow(factory->ecdsa_secp256k1_signature, sizeof(factory->ecdsa_secp256k1_signature))); - PrintAndLogEx(INFO, " - Timestamp : %" PRIu64, factory->info.unix_timestamp); - PrintAndLogEx(INFO, " - Chip Unique ID : %s", - sprint_hex_inrow(factory->info.chip_unique_id, sizeof(factory->info.chip_unique_id))); - PrintAndLogEx(INFO, " - Production ID : %" PRIu32, factory->info.production_id); - PrintAndLogEx(INFO, " - Hardware Version : %" PRIu32, factory->info.hardware_version); - PrintAndLogEx(INFO, " - AES key : %s", - sprint_hex_inrow(factory->info.aes_key, sizeof(factory->info.aes_key))); - } - - // If data is provided, it needs to be written - if (fnlen) { - // Load the file content into data buffer - uint8_t *data = NULL; - size_t datalen = 0; - int res = loadFile_safe(filename, ".bin", (void **)&data, &datalen); - if (res != PM3_SUCCESS) { - free(data); - return PM3_EFILE; - } - - if (datalen != resp.length) { - PrintAndLogEx(WARNING, _RED_("The length of the data to write (%zu) does not match the length " - "of the factory data read from device (%u)."), datalen, (unsigned int)resp.length); - free(data); - return PM3_EINVARG; - } - // Write the data to the device - clearCommandBuffer(); - SendCommandNG(CMD_EEPROM_FACTORY_INFO_WRITE, data, datalen); - PacketResponseNG resp_write; - if (WaitForResponseTimeout(CMD_EEPROM_FACTORY_INFO_WRITE, &resp_write, 1000) == false) { - PrintAndLogEx(WARNING, "command execution time out"); - free(data); - return PM3_ETIMEOUT; - } - if (resp_write.status != PM3_SUCCESS) { - PrintAndLogEx(ERR, "failed to write factory data, maybe eeprom unavailable."); - free(data); - return resp_write.status; - } - // Verify the data write - clearCommandBuffer(); - SendCommandNG(CMD_EEPROM_FACTORY_INFO_READ, NULL, 0); - if (WaitForResponseTimeout(CMD_EEPROM_FACTORY_INFO_READ, &resp, 1000) == false) { - PrintAndLogEx(WARNING, "command execution time out"); - free(data); - return PM3_ETIMEOUT; - } - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(ERR, "failed to read factory data after write, maybe eeprom unavailable."); - free(data); - return resp.status; - } - bool verify_result = false; - if (resp.length == datalen) { - verify_result = memcmp(resp.data.asBytes, data, datalen) == 0; - } - if (verify_result) { - PrintAndLogEx(SUCCESS, "Factory data written and verified successfully."); - } else { - PrintAndLogEx(ERR, "Factory data verification failed after write."); - free(data); - return PM3_EFAILED; - } - free(data); - } - - return PM3_SUCCESS; -} - -static int CmdPM5QCTest(const char *Cmd) { - - CLIParserContext *ctx; - CLIParserInit(&ctx, "hw qc_pm5", "QC Test for the PM5", - "hw qc_pm5 -> run QC test with default 20 second timeout\n" - "hw qc_pm5 -t 3 -> run QC test with a 3 second timeout"); - - void *argtable[] = { - arg_param_begin, - arg_u64_0("t", "timeout", "", "test sequence timeout in seconds (default 20)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - uint32_t timeout_ms = arg_get_u32_def(ctx, 1, 20); - timeout_ms *= 1000; - CLIParserFree(ctx); - - if (timeout_ms == 0) { - PrintAndLogEx(ERR, "timeout must be greater than zero"); - return PM3_EINVARG; - } - - PrintAndLogEx(INFO, "Performing QC test for the PM5..."); - - clearCommandBuffer(); - SendCommandNG(CMD_PM5_QC_TEST, (uint8_t *)&timeout_ms, sizeof(timeout_ms)); - - PacketResponseNG resp; - // wait a bit longer than the device side sequence timeout, with headroom for RTC drift - if (WaitForResponseTimeout(CMD_PM5_QC_TEST, &resp, timeout_ms + (timeout_ms / 5) + 1000) == false) { - SendCommandNG(CMD_BREAK_LOOP, NULL, 0); - PrintAndLogEx(WARNING, "command execution time out"); - return PM3_ETIMEOUT; - } - - if (resp.status != PM3_SUCCESS) { - PrintAndLogEx(ERR, "failed to perform QC test on PM5, failed item: %d", resp.data.asBytes[0]); - return resp.status; - } - PrintAndLogEx(INFO, "PM5 QC test successful."); - 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; - clearCommandBuffer(); - // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) - 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) }; - SendCommandNG(CMD_PM5_BWM_ESP_OTA, beg, sizeof(beg)); - if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &resp, 20000) == false) || (resp.status != PM3_SUCCESS)) { - PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); - return PM3_EFAILED; - } - 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, 15000); - if (!got || resp.status != PM3_SUCCESS) { - PrintAndLogEx(NORMAL, ""); - if (!got) { - PrintAndLogEx(WARNING, "OTA write stalled at offset %zu (no response)", 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) - 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. - PrintAndLogEx(WARNING, "OTA finalize rejected (status %d) - data was lost in transit", resp.status); - PrintAndLogEx(HINT, "Try a per-write delay: " _YELLOW_("hw bwmupgrade -f --delay 10")); - return PM3_EFAILED; - } - // No answer at all. - 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, 500) == 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 bwmupgrade", - "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 bwmupgrade -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 10)"), - 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, 10); - 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 = 3; - for (int attempt = 1; attempt <= max_attempts; attempt++) { - if (attempt > 1) { - PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); - } - 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..."); - } - PrintAndLogEx(INFO, "BWM rebooting into the new image (link drops briefly)..."); - msleep(500); // 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 command_t CommandTable[] = { - {"help", CmdHelp, AlwaysAvailable, "This help"}, - {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"}, - {"detectreader", CmdDetectReader, IfPm3Present, "Detect external reader field"}, - {"status", CmdStatus, IfPm3Present, "Show runtime status information about the connected Proxmark3"}, - {"tearoff", CmdTearoff, IfPm3Present, "Program a tearoff hook for the next command supporting tearoff"}, - {"timeout", CmdTimeout, AlwaysAvailable, "Set the communication timeout on the client side"}, - {"version", CmdVersion, AlwaysAvailable, "Show version information about the client and Proxmark3"}, - {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Hardware") " -----------------------"}, - {"break", CmdBreak, IfPm3Present, "Send break loop usb command"}, - {"bootloader", CmdBootloader, IfPm3Present, "Reboot into bootloader mode"}, - {"connect", CmdConnect, AlwaysAvailable, "Connect to the device via serial port"}, - {"dbg", CmdDbg, IfPm3Present, "Set device side debug level"}, - {"fpga", CmdFPGA, IfPm3Present, "Fpga commands"}, - {"fpgaoff", CmdFPGAOff, IfPm3Present, "Turn off FPGA on device"}, - {"ant_pm5", CmdPM5Ant, IfPm5StdAnt, "Control the antennal of pm5"}, - {"qc_pm5", CmdPM5QCTest, IfPm5, "Perform QC test for the PM5"}, - {"factorydata", CmdDeviceFactoryData, IfI2cEeprom, "Get/Set the factory data for Device"}, - {"lcd", CmdLCD, IfPm3Lcd, "Send command/data to LCD"}, - {"lcdreset", CmdLCDReset, IfPm3Lcd, "Hardware reset LCD"}, - {"ping", CmdPing, IfPm3Present, "Test if the Proxmark3 is responsive"}, - {"readmem", CmdReadmem, IfPm3Present, "Read from MCU flash"}, - {"reset", CmdReset, IfPm3Present, "Reset the device"}, - {"setlfdivisor", CmdSetDivisor, IfPm3Lf, "Drive LF antenna at 12MHz / (divisor + 1)"}, - {"sethfthresh", CmdSetHFThreshold, IfPm3Iso14443a, "Set thresholds in HF/14a mode"}, - {"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)"}, - {"bwmupgrade", CmdBWMUpgrade, IfPm5, "Reflash BWM (ESP32) firmware over the BWM link, no header (PM5)"}, - {"tune", CmdTune, IfPm3Lf, "Measure tuning of device antenna"}, - {"decay", CmdDecay, IfPm3Present, "Measure HF antenna decay after field-off"}, - {NULL, NULL, NULL, NULL} -}; - -static int CmdHelp(const char *Cmd) { - (void)Cmd; // Cmd is not used so far - CmdsHelp(CommandTable); - return PM3_SUCCESS; -} - -int CmdHW(const char *Cmd) { - clearCommandBuffer(); - return CmdsParse(CommandTable, Cmd); -} - -#if defined(__MINGW64__) -#define PM3CLIENTCOMPILER "MinGW-w64 " -#elif defined(__MINGW32__) -#define PM3CLIENTCOMPILER "MinGW " -#elif defined(__clang__) -#define PM3CLIENTCOMPILER "Clang/LLVM " -#elif defined(__GNUC__) || defined(__GNUG__) -#define PM3CLIENTCOMPILER "GCC " -#else -#define PM3CLIENTCOMPILER "unknown compiler " -#endif - -#if defined(__APPLE__) || defined(__MACH__) -#define PM3HOSTOS "OSX" -#elif defined(__ANDROID__) || defined(ANDROID) -// must be tested before __linux__ -#define PM3HOSTOS "Android" -#elif defined(__linux__) -#define PM3HOSTOS "Linux" -#elif defined(__FreeBSD__) -#define PM3HOSTOS "FreeBSD" -#elif defined(__NetBSD__) -#define PM3HOSTOS "NetBSD" -#elif defined(__OpenBSD__) -#define PM3HOSTOS "OpenBSD" -#elif defined(__CYGWIN__) -#define PM3HOSTOS "Cygwin" -#elif defined(_WIN64) || defined(__WIN64__) -// must be tested before _WIN32 -#define PM3HOSTOS "Windows (64b)" -#elif defined(_WIN32) || defined(__WIN32__) -#define PM3HOSTOS "Windows (32b)" -#else -#define PM3HOSTOS "unknown" -#endif - -#if defined(__x86_64__) -#define PM3HOSTARCH "x86_64" -#elif defined(__i386__) -#define PM3HOSTARCH "x86" -#elif defined(__aarch64__) -#define PM3HOSTARCH "aarch64" -#elif defined(__arm__) -#define PM3HOSTARCH "arm" -#elif defined(__powerpc64__) -#define PM3HOSTARCH "powerpc64" -#elif defined(__mips__) -#define PM3HOSTARCH "mips" -#else -#define PM3HOSTARCH "unknown" -#endif - -void pm3_version_short(void) { - // PrintAndLogEx(NORMAL, " [ " _CYAN_("Proxmark3 RFID instrument") " ]"); - if (IfPm5()) { - PrintAndLogEx(NORMAL, " [ " _CYAN_(_URL_("https://github.com/RfidResearchGroup/proxmark3", "Proxmark5")) " ]"); - } else { - PrintAndLogEx(NORMAL, " [ " _CYAN_(_URL_("https://github.com/RfidResearchGroup/proxmark3", "Proxmark3")) " ]"); - } - PrintAndLogEx(NORMAL, ""); - - if (g_session.pm3_present) { - - PacketResponseNG resp; - clearCommandBuffer(); - SendCommandNG(CMD_VERSION, NULL, 0); - - if (WaitForResponseTimeout(CMD_VERSION, &resp, 1000)) { - - struct p { - uint32_t id; - uint32_t section_size; - uint32_t versionstr_len; - char versionstr[PM3_CMD_DATA_SIZE - 12]; - } PACKED; - - struct p *payload = (struct p *)&resp.data.asBytes; - - // Flash size (bytes) is appended after the version string by newer - // firmware; 0 if the device didn't send it (older firmware). - uint32_t flash_size = 0; - if (resp.length >= 12 + payload->versionstr_len + sizeof(uint32_t)) { - memcpy(&flash_size, payload->versionstr + payload->versionstr_len, sizeof(flash_size)); - } - - lookup_chipid_short(payload->id, payload->section_size, flash_size); - - if (IfPm5()) { - PrintAndLogEx(NORMAL, " Target.... %s", _YELLOW_("PM5")); - } else if (IfPm3Rdv4Fw()) { - - // validate signature data - rdv40_validation_t mem; - signature_e type; - - if (pm3_get_signature(&mem) == PM3_SUCCESS) { - if (pm3_validate(&mem, &type) == PM3_SUCCESS) { - - if (type == SIGN_RDV4) { - PrintAndLogEx(NORMAL, " Target.... %s", _YELLOW_("RDV4")); - } else if (type == SIGN_GENERIC) { - PrintAndLogEx(NORMAL, " Target.... %s", _YELLOW_("GENERIC")); - } else { - PrintAndLogEx(NORMAL, " Target.... %s", _RED_("device / fw mismatch")); - } - } - } - } else { - PrintAndLogEx(NORMAL, " Target.... %s", _YELLOW_("PM3 GENERIC")); - } - PrintAndLogEx(NORMAL, ""); - - // client - char temp[PM3_CMD_DATA_SIZE - 12]; // same limit as for ARM image - format_version_information_short(temp, sizeof(temp), &g_version_information); - PrintAndLogEx(NORMAL, " Client.... %s", temp); - - bool armsrc_mismatch = false; - char *ptr = strstr(payload->versionstr, "OS......... "); - if (ptr != NULL) { - ptr = strstr(ptr, "\n"); - if ((ptr != NULL) && (strlen(g_version_information.armsrc) == 9)) { - if (strncmp(ptr - 9, g_version_information.armsrc, 9) != 0) { - armsrc_mismatch = true; - } - } - } - - // bootrom - ptr = strstr(payload->versionstr, "Bootrom.... "); - if (ptr != NULL) { - char *ptr_end = strstr(ptr, "\n"); - if (ptr_end != NULL) { - uint8_t len = ptr_end - 12 - ptr; - PrintAndLogEx(NORMAL, " Bootrom... %.*s", len, ptr + 12); - } - } - - // os: - ptr = strstr(payload->versionstr, "OS......... "); - if (ptr != NULL) { - char *ptr_end = strstr(ptr, "\n"); - if (ptr_end != NULL) { - uint8_t len = ptr_end - 12 - ptr; - PrintAndLogEx(NORMAL, " OS........ %.*s", len, ptr + 12); - } - } - PrintAndLogEx(NORMAL, ""); - - if (armsrc_mismatch) { - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(WARNING, " --> " _RED_("ARM firmware does not match the source at the time the client was compiled")); - PrintAndLogEx(WARNING, " --> Make sure to flash a correct and up-to-date version"); - } - } - } - PrintAndLogEx(NORMAL, ""); -} - -void pm3_version(bool verbose, bool oneliner) { - - char temp[PM3_CMD_DATA_SIZE - 12]; // same limit as for ARM image - - if (oneliner) { - // For "proxmark3 -v", simple printf, avoid logging - FormatVersionInformation(temp, sizeof(temp), "Client: ", &g_version_information); - PrintAndLogEx(NORMAL, "%s compiler: " PM3CLIENTCOMPILER __VERSION__ " OS:" PM3HOSTOS " ARCH:" PM3HOSTARCH "\n", temp); - return; - } - - if (!verbose) { - return; - } - - PrintAndLogEx(NORMAL, "\n [ " _CYAN_("%s") " ]", IfPm5() ? "Proxmark5" : "Proxmark3"); - PrintAndLogEx(NORMAL, "\n [ " _YELLOW_("Client") " ]"); - FormatVersionInformation(temp, sizeof(temp), " ", &g_version_information); - PrintAndLogEx(NORMAL, "%s", temp); - PrintAndLogEx(NORMAL, " Compiler.................. " PM3CLIENTCOMPILER __VERSION__); - PrintAndLogEx(NORMAL, " Platform.................. " PM3HOSTOS " / " PM3HOSTARCH); -#if defined(HAVE_READLINE) - PrintAndLogEx(NORMAL, " Readline support.......... " _GREEN_("present")); -#elif defined(HAVE_LINENOISE) - PrintAndLogEx(NORMAL, " Linenoise support......... " _GREEN_("present")); -#else - PrintAndLogEx(NORMAL, " Readline/Linenoise support." _YELLOW_("absent")); -#endif -#ifdef HAVE_GUI - PrintAndLogEx(NORMAL, " QT GUI support............ " _GREEN_("present")); -#else - PrintAndLogEx(NORMAL, " QT GUI support............ " _YELLOW_("absent")); -#endif -#ifdef HAVE_BLUEZ - PrintAndLogEx(NORMAL, " Native BT support......... " _GREEN_("present")); -#else - PrintAndLogEx(NORMAL, " Native BT support......... " _YELLOW_("absent")); -#endif - -#ifdef HAVE_PYTHON -#ifndef PY_VERSION -#define PY_VERSION "unknown version" -#endif - PrintAndLogEx(NORMAL, " Python script support..... " _GREEN_("present") " ( " _YELLOW_(PY_VERSION) " )"); -#else - PrintAndLogEx(NORMAL, " Python script support..... " _YELLOW_("absent")); -#endif -#ifdef HAVE_PYTHON_SWIG - PrintAndLogEx(NORMAL, " Python SWIG support....... " _GREEN_("present")); -#else - PrintAndLogEx(NORMAL, " Python SWIG support....... " _YELLOW_("absent")); -#endif - PrintAndLogEx(NORMAL, " Lua script support........ " _GREEN_("present") " ( " _YELLOW_("%s.%s.%s") " )", LUA_VERSION_MAJOR, LUA_VERSION_MINOR, LUA_VERSION_RELEASE); -#ifdef HAVE_LUA_SWIG - PrintAndLogEx(NORMAL, " Lua SWIG support.......... " _GREEN_("present")); -#else - PrintAndLogEx(NORMAL, " Lua SWIG support.......... " _YELLOW_("absent")); -#endif - - if (g_session.pm3_present) { - PrintAndLogEx(NORMAL, "\n [ " _YELLOW_("Model") " ]"); - - PacketResponseNG resp; - clearCommandBuffer(); - SendCommandNG(CMD_VERSION, NULL, 0); - - if (WaitForResponseTimeout(CMD_VERSION, &resp, 1000)) { - if (IfPm5()) { - PrintAndLogEx(NORMAL, " Firmware.................. " _GREEN_("PM5")); - PrintAndLogEx(NORMAL, " External flash............ %s", IfPm3Flash() ? _GREEN_("present") : _YELLOW_("absent")); - } else if (IfPm3Rdv4Fw()) { - - // validate signature data - rdv40_validation_t mem; - signature_e type; - - if (pm3_get_signature(&mem) == PM3_SUCCESS) { - if (pm3_validate(&mem, &type) == PM3_SUCCESS) { - - if (type == SIGN_RDV4) { - PrintAndLogEx(NORMAL, " Device.................... " _GREEN_("RDV4")); - PrintAndLogEx(NORMAL, " Firmware.................. " _GREEN_("RDV4")); - } else if (type == SIGN_GENERIC) { - PrintAndLogEx(NORMAL, " Device.................... ", _GREEN_("GENERIC")); - PrintAndLogEx(NORMAL, " Firmware.................. ", _GREEN_("GENERIC")); - } else { - PrintAndLogEx(NORMAL, " Device.................... " _RED_("Bad signature detected!")); - PrintAndLogEx(NORMAL, " Firmware.................. " _YELLOW_("N/A")); - } - } - } - - PrintAndLogEx(NORMAL, " External flash............ %s", IfPm3Flash() ? _GREEN_("present") : _YELLOW_("absent")); - PrintAndLogEx(NORMAL, " Smartcard reader.......... %s", IfPm3Smartcard() ? _GREEN_("present") : _YELLOW_("absent")); - PrintAndLogEx(NORMAL, " FPC USART for BT add-on... %s", IfPm3FpcUsartHost() ? _GREEN_("present") : _YELLOW_("absent")); - } else { - PrintAndLogEx(NORMAL, " Firmware.................. %s", _YELLOW_("PM3 GENERIC")); - if (IfPm3Flash()) { - PrintAndLogEx(NORMAL, " External flash............ %s", _GREEN_("present")); - } - - if (IfPm3FpcUsartHost()) { - PrintAndLogEx(NORMAL, " FPC USART for BT add-on... %s", _GREEN_("present")); - } - } - - if (IfPm3FpcUsartDevFromUsb()) { - PrintAndLogEx(NORMAL, " FPC USART for developer... %s", _GREEN_("present")); - } - - PrintAndLogEx(NORMAL, ""); - - struct p { - uint32_t id; - uint32_t section_size; - uint32_t versionstr_len; - char versionstr[PM3_CMD_DATA_SIZE - 12]; - } PACKED; - - struct p *payload = (struct p *)&resp.data.asBytes; - - bool armsrc_mismatch = false; - char *ptr = strstr(payload->versionstr, "OS......... "); - if (ptr != NULL) { - ptr = strstr(ptr, "\n"); - if ((ptr != NULL) && (strlen(g_version_information.armsrc) == 9)) { - if (strncmp(ptr - 9, g_version_information.armsrc, 9) != 0) { - armsrc_mismatch = true; - } - } - } - PrintAndLogEx(NORMAL, payload->versionstr); - // PM5 doesn't report a built-in FPGA version (Gowin bitstream is loaded - // externally), so skip the Xilinx FPGA_TYPE match check for it. - if (!IfPm5() && strstr(payload->versionstr, FPGA_TYPE) == NULL) { - PrintAndLogEx(NORMAL, " FPGA firmware... %s", _RED_("chip mismatch")); - } - - // Flash size (bytes) is appended after the version string by newer - // firmware; 0 if the device didn't send it (older firmware). - uint32_t flash_size = 0; - if (resp.length >= 12 + payload->versionstr_len + sizeof(uint32_t)) { - memcpy(&flash_size, payload->versionstr + payload->versionstr_len, sizeof(flash_size)); - } - - lookupChipID(payload->id, payload->section_size, flash_size); - - // Get unique id of mainchip - clearCommandBuffer(); - SendCommandNG(CMD_MAIN_CHIP_UNIQUEID, NULL, 0); - if (WaitForResponseTimeout(CMD_MAIN_CHIP_UNIQUEID, &resp, 1000)) { - if (resp.length) { // Some processor maybe no unique id. - char *uniqueid_hex = sprint_hex_inrow(resp.data.asBytes, resp.length); - PrintAndLogEx(NORMAL, " --= Processor Unique ID: " _YELLOW_("0x%s"), uniqueid_hex); - } - } - - if (armsrc_mismatch) { - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(WARNING, _RED_("ARM firmware does not match the source at the time the client was compiled")); - PrintAndLogEx(WARNING, "Make sure to flash a correct and up-to-date version"); - } - } - } - PrintAndLogEx(NORMAL, ""); +uint32_t bwm_uart_read(uint8_t *data, size_t len) { + uint16_t head = bwm_uart_rx_head(); + uint32_t n = 0; + while (n < len && s_rx_tail != head) { + data[n++] = s_rx_ring[s_rx_tail]; + s_rx_tail = (uint16_t)((s_rx_tail + 1) & (BWM_RX_RING_SZ - 1)); + } + return n; } From afcfa2168b2934fec95d6f9402a085afb810773a Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:28:36 +0200 Subject: [PATCH 76/89] Update print statement from 'Hello' to 'Goodbye' Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 2982 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 2841 insertions(+), 141 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index e796f1945..798f88547 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -6,169 +6,2869 @@ // 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. //----------------------------------------------------------------------------- -// AT32F435 UART4 driver for the Proxmark5 BWM link - see bwm_uart_at32.h. -// -// RX is serviced by circular DMA (DMA1 channel 2) rather than a per-byte -// RX interrupt. The old RDBF ISR dropped bytes on overrun as soon as the -// main loop was busy (FPGA / USB) - fine at 460800, fatal at higher bauds, -// where the dropped byte becomes an app_com CRC-16 failure and a stall. -// Circular DMA lets the controller sink every byte independent of CPU load, -// so BWM_UART_BAUD can be raised to lift BLE/WiFi transfer rates. +// Hardware commands +// low-level hardware control //----------------------------------------------------------------------------- -#include "bwm_uart_at32.h" +#include +#include +#include +#include +#ifdef HAVE_PYTHON +#ifdef _POSIX_C_SOURCE +#undef _POSIX_C_SOURCE +#endif +#include +#endif + +#include "cmdparser.h" // command_t +#include "cliparser.h" +#include "comms.h" +#include "usart_defs.h" +#include "ui.h" +#include "fpga.h" +#include "cmdhw.h" +#include "cmdfpga.h" +#include "cmddata.h" +#include "commonutil.h" +#include "preferences.h" #include "pm3_cmd.h" -#include "at32f435_437.h" -#include "at32f435_437_crm.h" -#include "at32f435_437_gpio.h" -#include "at32f435_437_usart.h" -#include "at32f435_437_dma.h" -#include "at32f435_437_misc.h" +#include "pmflash.h" // rdv40validation_t +#include "cmdflashmem.h" // get_signature.. +#include "uart/uart.h" // configure timeout +#include "util_posix.h" +#include "flash.h" // reboot to bootloader mode +#include "proxgui.h" +#include "graph.h" // for graph data -#define BWM_UART UART4 -#define BWM_UART_GPIO GPIOA -#define BWM_UART_TX_PIN GPIO_PINS_0 -#define BWM_UART_RX_PIN GPIO_PINS_1 -#define BWM_UART_TX_SRC GPIO_PINS_SOURCE0 -#define BWM_UART_RX_SRC GPIO_PINS_SOURCE1 -#define BWM_UART_MUX GPIO_MUX_8 +#include "lua.h" -// SSC already owns DMA1 channel 1 (see fpga_hw_at32.c); UART4 RX uses channel 2. -#define BWM_DMA_CHANNEL DMA1_CHANNEL2 -#define BWM_DMA_MUX_CHANNEL DMA1MUX_CHANNEL2 +static int CmdHelp(const char *Cmd); -// Power-of-two so head/tail wrap with a mask. DMA target buffer. -// Must comfortably exceed one full forward frame or the DMA laps the reader. -// A frame is app_com(6) + NG(10) + up to PM3_CMD_DATA_SIZE data + CRC(2); at -// PM3_CMD_DATA_SIZE=4064 that is ~4082 bytes, so 4096 leaves ~14 bytes of slack -// and overruns the instant the consumer lags. 4x headroom on a 512K part. -#define BWM_RX_RING_SZ 16384 -static volatile uint8_t s_rx_ring[BWM_RX_RING_SZ]; -static volatile uint16_t s_rx_tail = 0; // software read cursor; head comes from DMA - -static volatile bool s_inited = false; -static volatile uint32_t s_cur_baud = BWM_UART_BAUD; - -// 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]. -static inline uint16_t bwm_uart_rx_head(void) { - return (uint16_t)((BWM_RX_RING_SZ - dma_data_number_get(BWM_DMA_CHANNEL)) - & (BWM_RX_RING_SZ - 1)); -} - -// Bring UART4 + circular RX DMA up at `baud`. Safe to call repeatedly: on a -// re-config it tears the channel down first, so the ring restarts empty (which -// also discards bytes straddling a baud switch). Mirrors the SSC RX setup in -// fpga_hw_at32.c. -static void bwm_uart_configure(uint32_t baud) { - crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE); - crm_periph_clock_enable(CRM_UART4_PERIPH_CLOCK, TRUE); - crm_periph_clock_enable(CRM_DMA1_PERIPH_CLOCK, TRUE); - - gpio_init_type gpio_init_struct; - gpio_default_para_init(&gpio_init_struct); - gpio_init_struct.gpio_mode = GPIO_MODE_MUX; - gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL; - gpio_init_struct.gpio_pull = GPIO_PULL_NONE; - gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER; - gpio_init_struct.gpio_pins = BWM_UART_TX_PIN | BWM_UART_RX_PIN; - gpio_init(BWM_UART_GPIO, &gpio_init_struct); - gpio_pin_mux_config(BWM_UART_GPIO, BWM_UART_TX_SRC, BWM_UART_MUX); - gpio_pin_mux_config(BWM_UART_GPIO, BWM_UART_RX_SRC, BWM_UART_MUX); - - // Quiesce before reconfiguring (matters on the re-config path). - dma_channel_enable(BWM_DMA_CHANNEL, FALSE); - usart_enable(BWM_UART, FALSE); - - usart_init(BWM_UART, baud, USART_DATA_8BITS, USART_STOP_1_BIT); - usart_parity_selection_config(BWM_UART, USART_PARITY_NONE); - usart_transmitter_enable(BWM_UART, TRUE); - usart_receiver_enable(BWM_UART, TRUE); - - // --- RX via circular DMA --- - s_rx_tail = 0; - - dma_reset(BWM_DMA_CHANNEL); - - dma_init_type dma_init_struct; - dma_default_para_init(&dma_init_struct); - dma_init_struct.buffer_size = BWM_RX_RING_SZ; - dma_init_struct.direction = DMA_DIR_PERIPHERAL_TO_MEMORY; - dma_init_struct.peripheral_base_addr = (uint32_t) & (BWM_UART->dt); - dma_init_struct.peripheral_inc_enable = FALSE; - dma_init_struct.memory_base_addr = (uint32_t)s_rx_ring; - dma_init_struct.memory_inc_enable = TRUE; - dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_BYTE; - dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_BYTE; - dma_init_struct.loop_mode_enable = TRUE; // circular: reloads at count 0 - // MEDIUM: single-byte requests, must not starve the SSC bulk channel (HIGH). - dma_init_struct.priority = DMA_PRIORITY_MEDIUM; - dma_init(BWM_DMA_CHANNEL, &dma_init_struct); - - dmamux_enable(DMA1, TRUE); - dmamux_init(BWM_DMA_MUX_CHANNEL, DMAMUX_DMAREQ_ID_UART4_RX); - - usart_dma_receiver_enable(BWM_UART, TRUE); - dma_channel_enable(BWM_DMA_CHANNEL, TRUE); - - usart_enable(BWM_UART, TRUE); - s_cur_baud = baud; -} - -void bwm_uart_init(void) { - if (s_inited) { +static void lookup_chipid_short(uint32_t iChipID, uint32_t mem_used, uint32_t flash_size) { + // AT32 (PM5): the chip id is an ARM DBGMCU IDCODE, not an Atmel CIDR, so the + // AT91 decode below does not apply (it would print "Unknown" and a bogus flash + // size). Report the MCU and use the real flash size the device reported. + if (IfPm5()) { + PrintAndLogEx(NORMAL, " MCU....... " _YELLOW_("%s"), "AT32F437"); + uint32_t mem_kb = flash_size / 1024; + PrintAndLogEx(NORMAL, " Memory.... " _YELLOW_("%u") " KB ( " _YELLOW_("%2.0f%%") " used )" + , mem_kb + , mem_kb == 0 ? 0.0f : (float)mem_used / (mem_kb * 1024) * 100 + ); return; } - bwm_uart_configure(BWM_UART_BAUD); - s_inited = true; + + const char *asBuff; + switch (iChipID) { + case 0x270B0A40: + asBuff = "AT91SAM7S512 Rev A"; + break; + case 0x270B0A4E: + case 0x270B0A4F: + asBuff = "AT91SAM7S512 Rev B"; + break; + case 0x270D0940: + asBuff = "AT91SAM7S256 Rev A"; + break; + case 0x270B0941: + asBuff = "AT91SAM7S256 Rev B"; + break; + case 0x270B0942: + asBuff = "AT91SAM7S256 Rev C"; + break; + case 0x270B0943: + asBuff = "AT91SAM7S256 Rev D"; + break; + case 0x270C0740: + asBuff = "AT91SAM7S128 Rev A"; + break; + case 0x270A0741: + asBuff = "AT91SAM7S128 Rev B"; + break; + case 0x270A0742: + asBuff = "AT91SAM7S128 Rev C"; + break; + case 0x270A0743: + asBuff = "AT91SAM7S128 Rev D"; + break; + case 0x27090540: + asBuff = "AT91SAM7S64 Rev A"; + break; + case 0x27090543: + asBuff = "AT91SAM7S64 Rev B"; + break; + case 0x27090544: + asBuff = "AT91SAM7S64 Rev C"; + break; + case 0x27080342: + asBuff = "AT91SAM7S321 Rev A"; + break; + case 0x27080340: + asBuff = "AT91SAM7S32 Rev A"; + break; + case 0x27080341: + asBuff = "AT91SAM7S32 Rev B"; + break; + case 0x27050241: + asBuff = "AT9SAM7S161 Rev A"; + break; + case 0x27050240: + asBuff = "AT91SAM7S16 Rev A"; + break; + default: + asBuff = "Unknown"; + break; + } + PrintAndLogEx(NORMAL, " MCU....... " _YELLOW_("%s"), asBuff); + + uint32_t mem_avail = 0; + switch ((iChipID & 0xF00) >> 8) { + case 0: + mem_avail = 0; + break; + case 1: + mem_avail = 8; + break; + case 2: + mem_avail = 16; + break; + case 3: + mem_avail = 32; + break; + case 5: + mem_avail = 64; + break; + case 7: + mem_avail = 128; + break; + case 9: + mem_avail = 256; + break; + case 10: + mem_avail = 512; + break; + case 12: + mem_avail = 1024; + break; + case 14: + mem_avail = 2048; + break; + } + + PrintAndLogEx(NORMAL, " Memory.... " _YELLOW_("%u") " KB ( " _YELLOW_("%2.0f%%") " used )" + , mem_avail + , mem_avail == 0 ? 0.0f : (float)mem_used / (mem_avail * 1024) * 100 + ); } -void bwm_uart_set_baud(uint32_t baud) { - if (baud == 0 || baud == s_cur_baud) { +static void lookupChipID(uint32_t iChipID, uint32_t mem_used, uint32_t flash_size) { + const char *asBuff; + uint32_t mem_avail = 0; + PrintAndLogEx(NORMAL, "\n [ " _YELLOW_("Hardware") " ]"); + + // AT32 (PM5): the chip id is an ARM DBGMCU IDCODE, not an Atmel CIDR, so the + // verbose AT91 decode below does not apply. Print a short AT32 summary instead. + if (IfPm5()) { + PrintAndLogEx(NORMAL, " --= uC: AT32F437"); + uint32_t mem_kb = flash_size / 1024; + PrintAndLogEx(NORMAL, " --= Nonvolatile Program Memory Size: %u KB, Used: %u bytes (%2.0f%%)" + , mem_kb + , mem_used + , mem_kb == 0 ? 0.0f : (float)mem_used / (mem_kb * 1024) * 100 + ); return; } - bwm_uart_configure(baud); -} -uint32_t bwm_uart_get_baud(void) { - return s_cur_baud; -} - -int bwm_uart_write(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) { - } - usart_data_transmit(BWM_UART, data[i]); + switch (iChipID) { + case 0x270B0A40: + asBuff = "AT91SAM7S512 Rev A"; + break; + case 0x270B0A4E: + case 0x270B0A4F: + asBuff = "AT91SAM7S512 Rev B"; + break; + case 0x270D0940: + asBuff = "AT91SAM7S256 Rev A"; + break; + case 0x270B0941: + asBuff = "AT91SAM7S256 Rev B"; + break; + case 0x270B0942: + asBuff = "AT91SAM7S256 Rev C"; + break; + case 0x270B0943: + asBuff = "AT91SAM7S256 Rev D"; + break; + case 0x270C0740: + asBuff = "AT91SAM7S128 Rev A"; + break; + case 0x270A0741: + asBuff = "AT91SAM7S128 Rev B"; + break; + case 0x270A0742: + asBuff = "AT91SAM7S128 Rev C"; + break; + case 0x270A0743: + asBuff = "AT91SAM7S128 Rev D"; + break; + case 0x27090540: + asBuff = "AT91SAM7S64 Rev A"; + break; + case 0x27090543: + asBuff = "AT91SAM7S64 Rev B"; + break; + case 0x27090544: + asBuff = "AT91SAM7S64 Rev C"; + break; + case 0x27080342: + asBuff = "AT91SAM7S321 Rev A"; + break; + case 0x27080340: + asBuff = "AT91SAM7S32 Rev A"; + break; + case 0x27080341: + asBuff = "AT91SAM7S32 Rev B"; + break; + case 0x27050241: + asBuff = "AT9SAM7S161 Rev A"; + break; + case 0x27050240: + asBuff = "AT91SAM7S16 Rev A"; + break; + default: + asBuff = "Unknown"; + break; } - while (usart_flag_get(BWM_UART, USART_TDC_FLAG) == RESET) { + PrintAndLogEx(NORMAL, " --= uC: " _YELLOW_("%s"), asBuff); + + switch ((iChipID & 0xE0) >> 5) { + case 1: + asBuff = "ARM946ES"; + break; + case 2: + asBuff = "ARM7TDMI"; + break; + case 4: + asBuff = "ARM920T"; + break; + case 5: + asBuff = "ARM926EJS"; + break; + default: + asBuff = "Unknown"; + break; + } + PrintAndLogEx(NORMAL, " --= Embedded Processor: %s", asBuff); + + switch ((iChipID & 0xF0000) >> 16) { + case 1: + asBuff = "1K bytes"; + break; + case 2: + asBuff = "2K bytes"; + break; + case 3: + asBuff = "6K bytes"; + break; + case 4: + asBuff = "112K bytes"; + break; + case 5: + asBuff = "4K bytes"; + break; + case 6: + asBuff = "80K bytes"; + break; + case 7: + asBuff = "160K bytes"; + break; + case 8: + asBuff = "8K bytes"; + break; + case 9: + asBuff = "16K bytes"; + break; + case 10: + asBuff = "32K bytes"; + break; + case 11: + asBuff = "64K bytes"; + break; + case 12: + asBuff = "128K bytes"; + break; + case 13: + asBuff = "256K bytes"; + break; + case 14: + asBuff = "96K bytes"; + break; + case 15: + asBuff = "512K bytes"; + break; + default: + asBuff = "Unknown"; + break; + } + PrintAndLogEx(NORMAL, " --= Internal SRAM size: %s", asBuff); + + switch ((iChipID & 0xFF00000) >> 20) { + case 0x19: + asBuff = "AT91SAM9xx Series"; + break; + case 0x29: + asBuff = "AT91SAM9XExx Series"; + break; + case 0x34: + asBuff = "AT91x34 Series"; + break; + case 0x37: + asBuff = "CAP7 Series"; + break; + case 0x39: + asBuff = "CAP9 Series"; + break; + case 0x3B: + asBuff = "CAP11 Series"; + break; + case 0x40: + asBuff = "AT91x40 Series"; + break; + case 0x42: + asBuff = "AT91x42 Series"; + break; + case 0x55: + asBuff = "AT91x55 Series"; + break; + case 0x60: + asBuff = "AT91SAM7Axx Series"; + break; + case 0x61: + asBuff = "AT91SAM7AQxx Series"; + break; + case 0x63: + asBuff = "AT91x63 Series"; + break; + case 0x70: + asBuff = "AT91SAM7Sxx Series"; + break; + case 0x71: + asBuff = "AT91SAM7XCxx Series"; + break; + case 0x72: + asBuff = "AT91SAM7SExx Series"; + break; + case 0x73: + asBuff = "AT91SAM7Lxx Series"; + break; + case 0x75: + asBuff = "AT91SAM7Xxx Series"; + break; + case 0x92: + asBuff = "AT91x92 Series"; + break; + case 0xF0: + asBuff = "AT75Cxx Series"; + break; + default: + asBuff = "Unknown"; + break; + } + PrintAndLogEx(NORMAL, " --= Architecture identifier: %s", asBuff); + + switch ((iChipID & 0x70000000) >> 28) { + case 0: + asBuff = "ROM"; + break; + case 1: + asBuff = "ROMless or on-chip Flash"; + break; + case 2: + asBuff = "Embedded flash memory"; + break; + case 3: + asBuff = "ROM and Embedded flash memory\nNVPSIZ is ROM size\nNVPSIZ2 is Flash size"; + break; + case 4: + asBuff = "SRAM emulating ROM"; + break; + default: + asBuff = "Unknown"; + break; + } + switch ((iChipID & 0xF00) >> 8) { + case 0: + mem_avail = 0; + break; + case 1: + mem_avail = 8; + break; + case 2: + mem_avail = 16; + break; + case 3: + mem_avail = 32; + break; + case 5: + mem_avail = 64; + break; + case 7: + mem_avail = 128; + break; + case 9: + mem_avail = 256; + break; + case 10: + mem_avail = 512; + break; + case 12: + mem_avail = 1024; + break; + case 14: + mem_avail = 2048; + break; + } + + PrintAndLogEx(NORMAL, " --= %s " _YELLOW_("%uK") " bytes ( " _YELLOW_("%2.0f%%") " used )" + , asBuff + , mem_avail + , mem_avail == 0 ? 0.0f : (float)mem_used / (mem_avail * 1024) * 100 + ); + + /* + switch ((iChipID & 0xF000) >> 12) { + case 0: + asBuff = "None"); + break; + case 1: + asBuff = "8K bytes"); + break; + case 2: + asBuff = "16K bytes"); + break; + case 3: + asBuff = "32K bytes"); + break; + case 5: + asBuff = "64K bytes"); + break; + case 7: + asBuff = "128K bytes"); + break; + case 9: + asBuff = "256K bytes"); + break; + case 10: + asBuff = "512K bytes"); + break; + case 12: + asBuff = "1024K bytes"); + break; + case 14: + asBuff = "2048K bytes"); + break; + } + PrintAndLogEx(NORMAL, " --= Second nonvolatile program memory size: %s", asBuff); + */ +} + +static int CmdDbg(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw dbg", + "Set device side debug level output.\n" + "Note: option `-4`, this option may cause malfunction itself by\n" + "introducing delays in time critical functions like simulation or sniffing", + "hw dbg --> get current log level\n" + "hw dbg -1 --> set log level to _error_\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_lit0("0", NULL, "no debug messages"), + arg_lit0("1", NULL, "error messages"), + arg_lit0("2", NULL, "plus information messages"), + arg_lit0("3", NULL, "plus debug messages"), + arg_lit0("4", NULL, "print even debug messages in timing critical functions"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool lv0 = arg_get_lit(ctx, 1); + bool lv1 = arg_get_lit(ctx, 2); + bool lv2 = arg_get_lit(ctx, 3); + bool lv3 = arg_get_lit(ctx, 4); + bool lv4 = arg_get_lit(ctx, 5); + CLIParserFree(ctx); + + if ((lv0 + lv1 + lv2 + lv3 + lv4) > 1) { + PrintAndLogEx(INFO, "Can only set one debug level"); + return PM3_EINVARG; + } + + uint8_t curr = DBG_NONE; + if (getDeviceDebugLevel(&curr) != PM3_SUCCESS) + return PM3_EFAILED; + + const char *dbglvlstr; + switch (curr) { + case DBG_NONE: + dbglvlstr = "none"; + break; + case DBG_ERROR: + dbglvlstr = "error"; + break; + case DBG_INFO: + dbglvlstr = "info"; + break; + case DBG_DEBUG: + dbglvlstr = "debug"; + break; + case DBG_EXTENDED: + dbglvlstr = "extended"; + break; + default: + dbglvlstr = "unknown"; + break; + } + PrintAndLogEx(INFO, " Current debug log level..... %d ( " _YELLOW_("%s") " )", curr, dbglvlstr); + + if ((lv0 + lv1 + lv2 + lv3 + lv4) == 1) { + uint8_t dbg = 0; + if (lv0) + dbg = 0; + else if (lv1) + dbg = 1; + else if (lv2) + dbg = 2; + else if (lv3) + dbg = 3; + else if (lv4) + dbg = 4; + + if (setDeviceDebugLevel(dbg, true) != PM3_SUCCESS) + return PM3_EFAILED; } return PM3_SUCCESS; } -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; +static int CmdDetectReader(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw detectreader", + "Start to detect presences of reader field", + "hw detectreader\n" + "hw detectreader -L\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_lit0("L", "LF", "only detect low frequency 125/134 kHz"), + arg_lit0("H", "HF", "only detect high frequency 13.56 MHZ"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool lf = arg_get_lit(ctx, 1); + bool hf = arg_get_lit(ctx, 2); + CLIParserFree(ctx); + + // 0: Detect both frequency in mode 1 + // 1: LF_ONLY + // 2: HF_ONLY + uint8_t arg = 0; + if (lf == true && hf == false) { + arg = 1; + } else if (hf == true && lf == false) { + arg = 2; } - return (uint16_t)((bwm_uart_rx_head() - s_rx_tail) & (BWM_RX_RING_SZ - 1)); + + clearCommandBuffer(); + SendCommandNG(CMD_LISTEN_READER_FIELD, (uint8_t *)&arg, sizeof(arg)); + PrintAndLogEx(INFO, "Press " _GREEN_("pm3 button") " or " _GREEN_("") " to change modes and exit"); + + for (;;) { + if (kbd_enter_pressed()) { + SendCommandNG(CMD_BREAK_LOOP, NULL, 0); + PrintAndLogEx(DEBUG, _GREEN_("") " pressed"); + } + + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_LISTEN_READER_FIELD, &resp, 1000)) { + if (resp.status != PM3_EOPABORTED) { + PrintAndLogEx(ERR, "Unexpected response: %d", resp.status); + } + break; + } + } + PrintAndLogEx(INFO, "Done!"); + return PM3_SUCCESS; } -uint32_t bwm_uart_read(uint8_t *data, size_t len) { - uint16_t head = bwm_uart_rx_head(); - uint32_t n = 0; - while (n < len && s_rx_tail != head) { - data[n++] = s_rx_ring[s_rx_tail]; - s_rx_tail = (uint16_t)((s_rx_tail + 1) & (BWM_RX_RING_SZ - 1)); - } - return n; +// ## FPGA Control +static int CmdFPGAOff(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw fpgaoff", + "Turn of fpga and antenna field", + "hw fpgaoff\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + + clearCommandBuffer(); + SendCommandNG(CMD_FPGA_MAJOR_MODE_OFF, NULL, 0); + return PM3_SUCCESS; +} + +static int CmdLCD(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw lcd", + "Send command/data to LCD", + "hw lcd -r AA -c 03 -> sends 0xAA three times" + ); + + void *argtable[] = { + arg_param_begin, + arg_int1("r", "raw", "", "data "), + arg_int1("c", "cnt", "", "number of times to send"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int r_len = 0; + uint8_t raw[1] = {0}; + CLIGetHexWithReturn(ctx, 1, raw, &r_len); + int j = arg_get_int_def(ctx, 2, 1); + CLIParserFree(ctx); + if (j < 1) { + PrintAndLogEx(WARNING, "Count must be larger than zero"); + return PM3_EINVARG; + } + + while (j--) { + clearCommandBuffer(); + lcd_cmd_t payload = { .cmd = raw[0] }; + SendCommandNG(CMD_LCD, (uint8_t *)&payload, sizeof(payload)); + } + return PM3_SUCCESS; +} + +static int CmdLCDReset(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw lcdreset", + "Hardware reset LCD", + "hw lcdreset\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + clearCommandBuffer(); + SendCommandNG(CMD_LCD_RESET, NULL, 0); + return PM3_SUCCESS; +} + +static int CmdReadmem(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw readmem", + "Reads processor flash memory into a file or views on console", + "hw readmem -f myfile -> save 512KB processor flash memory to file\n" + "hw readmem -a 8192 -l 512 -> display 512 bytes from offset 8192\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_u64_0("a", "adr", "", "flash address to start reading from"), + arg_u64_0("l", "len", "", "length (default 32 or 512KB)"), + arg_str0("f", "file", "", "save to file"), + arg_u64_0("c", "cols", "", "column breaks"), + arg_lit0("r", "raw", "use raw address mode: read from anywhere, not just flash"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, false); + + // check for -file option first to determine the output mode + int fnlen = 0; + char filename[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 3), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); + bool save_to_file = fnlen > 0; + + // default len to 512KB when saving to file, to 32 bytes when viewing on the console. + uint32_t default_len = save_to_file ? 512 * 1024 : 32; + + uint32_t address = arg_get_u32_def(ctx, 1, 0); + uint32_t len = arg_get_u32_def(ctx, 2, default_len); + int breaks = arg_get_int_def(ctx, 4, 32); + bool raw = arg_get_lit(ctx, 5); + CLIParserFree(ctx); + + uint8_t *buffer = calloc(len, sizeof(uint8_t)); + if (buffer == NULL) { + PrintAndLogEx(WARNING, "Failed to allocate memory"); + return PM3_EMALLOC; + } + + const char *flash_str = raw ? "" : " flash"; + PrintAndLogEx(INFO, "reading " _YELLOW_("%u") " bytes from processor%s memory", + len, flash_str); + + DeviceMemType_t type = raw ? MCU_MEM : MCU_FLASH; + if (!GetFromDevice(type, buffer, len, address, NULL, 0, NULL, -1, true)) { + PrintAndLogEx(FAILED, "ERROR; reading from MCU flash memory"); + free(buffer); + return PM3_EFLASH; + } + + if (save_to_file) { + saveFile(filename, ".bin", buffer, len); + } else { + PrintAndLogEx(INFO, "---- " _CYAN_("processor%s memory") " ----", flash_str); + print_hex_break(buffer, len, breaks); + } + + free(buffer); + return PM3_SUCCESS; +} + +static int CmdReset(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw reset", + "Reset the Proxmark3 device.", + "hw reset" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + clearCommandBuffer(); + SendCommandNG(CMD_HARDWARE_RESET, NULL, 0); + PrintAndLogEx(INFO, "Proxmark3 has been reset."); + return PM3_SUCCESS; +} + +/* + * Sets the divisor for LF frequency clock: lets the user choose any LF frequency below + * 600kHz. + */ +static int CmdSetDivisor(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw setlfdivisor", + "Drive LF antenna at 12 MHz / (divisor + 1).", + "hw setlfdivisor -d 88" + ); + + void *argtable[] = { + arg_param_begin, + arg_u64_1("d", "div", "", "19 - 255 divisor value (def 95)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + uint8_t arg = arg_get_u32_def(ctx, 1, 95); + CLIParserFree(ctx); + + if (arg < 19) { + PrintAndLogEx(ERR, "Divisor must be between " _YELLOW_("19") " and " _YELLOW_("255")); + return PM3_EINVARG; + } + // 12 000 000 (12MHz) + clearCommandBuffer(); + SendCommandNG(CMD_LF_SET_DIVISOR, (uint8_t *)&arg, sizeof(arg)); + PrintAndLogEx(SUCCESS, "Divisor set, expected " _YELLOW_("%.1f") " kHz", ((double)12000 / (arg + 1))); + return PM3_SUCCESS; +} + +static int CmdSetHFThreshold(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw sethfthresh", + "Set thresholds in HF/14a and Legic mode.", + "hw sethfthresh -t 7 -i 20 -l 8" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0("t", "thresh", "", "threshold, used in 14a reader mode (def 7)"), + arg_int0("i", "high", "", "high threshold, used in 14a sniff mode (def 20)"), + arg_int0("l", "legic", "", "threshold used in Legic mode (def 8)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + struct { + uint8_t threshold; + uint8_t threshold_high; + uint8_t legic_threshold; + } PACKED params; + + params.threshold = arg_get_int_def(ctx, 1, 7); + params.threshold_high = arg_get_int_def(ctx, 2, 20); + params.legic_threshold = arg_get_int_def(ctx, 3, 8); + CLIParserFree(ctx); + + if ((params.threshold < 1) || (params.threshold > 63) || (params.threshold_high < 1) || (params.threshold_high > 63)) { + PrintAndLogEx(ERR, "Thresholds must be between " _YELLOW_("1") " and " _YELLOW_("63")); + return PM3_EINVARG; + } + + clearCommandBuffer(); + SendCommandNG(CMD_HF_ISO14443A_SET_THRESHOLDS, (uint8_t *)¶ms, sizeof(params)); + PrintAndLogEx(SUCCESS, "Thresholds set."); + return PM3_SUCCESS; +} + +static int CmdSetMux(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw setmux", + "Set the ADC mux to a specific value", + "hw setmux --hipkd -> set HIGH PEAK\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_lit0(NULL, "lopkd", "low peak"), + arg_lit0(NULL, "loraw", "low raw"), + arg_lit0(NULL, "hipkd", "high peak"), + arg_lit0(NULL, "hiraw", "high raw"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool lopkd = arg_get_lit(ctx, 1); + bool loraw = arg_get_lit(ctx, 2); + bool hipkd = arg_get_lit(ctx, 3); + bool hiraw = arg_get_lit(ctx, 4); + CLIParserFree(ctx); + + if ((lopkd + loraw + hipkd + hiraw) > 1) { + PrintAndLogEx(INFO, "Can only set one mux"); + return PM3_EINVARG; + } + +#ifdef WITH_FPC_USART + if (loraw || hiraw) { + PrintAndLogEx(INFO, "this ADC mux option is unavailable on RDV4 compiled with FPC USART"); + return PM3_EINVARG; + } +#endif + + uint8_t arg = 0; + if (lopkd) + arg = 0; + else if (loraw) + arg = 1; + else if (hipkd) + arg = 2; + else if (hiraw) + arg = 3; + + clearCommandBuffer(); + SendCommandNG(CMD_SET_ADC_MUX, (uint8_t *)&arg, sizeof(arg)); + return PM3_SUCCESS; +} + +static int CmdStandalone(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw standalone", + "Start standalone mode", + "hw standalone -> start \n" + "hw standalone -a 1 -> start and send arg 1" + ); + + void *argtable[] = { + arg_param_begin, + arg_u64_0("a", "arg", "", "argument byte"), + arg_str0("b", NULL, "", "UniSniff arg: 14a, 14b, 15, iclass"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + struct p { + uint8_t arg; + uint8_t mlen; + uint8_t mode[10]; + } PACKED packet; + + packet.arg = arg_get_u32_def(ctx, 1, 1); + int mlen = 0; + CLIParamStrToBuf(arg_get_str(ctx, 2), packet.mode, sizeof(packet.mode), &mlen); + if (mlen) { + packet.mlen = mlen; + } + CLIParserFree(ctx); + clearCommandBuffer(); + SendCommandNG(CMD_STANDALONE, (uint8_t *)&packet, sizeof(struct p)); + return PM3_SUCCESS; +} + +static int CmdDecay(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw decay", + "Measure HF antenna decay after field-off.\n" + "Captures how quickly the peak-detect capacitor voltage drops\n" + "after the 13.56 MHz field is turned off. Different antenna loading\n" + "(unloaded, booster board, damaged) produces different decay profiles.", + "hw decay\n" + "hw decay --ms 100 --> stabilize for 100ms before measurement\n" + "hw decay --us 5000 --> measure 5ms decay window\n"); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "ms", "", "Field stabilization time in ms (default: 50)"), + arg_int0(NULL, "us", "", "Measurement window in us (default: 2000)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + uint16_t stabilize_ms = arg_get_int_def(ctx, 1, 50); + uint16_t measure_us = arg_get_int_def(ctx, 2, 2000); + CLIParserFree(ctx); + + // Build parameter packet + hf_decay_params_t decay_params = { + .stabilize_ms = stabilize_ms, + .measure_us = measure_us, + }; + + PrintAndLogEx(INFO, "Measuring HF antenna decay..."); + PrintAndLogEx(INFO, " Field stabilization: " _YELLOW_("%d") " ms", stabilize_ms); + PrintAndLogEx(INFO, " Measurement window: " _YELLOW_("%d") " us", measure_us); + + clearCommandBuffer(); + SendCommandNG(CMD_HF_DECAY, (uint8_t *)&decay_params, sizeof(decay_params)); + + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_HF_DECAY, &resp, 5000) == false) { + PrintAndLogEx(WARNING, "Timeout waiting for decay measurement"); + return PM3_ETIMEOUT; + } + + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Decay measurement failed"); + return PM3_ESOFT; + } + + // Parse response header + hf_decay_response_t *decay_resp = (hf_decay_response_t *)resp.data.asBytes; + uint16_t baseline_mv = decay_resp->baseline_mv; + uint16_t num_samples = decay_resp->num_samples; + uint16_t sample_interval_us = decay_resp->sample_interval_us; + uint16_t measure_window_us = decay_resp->measure_window_us; + uint16_t samples[num_samples]; + memcpy(samples, decay_resp->samples_mv, num_samples * sizeof(uint16_t)); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "-------- " _CYAN_("HF Decay Measurement") " ----------"); + PrintAndLogEx(SUCCESS, "Baseline (field on).... " _YELLOW_("%d") " mV (%.2f V)", + baseline_mv, baseline_mv / 1000.0); + PrintAndLogEx(SUCCESS, "Samples captured....... %d", num_samples); + PrintAndLogEx(SUCCESS, "Sample interval........ ~%d us", sample_interval_us); + PrintAndLogEx(SUCCESS, "Total window........... %d us", measure_window_us); + + if (num_samples == 0) { + PrintAndLogEx(WARNING, "No samples captured"); + return PM3_ESOFT; + } + + // Decay samples use fast ADC (reduced S&H) for ~5us/sample resolution. + // Absolute mV values are ~11% of truth due to RC charging limitation, + // but relative decay shape is accurate. Use first sample as 100% reference. + uint16_t ref_mv = samples[0]; + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, " idx | time (us) | raw | %% of peak"); + PrintAndLogEx(INFO, "-----+-----------+-------+-------------"); + + for (uint16_t i = 0; i < num_samples; i++) { + uint32_t time_us = (num_samples > 1) + ? (uint32_t)i * measure_window_us / (num_samples - 1) + : 0; + double pct = (ref_mv > 0) + ? 100.0 * samples[i] / ref_mv + : 0; + PrintAndLogEx(INFO, " %3d | %7d | %5d | %.1f%%", + i, time_us, samples[i], pct); + } + + // Find time to 50% decay (relative to first sample) + uint16_t half_ref = ref_mv / 2; + int t_half_idx = -1; + for (uint16_t i = 0; i < num_samples; i++) { + if (samples[i] <= half_ref) { + t_half_idx = i; + break; + } + } + + PrintAndLogEx(NORMAL, ""); + if (t_half_idx >= 0) { + uint32_t t_half_us = (num_samples > 1) + ? (uint32_t)t_half_idx * measure_window_us / (num_samples - 1) + : 0; + PrintAndLogEx(SUCCESS, "Time to 50%% decay..... ~" _YELLOW_("%d") " us (sample %d)", t_half_us, t_half_idx); + } else { + PrintAndLogEx(INFO, "Voltage did not reach 50%% decay within measurement window"); + } + + uint16_t final_mv = samples[num_samples - 1]; + double final_pct = (ref_mv > 0) ? 100.0 * final_mv / ref_mv : 0; + PrintAndLogEx(SUCCESS, "Final voltage.......... %d raw (%.1f%% of peak)", final_mv, final_pct); + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "Note: decay samples use fast ADC (~5us/sample, relative values)"); + + // Load into graph window + for (uint16_t i = 0; i < num_samples; i++) { + g_GraphBuffer[i] = (int)samples[i]; + } + g_GraphTraceLen = num_samples; + ShowGraphWindow(); + RepaintGraphWindow(); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "Decay curve loaded into graph window (mV vs sample index)"); + PrintAndLogEx(NORMAL, ""); + + return PM3_SUCCESS; +} + +static int CmdTune(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw tune", + "Measure tuning of device antenna. Results shown in graph window.\n" + "This command doesn't actively tune your antennas, \n" + "it's only informative by measuring voltage that the antennas will generate", + "hw tune" + ); + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + +#define NON_VOLTAGE 1000 +#define LF_UNUSABLE_V 2000 +#define LF_MARGINAL_V 10000 +#define HF_UNUSABLE_V 3000 +#define HF_MARGINAL_V 5000 +#define ANTENNA_ERROR 1.00 // current algo has 3% error margin. + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "-------- " _CYAN_("Reminder") " ----------------------------"); + PrintAndLogEx(INFO, "`" _YELLOW_("hw tune") "` doesn't actively tune your antennas."); + PrintAndLogEx(INFO, "It's only informative."); + PrintAndLogEx(INFO, "Measuring antenna characteristics..."); + + // hide demod plot line + g_DemodBufferLen = 0; + setClockGrid(0, 0); + RepaintGraphWindow(); + int timeout = 0; + int timeout_max = 20; + + clearCommandBuffer(); + SendCommandNG(CMD_MEASURE_ANTENNA_TUNING, NULL, 0); + PacketResponseNG resp; + PrintAndLogEx(INPLACE, "% 3i", timeout_max - timeout); + + while (WaitForResponseTimeout(CMD_MEASURE_ANTENNA_TUNING, &resp, 500) == false) { + + fflush(stdout); + if (timeout >= timeout_max) { + PrintAndLogEx(WARNING, "\nNo response from Proxmark3. Aborting..."); + return PM3_ETIMEOUT; + } + + timeout++; + PrintAndLogEx(INPLACE, "% 3i", timeout_max - timeout); + } + + PrintAndLogEx(NORMAL, ""); + + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(WARNING, "Antenna tuning failed"); + return PM3_ESOFT; + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "-------- " _CYAN_("LF Antenna") " ----------"); + // in mVolt + struct p { + uint32_t v_lf134; + uint32_t v_lf125; + uint32_t v_lfconf; + uint32_t v_hf; + uint32_t peak_v; + uint32_t peak_f; + int divisor; + uint8_t results[256]; + } PACKED; + + struct p *package = (struct p *)resp.data.asBytes; + + if (package->v_lf125 > NON_VOLTAGE) + PrintAndLogEx(SUCCESS, "%.2f kHz ........... " _YELLOW_("%5.2f") " V", LF_DIV2FREQ(LF_DIVISOR_125), (package->v_lf125 * ANTENNA_ERROR) / 1000.0); + + if (package->v_lf134 > NON_VOLTAGE) + PrintAndLogEx(SUCCESS, "%.2f kHz ........... " _YELLOW_("%5.2f") " V", LF_DIV2FREQ(LF_DIVISOR_134), (package->v_lf134 * ANTENNA_ERROR) / 1000.0); + + if (package->v_lfconf > NON_VOLTAGE && package->divisor > 0 && package->divisor != LF_DIVISOR_125 && package->divisor != LF_DIVISOR_134) + PrintAndLogEx(SUCCESS, "%.2f kHz ........... " _YELLOW_("%5.2f") " V", LF_DIV2FREQ(package->divisor), (package->v_lfconf * ANTENNA_ERROR) / 1000.0); + + if (package->peak_v > NON_VOLTAGE && package->peak_f > 0) + PrintAndLogEx(SUCCESS, "%.2f kHz optimal.... " _BACK_GREEN_("%5.2f") " V", LF_DIV2FREQ(package->peak_f), (package->peak_v * ANTENNA_ERROR) / 1000.0); + + // Empirical measures in mV + const double vdd_rdv4 = 9000; + const double vdd_other = 5400; + double vdd = IfPm3Rdv4Fw() ? vdd_rdv4 : vdd_other; + + if (package->peak_v > NON_VOLTAGE && package->peak_f > 0) { + + // Q measure with Q=f/delta_f + double v_3db_scaled = (double)(package->peak_v * 0.707) / 512; // /512 == >>9 + uint32_t s2 = 0, s4 = 0; + for (int i = 1; i < 256; i++) { + if ((s2 == 0) && (package->results[i] > v_3db_scaled)) { + s2 = i; + } + if ((s2 != 0) && (package->results[i] < v_3db_scaled)) { + s4 = i; + break; + } + } + + PrintAndLogEx(SUCCESS, ""); + PrintAndLogEx(SUCCESS, "Approx. Q factor measurement"); + double lfq1 = 0; + if (s4 != 0) { + // we got all our points of interest + double a = package->results[s2 - 1]; + double b = package->results[s2]; + double f1 = LF_DIV2FREQ(s2 - 1 + (v_3db_scaled - a) / (b - a)); + double c = package->results[s4 - 1]; + double d = package->results[s4]; + double f2 = LF_DIV2FREQ(s4 - 1 + (c - v_3db_scaled) / (c - d)); + lfq1 = LF_DIV2FREQ(package->peak_f) / (f1 - f2); + PrintAndLogEx(SUCCESS, "Frequency bandwidth... " _YELLOW_("%.1lf"), lfq1); + } + + // Q measure with Vlr=Q*(2*Vdd/pi) + double lfq2 = (double)package->peak_v * 3.14 / 2 / vdd; + PrintAndLogEx(SUCCESS, "Peak voltage.......... " _YELLOW_("%.1lf"), lfq2); + // cross-check results + // TODO DXL pm5 to be covered + if (IfPm5() == false) { + if (lfq1 > 3) { + double approx_vdd = (double)package->peak_v * 3.14 / 2 / lfq1; + // Got 8858 on a RDV4 with large antenna 134/14 + // Got 8761 on a non-RDV4 + const double approx_vdd_other_max = 8840; + + // 1% over threshold and supposedly non-RDV4 + if ((approx_vdd > approx_vdd_other_max * 1.01) && (!IfPm3Rdv4Fw())) { + PrintAndLogEx(WARNING, "Contradicting measures seem to indicate you're running a " _YELLOW_("PM3GENERIC firmware on a RDV4")); + PrintAndLogEx(WARNING, "False positives is possible but please check your setup"); + } + // 1% below threshold and supposedly RDV4 + if ((approx_vdd < approx_vdd_other_max * 0.99) && (IfPm3Rdv4Fw())) { + PrintAndLogEx(WARNING, "Contradicting measures seem to indicate you're running a " _YELLOW_("PM3_RDV4 firmware on a generic device")); + PrintAndLogEx(WARNING, "False positives is possible but please check your setup"); + } + } + } + } + + char judgement[20]; + memset(judgement, 0, sizeof(judgement)); + // LF evaluation + if (package->peak_v < LF_UNUSABLE_V) + snprintf(judgement, sizeof(judgement), _RED_("unusable")); + else if (package->peak_v < LF_MARGINAL_V) + snprintf(judgement, sizeof(judgement), _YELLOW_("marginal")); + else + snprintf(judgement, sizeof(judgement), _GREEN_("ok")); + + // PrintAndLogEx((package->peak_v < LF_UNUSABLE_V) ? WARNING : SUCCESS, "LF antenna ( %s )", judgement); + PrintAndLogEx((package->peak_v < LF_UNUSABLE_V) ? WARNING : SUCCESS, "LF antenna............ %s", judgement); + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "-------- " _CYAN_("HF Antenna") " ----------"); + // HF evaluation + if (package->v_hf > NON_VOLTAGE) { + PrintAndLogEx(SUCCESS, "13.56 MHz............. " _BACK_GREEN_("%5.2f") " V", (package->v_hf * ANTENNA_ERROR) / 1000.0); + } + + memset(judgement, 0, sizeof(judgement)); + + // If HF is unusable or marginal, run a quick decay measurement to check + // for booster board. With a booster, the first fast-ADC decay sample reads + // 50-500 (rapid discharge). Without a booster, it reads >1000. + bool hf_booster_detected = false; + if (!IfPm3Rdv4Fw() && package->v_hf < HF_MARGINAL_V) { + hf_decay_params_t decay_params = { + .stabilize_ms = 50, + .measure_us = 50, + }; + + clearCommandBuffer(); + SendCommandNG(CMD_HF_DECAY, (uint8_t *)&decay_params, sizeof(decay_params)); + + if (WaitForResponseTimeout(CMD_HF_DECAY, &resp, 3000) && resp.status == PM3_SUCCESS) { + hf_decay_response_t *decay_resp = (hf_decay_response_t *)resp.data.asBytes; + if (decay_resp->num_samples > 0) { + uint16_t samples[1]; + memcpy(samples, decay_resp->samples_mv, sizeof(uint16_t)); + if (samples[0] >= 50 && samples[0] <= 500) { + hf_booster_detected = true; + } + } + } + } + + if (hf_booster_detected) { + PrintAndLogEx(SUCCESS, ""); + PrintAndLogEx(SUCCESS, "Your HF antenna measurement shows"); + PrintAndLogEx(SUCCESS, "low voltage that is consistent"); + PrintAndLogEx(SUCCESS, "with the installation of a booster"); + PrintAndLogEx(SUCCESS, "board. If you do not have a"); + PrintAndLogEx(SUCCESS, "booster board installed, either"); + PrintAndLogEx(SUCCESS, "your antenna is malfunctioning or"); + PrintAndLogEx(SUCCESS, "you have a tag on the HF antenna."); + } + + PrintAndLogEx(SUCCESS, ""); + PrintAndLogEx(SUCCESS, "Approx. Q factor measurement"); + + if (package->v_hf >= HF_UNUSABLE_V) { + // Q measure with Vlr=Q*(2*Vdd/pi) + double hfq = (double)package->v_hf * 3.14 / 2 / vdd; + PrintAndLogEx(SUCCESS, "Peak voltage.......... " _YELLOW_("%.1lf"), hfq); + } + + if (package->v_hf < HF_UNUSABLE_V) + snprintf(judgement, sizeof(judgement), _RED_("unusable")); + else if (package->v_hf < HF_MARGINAL_V) + snprintf(judgement, sizeof(judgement), _YELLOW_("marginal")); + else + snprintf(judgement, sizeof(judgement), _GREEN_("ok")); + + PrintAndLogEx((package->v_hf < HF_UNUSABLE_V) ? WARNING : SUCCESS, "HF antenna ( %s )", judgement); + + // If HF voltage is ok/marginal but below 13V, check for + // surface interference via decay measurement. + // Only on PM3 Easy — RDV4 has different voltage divider. + if (!IfPm3Rdv4Fw() && package->v_hf >= HF_MARGINAL_V && package->v_hf < 13000) { + hf_decay_params_t surface_params = { + .stabilize_ms = 50, + .measure_us = 50, + }; + + clearCommandBuffer(); + SendCommandNG(CMD_HF_DECAY, (uint8_t *)&surface_params, sizeof(surface_params)); + + if (WaitForResponseTimeout(CMD_HF_DECAY, &resp, 3000) && resp.status == PM3_SUCCESS) { + hf_decay_response_t *surface_resp = (hf_decay_response_t *)resp.data.asBytes; + if (surface_resp->num_samples > 0) { + uint16_t samples[1]; + memcpy(samples, surface_resp->samples_mv, sizeof(uint16_t)); + if (samples[0] >= 600 && samples[0] <= 900) { + PrintAndLogEx(SUCCESS, ""); + PrintAndLogEx(SUCCESS, "The surface your proxmark is on could"); + PrintAndLogEx(SUCCESS, "contain interfering materials. Try again"); + PrintAndLogEx(SUCCESS, "while holding the proxmark in free space."); + } + } + } + } + + // graph LF measurements + // even here, these values has 3% error. + uint16_t test1 = 0; + for (int i = 0; i < 256; i++) { + g_GraphBuffer[i] = package->results[i] - 128; + test1 += package->results[i]; + } + + if (test1 > 0) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "-------- " _CYAN_("LF tuning graph") " ------------"); + PrintAndLogEx(SUCCESS, "Orange line - divisor %d / %.2f kHz" + , LF_DIVISOR_125 + , LF_DIV2FREQ(LF_DIVISOR_125) + ); + PrintAndLogEx(SUCCESS, "Blue line - divisor %d / %.2f kHz\n\n" + , LF_DIVISOR_134 + , LF_DIV2FREQ(LF_DIVISOR_134) + ); + g_GraphTraceLen = 256; + g_MarkerC.pos = LF_DIVISOR_125; + g_MarkerD.pos = LF_DIVISOR_134; + ShowGraphWindow(); + RepaintGraphWindow(); + } else { + PrintAndLogEx(FAILED, "\nAll values are zero. Not showing LF tuning graph\n\n"); + } + + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(INFO, "Q factor must be measured without tag on the antenna"); + PrintAndLogEx(NORMAL, ""); + return PM3_SUCCESS; +} + +static int CmdVersion(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw version", + "Show version information about the client and the connected Proxmark3", + "hw version" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + pm3_version(true, false); + return PM3_SUCCESS; +} + +static int CmdStatus(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw status", + "Show runtime status information about the connected Proxmark3", + "hw status\n" + "hw status --ms 1000 -> Test connection speed with 1000ms timeout\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0("m", "ms", "", "speed test timeout in micro seconds"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + int32_t speedTestTimeout = arg_get_int_def(ctx, 1, -1); + CLIParserFree(ctx); + + clearCommandBuffer(); + PacketResponseNG resp; + if (speedTestTimeout < 0) { + speedTestTimeout = 0; + SendCommandNG(CMD_STATUS, NULL, 0); + } else { + SendCommandNG(CMD_STATUS, (uint8_t *)&speedTestTimeout, sizeof(speedTestTimeout)); + } + + if (WaitForResponseTimeout(CMD_STATUS, &resp, 2000 + speedTestTimeout) == false) { + PrintAndLogEx(WARNING, "Status command timeout. Communication speed test timed out"); + return PM3_ETIMEOUT; + } + return PM3_SUCCESS; +} + +int handle_tearoff(tearoff_params_t *params, bool verbose) { + + if (params == NULL) + return PM3_EINVARG; + + clearCommandBuffer(); + SendCommandNG(CMD_SET_TEAROFF, (uint8_t *)params, sizeof(tearoff_params_t)); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_SET_TEAROFF, &resp, 500) == false) { + PrintAndLogEx(WARNING, "Tear-off command timeout."); + return PM3_ETIMEOUT; + } + + if (resp.status == PM3_SUCCESS) { + if (params->delay_us > 0 && verbose) + PrintAndLogEx(INFO, "Tear-off hook configured with delay of " _GREEN_("%i us"), params->delay_us); + + if (params->skip > 0 && verbose) + PrintAndLogEx(INFO, "Tear-off hook will be skipped " _YELLOW_("%i times") " before being activated", params->skip); + if (params->skip == 0 && verbose) + PrintAndLogEx(INFO, "Tear-off hook skipping " _GREEN_("disabled")); + + if (params->on && verbose) + PrintAndLogEx(INFO, "Tear-off hook " _GREEN_("enabled")); + + if (params->off && verbose) + PrintAndLogEx(INFO, "Tear-off hook " _RED_("disabled")); + } else if (verbose) + PrintAndLogEx(WARNING, "Tear-off command failed."); + return resp.status; +} + +static int CmdTearoff(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw tearoff", + "Configure a tear-off hook for the next write command supporting tear-off\n" + "After having been triggered by a write command, the tear-off hook is deactivated\n" + "Delay (in us) must be between 1 and 65535 (65ms). Precision is about 1/3us.", + "hw tearoff --delay 1200 --> define delay of 1200us\n" + "hw tearoff --on --> (re)activate a previously defined delay\n" + "hw tearoff --off --> deactivate a previously activated but not yet triggered hook\n" + "hw tearoff --list --> list commands implementing tear-off hooks\n"); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "delay", "", "Delay in us before triggering tear-off, must be between 1 and 65535"), + arg_lit0(NULL, "on", "Activate tear-off hook"), + arg_lit0(NULL, "off", "Deactivate tear-off hook"), + arg_int0(NULL, "skip", "", "Skip N triggers before activating the hook"), + arg_lit0("s", "silent", "less verbose output"), + arg_lit0(NULL, "list", "List commands implementing tear-off hooks"), + arg_param_end + }; + + CLIExecWithReturn(ctx, Cmd, argtable, false); + tearoff_params_t params; + int delay = arg_get_int_def(ctx, 1, -1); + params.on = arg_get_lit(ctx, 2); + params.off = arg_get_lit(ctx, 3); + int skip = arg_get_int_def(ctx, 4, -1); + bool silent = arg_get_lit(ctx, 5); + bool list = arg_get_lit(ctx, 6); + CLIParserFree(ctx); + + if (list) { + PrintAndLogEx(INFO, "Commands implementing tear-off hooks:"); + PrintAndLogEx(INFO, " hf 14a raw"); + PrintAndLogEx(INFO, " hf 14b apdu"); + PrintAndLogEx(INFO, " hf 14b raw"); + PrintAndLogEx(INFO, " hf 15 raw"); + PrintAndLogEx(INFO, " hf iclass creditepurse"); + PrintAndLogEx(INFO, " hf iclass wrbl"); + PrintAndLogEx(INFO, " hf mf wrbl"); + PrintAndLogEx(INFO, " hf mfu wrbl (with --skip 3)"); + PrintAndLogEx(INFO, " hf topaz wrbl"); + PrintAndLogEx(INFO, " lf em 4x05 write"); + PrintAndLogEx(INFO, " lf em 4x50 wrbl"); + PrintAndLogEx(INFO, " lf em 4x50 wrpwd"); + PrintAndLogEx(INFO, " lf hitag wrbl"); + PrintAndLogEx(INFO, " lf hitag hts wrbl"); + PrintAndLogEx(INFO, ""); + PrintAndLogEx(INFO, "See also commands implementing tearing-off on their own:"); + PrintAndLogEx(INFO, " lf em 4x05_unlock"); + PrintAndLogEx(INFO, " lf t55xx dangerraw"); + PrintAndLogEx(INFO, " hf iclass tear"); + PrintAndLogEx(INFO, " hf iclass blacktears"); + PrintAndLogEx(INFO, " hf mfu otptear"); + PrintAndLogEx(INFO, " Standalone mode HF_ST25_TEAROFF"); + return PM3_SUCCESS; + } + + if (delay != -1) { + // 65535 is where tearoff_params_t.delay_us runs out, not where the + // timer does. The old 43000 was the point past which the PWM tick + // count wrapped into 16 bits and the delay silently came up short. + if ((delay < 1) || (delay > 65535)) { + PrintAndLogEx(WARNING, "You can't set delay out of 1..65535 range!"); + return PM3_EINVARG; + } + } else { + delay = 0; // will be ignored by ARM + } + + params.delay_us = delay; + + if (skip != -1) { + if ((skip < 0) || (skip > 127)) { + PrintAndLogEx(WARNING, "You can't set skip out of 0..127 range!"); + return PM3_EINVARG; + } + } + + params.skip = skip; + + if (params.on && params.off) { + PrintAndLogEx(WARNING, "You can't set both --on and --off!"); + return PM3_EINVARG; + } + + return handle_tearoff(¶ms, !silent); +} + +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"); + + 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"); + return PM3_EINVARG; + } + uint8_t payload = off ? 0 : 1; // default (neither flag) = enable + + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_AUTOOFF, &payload, sizeof(payload)); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_PM5_BWM_AUTOOFF, &resp, 2500) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5?)"); + return PM3_ETIMEOUT; + } + if (resp.status == PM3_ENOTIMPL) { + PrintAndLogEx(WARNING, "firmware built without auto power-off support"); + return resp.status; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "failed to set auto power-off"); + return resp.status; + } + PrintAndLogEx(SUCCESS, "Auto power-off %s.", payload ? _GREEN_("enabled") : _YELLOW_("disabled")); + return PM3_SUCCESS; +} + +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"); + + 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) { + uint8_t q[1] = { BWM_WIFI_ACTION_STATUS }; + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_WIFI, q, sizeof(q)); + PacketResponseNG r; + if (WaitForResponseTimeout(CMD_PM5_BWM_WIFI, &r, 5000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); + return PM3_ETIMEOUT; + } + if (r.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "could not query BWM WiFi status (BWM present?)"); + return r.status; + } + uint8_t state = (r.length >= 1) ? r.data.asBytes[0] : 0xFF; + uint32_t ip = 0; + if (r.length >= 5) { + ip = r.data.asBytes[1] | (r.data.asBytes[2] << 8) | (r.data.asBytes[3] << 16) | ((uint32_t)r.data.asBytes[4] << 24); + } + switch (state) { + case 0xFF: + PrintAndLogEx(INFO, "BWM WiFi disabled (BLE-only). Bring it up with " _YELLOW_("hw bwmwifi --ssid --pwd ")); + break; + case 2: // connected + if (ip) { + PrintAndLogEx(SUCCESS, "BWM WiFi connected, IP " _YELLOW_("%u.%u.%u.%u"), + ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); + PrintAndLogEx(HINT, "Connect with: " _YELLOW_("pm3 -p tcp:%u.%u.%u.%u:"), + ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); + } else { + PrintAndLogEx(INFO, "BWM WiFi associated, waiting for a DHCP lease..."); + } + break; + case 1: // connecting + PrintAndLogEx(INFO, "BWM WiFi connecting..."); + break; + case 3: // reconnect wait + PrintAndLogEx(INFO, "BWM WiFi reconnecting..."); + break; + case 4: // task stopped + PrintAndLogEx(INFO, "BWM WiFi connect task stopped"); + break; + case 0: // disconnected + default: + PrintAndLogEx(INFO, "BWM WiFi configured but not connected"); + break; + } + return PM3_SUCCESS; + } + + if (stop) { + uint8_t off[1] = { BWM_WIFI_ACTION_STOP }; + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_WIFI, off, sizeof(off)); + PacketResponseNG r; + if (WaitForResponseTimeout(CMD_PM5_BWM_WIFI, &r, 5000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); + return PM3_ETIMEOUT; + } + if (r.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "failed to disable BWM WiFi"); + return r.status; + } + PrintAndLogEx(SUCCESS, "BWM WiFi disabled (back to BLE-only)"); + return PM3_SUCCESS; + } + + if (ssid_len == 0) { + PrintAndLogEx(FAILED, "an SSID is required (or use --stop to tear down)"); + return PM3_EINVARG; + } + if (port < 1 || port > 65535) { + PrintAndLogEx(FAILED, "port must be 1..65535"); + return PM3_EINVARG; + } + + // payload: [action:u8][port:u16 LE][ssid\0][pwd\0][hostname\0] + uint8_t data[200] = {0}; + int n = 0; + data[n++] = BWM_WIFI_ACTION_START; + data[n++] = (uint8_t)(port & 0xFF); + data[n++] = (uint8_t)((port >> 8) & 0xFF); + memcpy(&data[n], ssid, ssid_len); + n += ssid_len; + data[n++] = 0; + memcpy(&data[n], pwd, pwd_len); + n += pwd_len; + data[n++] = 0; + memcpy(&data[n], host, host_len); + n += host_len; + data[n++] = 0; + + PrintAndLogEx(INFO, "Bringing up BWM WiFi (SSID \"%s\", port %d)... this can take ~15s", ssid, port); + + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_WIFI, data, n); + PacketResponseNG resp; + // ARM blocks during join + DHCP wait, so allow a long client timeout + if (WaitForResponseTimeout(CMD_PM5_BWM_WIFI, &resp, 60000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); + return PM3_ETIMEOUT; + } + 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")); + return resp.status; + } + + uint32_t ip = resp.data.asDwords[0]; + PrintAndLogEx(SUCCESS, "BWM on WiFi at %u.%u.%u.%u", + ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); + PrintAndLogEx(HINT, "Connect with: " _YELLOW_("pm3 -p tcp:%u.%u.%u.%u:%d"), + ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF, port); + PrintAndLogEx(HINT, "Or by name (router-dependent): " _YELLOW_("pm3 -p tcp:%s:%d"), host, port); + return PM3_SUCCESS; +} + +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"); + + 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"); + return PM3_EINVARG; + } + uint8_t payload = off ? 0 : 1; // default (neither flag) = enable + PrintAndLogEx(INFO, "%s BWM battery charging...", off ? "Disabling" : "Enabling"); + + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_CHARGE_EN, &payload, sizeof(payload)); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_PM5_BWM_CHARGE_EN, &resp, 2500) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "charger did not respond (check BWM present)"); + return resp.status; + } + PrintAndLogEx(SUCCESS, "Charging %s. Verify with " _YELLOW_("hw status") ".", + off ? "disabled" : "enabled"); + if (off == false) { + PrintAndLogEx(HINT, "Reverts on the charger watchdog (~160 s) if not serviced."); + } + return PM3_SUCCESS; +} + +static int CmdBwmVchg(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwmvchg", + "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"); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "mv", "", "charge voltage in mV (default 4100, clamped 3600..4200)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + int mv = arg_get_int_def(ctx, 1, 4100); + CLIParserFree(ctx); + + if (mv < 3600 || mv > 4200) { + PrintAndLogEx(WARNING, "charge voltage out of range (3600..4200 mV): %d", mv); + return PM3_EINVARG; + } + + uint8_t payload[2] = { (uint8_t)(mv & 0xFF), (uint8_t)((mv >> 8) & 0xFF) }; + PrintAndLogEx(INFO, "Setting BWM charge voltage to " _YELLOW_("%d mV") "...", mv); + + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_SET_VCHG, payload, sizeof(payload)); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_PM5_BWM_SET_VCHG, &resp, 5000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "failed to set charge voltage - check BWM present"); + return resp.status; + } + uint16_t applied = (resp.length >= 2) ? (resp.data.asBytes[0] | (resp.data.asBytes[1] << 8)) : 0; + PrintAndLogEx(SUCCESS, "Charge voltage set to " _YELLOW_("%u.%03u V") " (nearest 15 mV step).", applied / 1000, applied % 1000); + return PM3_SUCCESS; +} + +static int CmdBwmSetCap(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bwmsetcap", + "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"); + + void *argtable[] = { + arg_param_begin, + arg_int0(NULL, "cap", "", "design capacity in mAh (default 500)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + int cap = arg_get_int_def(ctx, 1, 500); + CLIParserFree(ctx); + + if (cap <= 0 || cap > 32000) { + PrintAndLogEx(WARNING, "capacity out of range: %d mAh", cap); + return PM3_EINVARG; + } + + uint8_t payload[2] = { (uint8_t)(cap & 0xFF), (uint8_t)((cap >> 8) & 0xFF) }; + PrintAndLogEx(INFO, "Programming BWM gauge design capacity to " _YELLOW_("%d mAh") "...", cap); + PrintAndLogEx(INFO, "Run this " _YELLOW_("once") "; then perform a full charge/discharge learning cycle."); + + clearCommandBuffer(); + SendCommandNG(CMD_PM5_BWM_SET_CAP, payload, sizeof(payload)); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_PM5_BWM_SET_CAP, &resp, 5000) == false) { + PrintAndLogEx(WARNING, "command timeout (is this a PM5 with a BWM fitted?)"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(FAILED, "gauge provisioning failed - check BWM present and gauge unsealed"); + return resp.status; + } + PrintAndLogEx(SUCCESS, "Design capacity programmed. `hw status` should now report sane capacity."); + return PM3_SUCCESS; +} + +static int CmdTia(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw tia", + "Trigger a Timing Interval Acquisition to re-adjust the RealTimeCounter divider", + "hw tia" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + + PrintAndLogEx(INFO, "Triggering new Timing Interval Acquisition (TIA)..."); + clearCommandBuffer(); + SendCommandNG(CMD_TIA, NULL, 0); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_TIA, &resp, 2000) == false) { + PrintAndLogEx(WARNING, "TIA command timeout. You probably need to unplug the Proxmark3."); + return PM3_ETIMEOUT; + } + PrintAndLogEx(INFO, "TIA done."); + return PM3_SUCCESS; +} + +static int CmdTimeout(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw timeout", + "Set the communication timeout on the client side", + "hw timeout --> Show current timeout\n" + "hw timeout -m 20 --> Set the timeout to 20ms\n" + "hw timeout --ms 500 --> Set the timeout to 500ms\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_int0("m", "ms", "", "timeout in micro seconds"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + int32_t arg = arg_get_int_def(ctx, 1, -1); + CLIParserFree(ctx); + + uint32_t oldTimeout = uart_get_timeouts(); + + // timeout is not given/invalid, just show the current timeout then return + if (arg < 0) { + PrintAndLogEx(INFO, "Current communication timeout... " _GREEN_("%u") " ms", oldTimeout); + return PM3_SUCCESS; + } + + uint32_t newTimeout = arg; + // UART_USB_CLIENT_RX_TIMEOUT_MS is considered as the minimum required timeout. + if (newTimeout < UART_USB_CLIENT_RX_TIMEOUT_MS) { + PrintAndLogEx(WARNING, "Timeout less than %u ms might cause errors.", UART_USB_CLIENT_RX_TIMEOUT_MS); + } else if (newTimeout > 5000) { + PrintAndLogEx(WARNING, "Timeout greater than 5000 ms makes the client unresponsive."); + } + uart_reconfigure_timeouts(newTimeout); + PrintAndLogEx(INFO, "Old communication timeout... %u ms", oldTimeout); + PrintAndLogEx(INFO, "New communication timeout... " _GREEN_("%u") " ms", newTimeout); + return PM3_SUCCESS; +} + +static int CmdPing(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw ping", + "Test if the Proxmark3 is responsive", + "hw ping\n" + "hw ping --len 32" + ); + + void *argtable[] = { + arg_param_begin, + arg_u64_0("l", "len", "", "length of payload to send"), + arg_param_end + }; + + CLIExecWithReturn(ctx, Cmd, argtable, true); + uint32_t len = arg_get_u32_def(ctx, 1, 32); + CLIParserFree(ctx); + + if (len > PM3_CMD_DATA_SIZE) + len = PM3_CMD_DATA_SIZE; + + if (len) { + PrintAndLogEx(INFO, "Ping sent with payload len... " _YELLOW_("%d"), len); + } else { + PrintAndLogEx(INFO, "Ping sent"); + } + + clearCommandBuffer(); + PacketResponseNG resp; + uint8_t data[PM3_CMD_DATA_SIZE] = {0}; + + for (uint16_t i = 0; i < len; i++) { + data[i] = i & 0xFF; + } + + uint64_t tms = msclock(); + SendCommandNG(CMD_PING, data, len); + if (WaitForResponseTimeout(CMD_PING, &resp, 1000)) { + tms = msclock() - tms; + if (len) { + bool error = (memcmp(data, resp.data.asBytes, len) != 0); + PrintAndLogEx((error) ? ERR : SUCCESS, "Ping response " _GREEN_("received") + " in " _YELLOW_("%" PRIu64) " ms and content ( %s )", + tms, error ? _RED_("fail") : _GREEN_("ok")); + } else { + PrintAndLogEx(SUCCESS, "Ping response " _GREEN_("received") + " in " _YELLOW_("%" PRIu64) " ms", tms); + } + } else + PrintAndLogEx(WARNING, "Ping response " _RED_("timeout")); + return PM3_SUCCESS; +} + +static int CmdConnect(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw connect", + "Connects to a Proxmark3 device via specified serial port.\n" + "Baudrate here is only for physical UART or UART-BT, NOT for USB-CDC or blue shark add-on", + "hw connect -p " SERIAL_PORT_EXAMPLE_H "\n" + "hw connect -p "SERIAL_PORT_EXAMPLE_H" -b 115200" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0("p", "port", "", "Serial port to connect to, else retry the last used one"), + arg_u64_0("b", "baud", "", "Baudrate"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + char port[FILE_PATH_SIZE] = {0}; + int p_len = sizeof(port) - 1; // CLIGetStrWithReturn does not guarantee string to be null-terminated; + CLIGetStrWithReturn(ctx, 1, (uint8_t *)port, &p_len); + uint32_t baudrate = arg_get_u32_def(ctx, 2, USART_BAUD_RATE); + CLIParserFree(ctx); + + if (baudrate == 0) { + PrintAndLogEx(WARNING, "Baudrate can't be zero"); + return PM3_EINVARG; + } + + // default back to previous used serial port + if (strlen(port) == 0) { + if (strlen(g_conn.serial_port_name) == 0) { + PrintAndLogEx(WARNING, "Must specify a serial port"); + return PM3_EINVARG; + } + memcpy(port, g_conn.serial_port_name, sizeof(port)); + } + + if (g_session.pm3_present) { + CloseProxmark(g_session.current_device); + } + + // 10 second timeout + OpenProxmark(&g_session.current_device, port, false, 10, false, baudrate); + + if (g_session.pm3_present && (TestProxmark(g_session.current_device) != PM3_SUCCESS)) { + PrintAndLogEx(ERR, _RED_("ERROR:") " cannot communicate with the Proxmark3\n"); + CloseProxmark(g_session.current_device); + return PM3_ENOTTY; + } + return PM3_SUCCESS; +} + +static int CmdBreak(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw break", + "send break loop package", + "hw break\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + clearCommandBuffer(); + SendCommandNG(CMD_BREAK_LOOP, NULL, 0); + return PM3_SUCCESS; +} + +static int CmdBootloader(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw bootloader", + "Reboot Proxmark3 into bootloader mode", + "hw bootloader\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + CLIParserFree(ctx); + clearCommandBuffer(); + flash_reboot_bootloader(g_conn.serial_port_name, false); + return PM3_SUCCESS; +} + +int set_fpga_mode(uint8_t mode) { + if (mode < FPGA_BITSTREAM_MIN || mode > FPGA_BITSTREAM_MAX) { + return PM3_EINVARG; + } + uint8_t d[] = {mode}; + clearCommandBuffer(); + SendCommandNG(CMD_SET_FPGAMODE, d, sizeof(d)); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_SET_FPGAMODE, &resp, 1000) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "failed to set FPGA mode"); + } + return resp.status; +} + +static int CmdPM5Ant(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw ant_pm5", + "Control the antennal of pm5", + "hw ant_pm5 --set -> Write the data of IO data register\n" + "hw ant_pm5 -m --set -> Write the data of IO map register\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_lit0("m", "map", "Write the IO map register"), + arg_u64_0("s", "set", "", "Set PM5 antenna"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + bool rw_map = arg_get_lit(ctx, 1); + uint64_t io = arg_get_u64_def(ctx, 2, -1); + CLIParserFree(ctx); + + PacketResponseNG resp; + + // Read the current value of the register before writing + struct { + uint8_t reg_type; // 0 is io reg, 1 is map reg. + } PACKED payload_read = { + .reg_type = rw_map ? 1 : 0, + }; + clearCommandBuffer(); + SendCommandNG(CMD_ANT_CONTROL_READ, (uint8_t *)&payload_read, sizeof(payload_read)); + if (WaitForResponseTimeout(CMD_ANT_CONTROL_READ, &resp, 1000) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "failed to read PM5 antenna register"); + return resp.status; + } + PrintAndLogEx(INFO, "PM5 antenna register read: 0x%02X", resp.data.asBytes[0]); + + // Write the new value to the register(If need) + if (io != (uint64_t) -1) { + struct { + uint8_t data; + uint8_t reg_type; // 0 is io reg, 1 is map reg. + } PACKED payload_write = { + .reg_type = rw_map ? 1 : 0, + .data = io & 0xFF, + }; + clearCommandBuffer(); + SendCommandNG(CMD_ANT_CONTROL_WRITE, (uint8_t *)&payload_write, sizeof(payload_write)); + if (WaitForResponseTimeout(CMD_ANT_CONTROL_WRITE, &resp, 1000) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "failed to write PM5 antenna register"); + return resp.status; + } + PrintAndLogEx(INFO, "PM5 antenna register written: 0x%02X", payload_write.data); + } + + return PM3_SUCCESS; +} + +static int CmdDeviceFactoryData(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw factorydata", + "Get/Set the factory data for Device", + "hw factorydata --load -> Write the factory data to device from file\n" + "hw factorydata -> Read and parse the factory data from device\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0(NULL, "load", "", "Load factory data from file to device"), + arg_param_end + }; + + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int fnlen = 0; + char filename[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); + + CLIParserFree(ctx); + + // Read the factory data from device + clearCommandBuffer(); + SendCommandNG(CMD_EEPROM_FACTORY_INFO_READ, NULL, 0); + PacketResponseNG resp; + if (WaitForResponseTimeout(CMD_EEPROM_FACTORY_INFO_READ, &resp, 1000) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "failed to read factory data, maybe eeprom unavailable."); + return resp.status; + } + // Parse the factory data + if (resp.length) { + proxmark5_factory_info_v1_t *factory = (proxmark5_factory_info_v1_t *)resp.data.asBytes; + PrintAndLogEx(INFO, "Factory data read from device:"); + PrintAndLogEx(INFO, " - Data raw(hex) : 0x%s", sprint_hex_inrow(resp.data.asBytes, resp.length)); + PrintAndLogEx(INFO, " - Version : %d", factory->factory_info_version); + PrintAndLogEx(INFO, " - Signature : %s", + sprint_hex_inrow(factory->ecdsa_secp256k1_signature, sizeof(factory->ecdsa_secp256k1_signature))); + PrintAndLogEx(INFO, " - Timestamp : %" PRIu64, factory->info.unix_timestamp); + PrintAndLogEx(INFO, " - Chip Unique ID : %s", + sprint_hex_inrow(factory->info.chip_unique_id, sizeof(factory->info.chip_unique_id))); + PrintAndLogEx(INFO, " - Production ID : %" PRIu32, factory->info.production_id); + PrintAndLogEx(INFO, " - Hardware Version : %" PRIu32, factory->info.hardware_version); + PrintAndLogEx(INFO, " - AES key : %s", + sprint_hex_inrow(factory->info.aes_key, sizeof(factory->info.aes_key))); + } + + // If data is provided, it needs to be written + if (fnlen) { + // Load the file content into data buffer + uint8_t *data = NULL; + size_t datalen = 0; + int res = loadFile_safe(filename, ".bin", (void **)&data, &datalen); + if (res != PM3_SUCCESS) { + free(data); + return PM3_EFILE; + } + + if (datalen != resp.length) { + PrintAndLogEx(WARNING, _RED_("The length of the data to write (%zu) does not match the length " + "of the factory data read from device (%u)."), datalen, (unsigned int)resp.length); + free(data); + return PM3_EINVARG; + } + // Write the data to the device + clearCommandBuffer(); + SendCommandNG(CMD_EEPROM_FACTORY_INFO_WRITE, data, datalen); + PacketResponseNG resp_write; + if (WaitForResponseTimeout(CMD_EEPROM_FACTORY_INFO_WRITE, &resp_write, 1000) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + free(data); + return PM3_ETIMEOUT; + } + if (resp_write.status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "failed to write factory data, maybe eeprom unavailable."); + free(data); + return resp_write.status; + } + // Verify the data write + clearCommandBuffer(); + SendCommandNG(CMD_EEPROM_FACTORY_INFO_READ, NULL, 0); + if (WaitForResponseTimeout(CMD_EEPROM_FACTORY_INFO_READ, &resp, 1000) == false) { + PrintAndLogEx(WARNING, "command execution time out"); + free(data); + return PM3_ETIMEOUT; + } + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "failed to read factory data after write, maybe eeprom unavailable."); + free(data); + return resp.status; + } + bool verify_result = false; + if (resp.length == datalen) { + verify_result = memcmp(resp.data.asBytes, data, datalen) == 0; + } + if (verify_result) { + PrintAndLogEx(SUCCESS, "Factory data written and verified successfully."); + } else { + PrintAndLogEx(ERR, "Factory data verification failed after write."); + free(data); + return PM3_EFAILED; + } + free(data); + } + + return PM3_SUCCESS; +} + +static int CmdPM5QCTest(const char *Cmd) { + + CLIParserContext *ctx; + CLIParserInit(&ctx, "hw qc_pm5", "QC Test for the PM5", + "hw qc_pm5 -> run QC test with default 20 second timeout\n" + "hw qc_pm5 -t 3 -> run QC test with a 3 second timeout"); + + void *argtable[] = { + arg_param_begin, + arg_u64_0("t", "timeout", "", "test sequence timeout in seconds (default 20)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + uint32_t timeout_ms = arg_get_u32_def(ctx, 1, 20); + timeout_ms *= 1000; + CLIParserFree(ctx); + + if (timeout_ms == 0) { + PrintAndLogEx(ERR, "timeout must be greater than zero"); + return PM3_EINVARG; + } + + PrintAndLogEx(INFO, "Performing QC test for the PM5..."); + + clearCommandBuffer(); + SendCommandNG(CMD_PM5_QC_TEST, (uint8_t *)&timeout_ms, sizeof(timeout_ms)); + + PacketResponseNG resp; + // wait a bit longer than the device side sequence timeout, with headroom for RTC drift + if (WaitForResponseTimeout(CMD_PM5_QC_TEST, &resp, timeout_ms + (timeout_ms / 5) + 1000) == false) { + SendCommandNG(CMD_BREAK_LOOP, NULL, 0); + PrintAndLogEx(WARNING, "command execution time out"); + return PM3_ETIMEOUT; + } + + if (resp.status != PM3_SUCCESS) { + PrintAndLogEx(ERR, "failed to perform QC test on PM5, failed item: %d", resp.data.asBytes[0]); + return resp.status; + } + PrintAndLogEx(INFO, "PM5 QC test successful."); + return PM3_SUCCESS; +} + +// 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) { + PacketResponseNG resp; + + // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) + 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { + PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); + return PM3_EFAILED; + } + 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) { + msleep(10); + 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, 15000); + if (!got || resp.status != PM3_SUCCESS) { + PrintAndLogEx(NORMAL, ""); + if (!got) { + PrintAndLogEx(WARNING, "OTA write stalled at offset %zu (no response)", sent); + } else { + PrintAndLogEx(WARNING, "OTA write rejected at offset %zu (status %d)", sent, resp.status); + } + free(buf); + return PM3_EFAILED; + } + sent += n; + print_progress(sent, fwlen, STYLE_MIXED); ///// DEBUG TEST FOR USB timeout + } + free(buf); + printf("\n"); + 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 == false) { + // All data was sent and OTA_END was issued. esp_ota_end + set_boot_partition + // is slow, so its ack is easily lost even though the flash completed - this + // is the case that used to discard a finished image and restart. Signal + // "reached END, unconfirmed" so the caller verifies by version instead. + return PM3_ETIMEOUT; + } + PrintAndLogEx(WARNING, "OTA finalize rejected (status %d)", resp.status); + return PM3_EFAILED; +} + +// 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 bwmupgrade", + "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 bwmupgrade -f bwm_esp32.bin"); + void *argtable[] = { + arg_param_begin, + arg_str1("f", "file", "", "ESP32 firmware image (.bin)"), + 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); + 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 = 3; + for (int attempt = 1; attempt <= max_attempts; attempt++) { + if (attempt > 1) { + PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); + } + int res = bwm_ota_once(fw, fwlen); + + // 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(10000); // 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?)"); + 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 command_t CommandTable[] = { + {"help", CmdHelp, AlwaysAvailable, "This help"}, + {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Operation") " -----------------------"}, + {"detectreader", CmdDetectReader, IfPm3Present, "Detect external reader field"}, + {"status", CmdStatus, IfPm3Present, "Show runtime status information about the connected Proxmark3"}, + {"tearoff", CmdTearoff, IfPm3Present, "Program a tearoff hook for the next command supporting tearoff"}, + {"timeout", CmdTimeout, AlwaysAvailable, "Set the communication timeout on the client side"}, + {"version", CmdVersion, AlwaysAvailable, "Show version information about the client and Proxmark3"}, + {"-------------", CmdHelp, AlwaysAvailable, "----------------------- " _CYAN_("Hardware") " -----------------------"}, + {"break", CmdBreak, IfPm3Present, "Send break loop usb command"}, + {"bootloader", CmdBootloader, IfPm3Present, "Reboot into bootloader mode"}, + {"connect", CmdConnect, AlwaysAvailable, "Connect to the device via serial port"}, + {"dbg", CmdDbg, IfPm3Present, "Set device side debug level"}, + {"fpga", CmdFPGA, IfPm3Present, "Fpga commands"}, + {"fpgaoff", CmdFPGAOff, IfPm3Present, "Turn off FPGA on device"}, + {"ant_pm5", CmdPM5Ant, IfPm5StdAnt, "Control the antennal of pm5"}, + {"qc_pm5", CmdPM5QCTest, IfPm5, "Perform QC test for the PM5"}, + {"factorydata", CmdDeviceFactoryData, IfI2cEeprom, "Get/Set the factory data for Device"}, + {"lcd", CmdLCD, IfPm3Lcd, "Send command/data to LCD"}, + {"lcdreset", CmdLCDReset, IfPm3Lcd, "Hardware reset LCD"}, + {"ping", CmdPing, IfPm3Present, "Test if the Proxmark3 is responsive"}, + {"readmem", CmdReadmem, IfPm3Present, "Read from MCU flash"}, + {"reset", CmdReset, IfPm3Present, "Reset the device"}, + {"setlfdivisor", CmdSetDivisor, IfPm3Lf, "Drive LF antenna at 12MHz / (divisor + 1)"}, + {"sethfthresh", CmdSetHFThreshold, IfPm3Iso14443a, "Set thresholds in HF/14a mode"}, + {"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)"}, + {"bwmupgrade", CmdBWMUpgrade, IfPm5, "Reflash BWM (ESP32) firmware over the BWM link, no header (PM5)"}, + {"tune", CmdTune, IfPm3Lf, "Measure tuning of device antenna"}, + {"decay", CmdDecay, IfPm3Present, "Measure HF antenna decay after field-off"}, + {NULL, NULL, NULL, NULL} +}; + +static int CmdHelp(const char *Cmd) { + (void)Cmd; // Cmd is not used so far + CmdsHelp(CommandTable); + return PM3_SUCCESS; +} + +int CmdHW(const char *Cmd) { + clearCommandBuffer(); + return CmdsParse(CommandTable, Cmd); +} + +#if defined(__MINGW64__) +#define PM3CLIENTCOMPILER "MinGW-w64 " +#elif defined(__MINGW32__) +#define PM3CLIENTCOMPILER "MinGW " +#elif defined(__clang__) +#define PM3CLIENTCOMPILER "Clang/LLVM " +#elif defined(__GNUC__) || defined(__GNUG__) +#define PM3CLIENTCOMPILER "GCC " +#else +#define PM3CLIENTCOMPILER "unknown compiler " +#endif + +#if defined(__APPLE__) || defined(__MACH__) +#define PM3HOSTOS "OSX" +#elif defined(__ANDROID__) || defined(ANDROID) +// must be tested before __linux__ +#define PM3HOSTOS "Android" +#elif defined(__linux__) +#define PM3HOSTOS "Linux" +#elif defined(__FreeBSD__) +#define PM3HOSTOS "FreeBSD" +#elif defined(__NetBSD__) +#define PM3HOSTOS "NetBSD" +#elif defined(__OpenBSD__) +#define PM3HOSTOS "OpenBSD" +#elif defined(__CYGWIN__) +#define PM3HOSTOS "Cygwin" +#elif defined(_WIN64) || defined(__WIN64__) +// must be tested before _WIN32 +#define PM3HOSTOS "Windows (64b)" +#elif defined(_WIN32) || defined(__WIN32__) +#define PM3HOSTOS "Windows (32b)" +#else +#define PM3HOSTOS "unknown" +#endif + +#if defined(__x86_64__) +#define PM3HOSTARCH "x86_64" +#elif defined(__i386__) +#define PM3HOSTARCH "x86" +#elif defined(__aarch64__) +#define PM3HOSTARCH "aarch64" +#elif defined(__arm__) +#define PM3HOSTARCH "arm" +#elif defined(__powerpc64__) +#define PM3HOSTARCH "powerpc64" +#elif defined(__mips__) +#define PM3HOSTARCH "mips" +#else +#define PM3HOSTARCH "unknown" +#endif + +void pm3_version_short(void) { + // PrintAndLogEx(NORMAL, " [ " _CYAN_("Proxmark3 RFID instrument") " ]"); + if (IfPm5()) { + PrintAndLogEx(NORMAL, " [ " _CYAN_(_URL_("https://github.com/RfidResearchGroup/proxmark3", "Proxmark5")) " ]"); + } else { + PrintAndLogEx(NORMAL, " [ " _CYAN_(_URL_("https://github.com/RfidResearchGroup/proxmark3", "Proxmark3")) " ]"); + } + PrintAndLogEx(NORMAL, ""); + + if (g_session.pm3_present) { + + PacketResponseNG resp; + clearCommandBuffer(); + SendCommandNG(CMD_VERSION, NULL, 0); + + if (WaitForResponseTimeout(CMD_VERSION, &resp, 1000)) { + + struct p { + uint32_t id; + uint32_t section_size; + uint32_t versionstr_len; + char versionstr[PM3_CMD_DATA_SIZE - 12]; + } PACKED; + + struct p *payload = (struct p *)&resp.data.asBytes; + + // Flash size (bytes) is appended after the version string by newer + // firmware; 0 if the device didn't send it (older firmware). + uint32_t flash_size = 0; + if (resp.length >= 12 + payload->versionstr_len + sizeof(uint32_t)) { + memcpy(&flash_size, payload->versionstr + payload->versionstr_len, sizeof(flash_size)); + } + + lookup_chipid_short(payload->id, payload->section_size, flash_size); + + if (IfPm5()) { + PrintAndLogEx(NORMAL, " Target.... %s", _YELLOW_("PM5")); + } else if (IfPm3Rdv4Fw()) { + + // validate signature data + rdv40_validation_t mem; + signature_e type; + + if (pm3_get_signature(&mem) == PM3_SUCCESS) { + if (pm3_validate(&mem, &type) == PM3_SUCCESS) { + + if (type == SIGN_RDV4) { + PrintAndLogEx(NORMAL, " Target.... %s", _YELLOW_("RDV4")); + } else if (type == SIGN_GENERIC) { + PrintAndLogEx(NORMAL, " Target.... %s", _YELLOW_("GENERIC")); + } else { + PrintAndLogEx(NORMAL, " Target.... %s", _RED_("device / fw mismatch")); + } + } + } + } else { + PrintAndLogEx(NORMAL, " Target.... %s", _YELLOW_("PM3 GENERIC")); + } + PrintAndLogEx(NORMAL, ""); + + // client + char temp[PM3_CMD_DATA_SIZE - 12]; // same limit as for ARM image + format_version_information_short(temp, sizeof(temp), &g_version_information); + PrintAndLogEx(NORMAL, " Client.... %s", temp); + + bool armsrc_mismatch = false; + char *ptr = strstr(payload->versionstr, "OS......... "); + if (ptr != NULL) { + ptr = strstr(ptr, "\n"); + if ((ptr != NULL) && (strlen(g_version_information.armsrc) == 9)) { + if (strncmp(ptr - 9, g_version_information.armsrc, 9) != 0) { + armsrc_mismatch = true; + } + } + } + + // bootrom + ptr = strstr(payload->versionstr, "Bootrom.... "); + if (ptr != NULL) { + char *ptr_end = strstr(ptr, "\n"); + if (ptr_end != NULL) { + uint8_t len = ptr_end - 12 - ptr; + PrintAndLogEx(NORMAL, " Bootrom... %.*s", len, ptr + 12); + } + } + + // os: + ptr = strstr(payload->versionstr, "OS......... "); + if (ptr != NULL) { + char *ptr_end = strstr(ptr, "\n"); + if (ptr_end != NULL) { + uint8_t len = ptr_end - 12 - ptr; + PrintAndLogEx(NORMAL, " OS........ %.*s", len, ptr + 12); + } + } + PrintAndLogEx(NORMAL, ""); + + if (armsrc_mismatch) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(WARNING, " --> " _RED_("ARM firmware does not match the source at the time the client was compiled")); + PrintAndLogEx(WARNING, " --> Make sure to flash a correct and up-to-date version"); + } + } + } + PrintAndLogEx(NORMAL, ""); +} + +void pm3_version(bool verbose, bool oneliner) { + + char temp[PM3_CMD_DATA_SIZE - 12]; // same limit as for ARM image + + if (oneliner) { + // For "proxmark3 -v", simple printf, avoid logging + FormatVersionInformation(temp, sizeof(temp), "Client: ", &g_version_information); + PrintAndLogEx(NORMAL, "%s compiler: " PM3CLIENTCOMPILER __VERSION__ " OS:" PM3HOSTOS " ARCH:" PM3HOSTARCH "\n", temp); + return; + } + + if (!verbose) { + return; + } + + PrintAndLogEx(NORMAL, "\n [ " _CYAN_("%s") " ]", IfPm5() ? "Proxmark5" : "Proxmark3"); + PrintAndLogEx(NORMAL, "\n [ " _YELLOW_("Client") " ]"); + FormatVersionInformation(temp, sizeof(temp), " ", &g_version_information); + PrintAndLogEx(NORMAL, "%s", temp); + PrintAndLogEx(NORMAL, " Compiler.................. " PM3CLIENTCOMPILER __VERSION__); + PrintAndLogEx(NORMAL, " Platform.................. " PM3HOSTOS " / " PM3HOSTARCH); +#if defined(HAVE_READLINE) + PrintAndLogEx(NORMAL, " Readline support.......... " _GREEN_("present")); +#elif defined(HAVE_LINENOISE) + PrintAndLogEx(NORMAL, " Linenoise support......... " _GREEN_("present")); +#else + PrintAndLogEx(NORMAL, " Readline/Linenoise support." _YELLOW_("absent")); +#endif +#ifdef HAVE_GUI + PrintAndLogEx(NORMAL, " QT GUI support............ " _GREEN_("present")); +#else + PrintAndLogEx(NORMAL, " QT GUI support............ " _YELLOW_("absent")); +#endif +#ifdef HAVE_BLUEZ + PrintAndLogEx(NORMAL, " Native BT support......... " _GREEN_("present")); +#else + PrintAndLogEx(NORMAL, " Native BT support......... " _YELLOW_("absent")); +#endif + +#ifdef HAVE_PYTHON +#ifndef PY_VERSION +#define PY_VERSION "unknown version" +#endif + PrintAndLogEx(NORMAL, " Python script support..... " _GREEN_("present") " ( " _YELLOW_(PY_VERSION) " )"); +#else + PrintAndLogEx(NORMAL, " Python script support..... " _YELLOW_("absent")); +#endif +#ifdef HAVE_PYTHON_SWIG + PrintAndLogEx(NORMAL, " Python SWIG support....... " _GREEN_("present")); +#else + PrintAndLogEx(NORMAL, " Python SWIG support....... " _YELLOW_("absent")); +#endif + PrintAndLogEx(NORMAL, " Lua script support........ " _GREEN_("present") " ( " _YELLOW_("%s.%s.%s") " )", LUA_VERSION_MAJOR, LUA_VERSION_MINOR, LUA_VERSION_RELEASE); +#ifdef HAVE_LUA_SWIG + PrintAndLogEx(NORMAL, " Lua SWIG support.......... " _GREEN_("present")); +#else + PrintAndLogEx(NORMAL, " Lua SWIG support.......... " _YELLOW_("absent")); +#endif + + if (g_session.pm3_present) { + PrintAndLogEx(NORMAL, "\n [ " _YELLOW_("Model") " ]"); + + PacketResponseNG resp; + clearCommandBuffer(); + SendCommandNG(CMD_VERSION, NULL, 0); + + if (WaitForResponseTimeout(CMD_VERSION, &resp, 1000)) { + if (IfPm5()) { + PrintAndLogEx(NORMAL, " Firmware.................. " _GREEN_("PM5")); + PrintAndLogEx(NORMAL, " External flash............ %s", IfPm3Flash() ? _GREEN_("present") : _YELLOW_("absent")); + } else if (IfPm3Rdv4Fw()) { + + // validate signature data + rdv40_validation_t mem; + signature_e type; + + if (pm3_get_signature(&mem) == PM3_SUCCESS) { + if (pm3_validate(&mem, &type) == PM3_SUCCESS) { + + if (type == SIGN_RDV4) { + PrintAndLogEx(NORMAL, " Device.................... " _GREEN_("RDV4")); + PrintAndLogEx(NORMAL, " Firmware.................. " _GREEN_("RDV4")); + } else if (type == SIGN_GENERIC) { + PrintAndLogEx(NORMAL, " Device.................... ", _GREEN_("GENERIC")); + PrintAndLogEx(NORMAL, " Firmware.................. ", _GREEN_("GENERIC")); + } else { + PrintAndLogEx(NORMAL, " Device.................... " _RED_("Bad signature detected!")); + PrintAndLogEx(NORMAL, " Firmware.................. " _YELLOW_("N/A")); + } + } + } + + PrintAndLogEx(NORMAL, " External flash............ %s", IfPm3Flash() ? _GREEN_("present") : _YELLOW_("absent")); + PrintAndLogEx(NORMAL, " Smartcard reader.......... %s", IfPm3Smartcard() ? _GREEN_("present") : _YELLOW_("absent")); + PrintAndLogEx(NORMAL, " FPC USART for BT add-on... %s", IfPm3FpcUsartHost() ? _GREEN_("present") : _YELLOW_("absent")); + } else { + PrintAndLogEx(NORMAL, " Firmware.................. %s", _YELLOW_("PM3 GENERIC")); + if (IfPm3Flash()) { + PrintAndLogEx(NORMAL, " External flash............ %s", _GREEN_("present")); + } + + if (IfPm3FpcUsartHost()) { + PrintAndLogEx(NORMAL, " FPC USART for BT add-on... %s", _GREEN_("present")); + } + } + + if (IfPm3FpcUsartDevFromUsb()) { + PrintAndLogEx(NORMAL, " FPC USART for developer... %s", _GREEN_("present")); + } + + PrintAndLogEx(NORMAL, ""); + + struct p { + uint32_t id; + uint32_t section_size; + uint32_t versionstr_len; + char versionstr[PM3_CMD_DATA_SIZE - 12]; + } PACKED; + + struct p *payload = (struct p *)&resp.data.asBytes; + + bool armsrc_mismatch = false; + char *ptr = strstr(payload->versionstr, "OS......... "); + if (ptr != NULL) { + ptr = strstr(ptr, "\n"); + if ((ptr != NULL) && (strlen(g_version_information.armsrc) == 9)) { + if (strncmp(ptr - 9, g_version_information.armsrc, 9) != 0) { + armsrc_mismatch = true; + } + } + } + PrintAndLogEx(NORMAL, payload->versionstr); + // PM5 doesn't report a built-in FPGA version (Gowin bitstream is loaded + // externally), so skip the Xilinx FPGA_TYPE match check for it. + if (!IfPm5() && strstr(payload->versionstr, FPGA_TYPE) == NULL) { + PrintAndLogEx(NORMAL, " FPGA firmware... %s", _RED_("chip mismatch")); + } + + // Flash size (bytes) is appended after the version string by newer + // firmware; 0 if the device didn't send it (older firmware). + uint32_t flash_size = 0; + if (resp.length >= 12 + payload->versionstr_len + sizeof(uint32_t)) { + memcpy(&flash_size, payload->versionstr + payload->versionstr_len, sizeof(flash_size)); + } + + lookupChipID(payload->id, payload->section_size, flash_size); + + // Get unique id of mainchip + clearCommandBuffer(); + SendCommandNG(CMD_MAIN_CHIP_UNIQUEID, NULL, 0); + if (WaitForResponseTimeout(CMD_MAIN_CHIP_UNIQUEID, &resp, 1000)) { + if (resp.length) { // Some processor maybe no unique id. + char *uniqueid_hex = sprint_hex_inrow(resp.data.asBytes, resp.length); + PrintAndLogEx(NORMAL, " --= Processor Unique ID: " _YELLOW_("0x%s"), uniqueid_hex); + } + } + + if (armsrc_mismatch) { + PrintAndLogEx(NORMAL, ""); + PrintAndLogEx(WARNING, _RED_("ARM firmware does not match the source at the time the client was compiled")); + PrintAndLogEx(WARNING, "Make sure to flash a correct and up-to-date version"); + } + } + } + PrintAndLogEx(NORMAL, ""); } From 81a3c61058e5aa1e0dce43ee532c14493043048c Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:31:47 +0200 Subject: [PATCH 77/89] Add progress bar and write delay to OTA function Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 52 ++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 798f88547..d08a53d3a 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2294,17 +2294,26 @@ 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) { +static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms) { PacketResponseNG resp; - + clearCommandBuffer(); // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); @@ -2321,7 +2330,6 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { } size_t sent = 0; while (sent < fwlen) { - msleep(10); size_t n = MIN(maxchunk, fwlen - sent); buf[0] = BWM_OTA_ACTION_WRITE; memcpy(buf + 1, fw + sent, n); @@ -2339,10 +2347,15 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { return PM3_EFAILED; } sent += n; - print_progress(sent, fwlen, STYLE_MIXED); ///// DEBUG TEST FOR USB timeout + progressbar(sent, fwlen, STYLE_MIXED); + + // Pace the stream. The client->AT32 hop (USB/BLE) is far faster than the + // AT32->ESP UART) + if (write_delay_ms) { + msleep(write_delay_ms); + } } free(buf); - printf("\n"); PrintAndLogEx(NORMAL, ""); // END: finalize + set the new boot partition @@ -2353,15 +2366,14 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen) { if (got_end && (resp.status == PM3_SUCCESS)) { return PM3_SUCCESS; } - if (got_end == false) { - // All data was sent and OTA_END was issued. esp_ota_end + set_boot_partition - // is slow, so its ack is easily lost even though the flash completed - this - // is the case that used to discard a finished image and restart. Signal - // "reached END, unconfirmed" so the caller verifies by version instead. - return PM3_ETIMEOUT; + if (got_end) { + // The BWM answered END with an error. + PrintAndLogEx(WARNING, "OTA finalize rejected (status %d) - data was lost in transit", resp.status); + PrintAndLogEx(HINT, "Try a per-write delay: " _YELLOW_("hw bwmupgrade -f --delay 10")); + return PM3_EFAILED; } - PrintAndLogEx(WARNING, "OTA finalize rejected (status %d)", resp.status); - return PM3_EFAILED; + // No answer at all. + return PM3_ETIMEOUT; } // Query the BWM's running firmware version string (APP_CMD_GET_VERSION_INFO). @@ -2370,7 +2382,7 @@ static int bwm_get_version(char *out, size_t outlen) { 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)) { + if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &r, 500) == false) || (r.status != PM3_SUCCESS)) { return PM3_EFAILED; } uint16_t n = (r.length < (uint16_t)(outlen - 1)) ? r.length : (uint16_t)(outlen - 1); @@ -2389,12 +2401,14 @@ static int CmdBWMUpgrade(const char *Cmd) { 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 10)"), 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, 10); CLIParserFree(ctx); if (fnlen == 0) { @@ -2445,7 +2459,7 @@ static int CmdBWMUpgrade(const char *Cmd) { if (attempt > 1) { PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); } - int res = bwm_ota_once(fw, fwlen); + 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)) { @@ -2457,11 +2471,8 @@ static int CmdBWMUpgrade(const char *Cmd) { 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(10000); // reboot + re-negotiate baud + re-link + msleep(500); // reboot + re-negotiate baud + re-link char ver_after[64] = {0}; bool have_after = (bwm_get_version(ver_after, sizeof(ver_after)) == PM3_SUCCESS); @@ -2483,6 +2494,7 @@ static int CmdBWMUpgrade(const char *Cmd) { // 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; } From 7e30a10bd4339af86719a7ef37036f148cb7760e Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:46:05 +0200 Subject: [PATCH 78/89] Add newline at end of bwm_uart_at32.c Fix formatting issue by adding a newline at the end of the file. Signed-off-by: Niel Nielsen From edd5ada64a779830b5e22d8d2703f66fc0ec4bd2 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:47:35 +0200 Subject: [PATCH 79/89] Increase response timeout and update comments in cmdhw.c Increase timeout for response when querying BWM version and adjust comments for clarity. Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index d08a53d3a..2762011b0 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2310,6 +2310,7 @@ static void progressbar(long sent, long total, int style) { static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms) { PacketResponseNG resp; clearCommandBuffer(); + // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) uint8_t beg[5] = { BWM_OTA_ACTION_BEGIN, (uint8_t)(fwlen & 0xFF), (uint8_t)((fwlen >> 8) & 0xFF), @@ -2350,7 +2351,10 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms progressbar(sent, fwlen, STYLE_MIXED); // Pace the stream. The client->AT32 hop (USB/BLE) is far faster than the - // AT32->ESP UART) + // 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); } @@ -2367,12 +2371,17 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms return PM3_SUCCESS; } if (got_end) { - // The BWM answered END with an error. + // 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 bwmupgrade -f --delay 10")); return PM3_EFAILED; } - // No answer at all. + // 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; } @@ -2382,7 +2391,7 @@ static int bwm_get_version(char *out, size_t outlen) { clearCommandBuffer(); SendCommandNG(CMD_PM5_BWM_ESP_OTA, a, sizeof(a)); PacketResponseNG r; - if ((WaitForResponseTimeout(CMD_PM5_BWM_ESP_OTA, &r, 500) == false) || (r.status != PM3_SUCCESS)) { + 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); @@ -2471,8 +2480,11 @@ static int CmdBWMUpgrade(const char *Cmd) { 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(500); // reboot + re-negotiate baud + re-link + 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); From 9d122041357ca017e26a408e71dfd6267e901541 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:56:04 +0200 Subject: [PATCH 80/89] Change BWM_OTA_CHUNK_MAX to 240 Updated maximum firmware bytes per WRITE action from 2048 to 240. Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 68d021f6b..54be972ed 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -891,7 +891,7 @@ typedef struct { #define BWM_OTA_ACTION_VERSION 0x04 #define BWM_OTA_ACTION_REBOOT 0x05 // Max firmware bytes per WRITE action. -#define BWM_OTA_CHUNK_MAX 2048 +#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 From beb5ee7106390909748f5deec25f679f13203b60 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 09:58:40 +0200 Subject: [PATCH 81/89] Update BWM_OTA_BAUD to 115200 Changed the OTA baud rate to a slower, more reliable speed. Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 23f861adb..299f038c1 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -77,7 +77,7 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); #define BWM_CMD_REBOOT 1803 // Run the OTA at a slow, reliable baud (restored to the fast rate at end/abort). #ifndef BWM_OTA_BAUD -#define BWM_OTA_BAUD 460800 +#define BWM_OTA_BAUD 115200 #endif #define BWM_CMD_GET_VERSION_INFO 1000 // resp: running firmware version string int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen); From 14df0485e8678d74c5b7129f2ab15b65594cb7ec Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 10:03:04 +0200 Subject: [PATCH 82/89] Update bwm_wifi.h Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 299f038c1..37c137be6 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -77,7 +77,7 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); #define BWM_CMD_REBOOT 1803 // Run the OTA at a slow, reliable baud (restored to the fast rate at end/abort). #ifndef BWM_OTA_BAUD -#define BWM_OTA_BAUD 115200 +#define BWM_OTA_BAUD 921600 #endif #define BWM_CMD_GET_VERSION_INFO 1000 // resp: running firmware version string int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen); From 5f568289123a0545246ed794afe1a8f01037cff4 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 10:05:50 +0200 Subject: [PATCH 83/89] Change BWM OTA baud rate from 921600 to 460000 Signed-off-by: Niel Nielsen --- armsrc/bwm_wifi.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index 37c137be6..f62be2e09 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -77,7 +77,7 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); #define BWM_CMD_REBOOT 1803 // Run the OTA at a slow, reliable baud (restored to the fast rate at end/abort). #ifndef BWM_OTA_BAUD -#define BWM_OTA_BAUD 921600 +#define BWM_OTA_BAUD 460000 #endif #define BWM_CMD_GET_VERSION_INFO 1000 // resp: running firmware version string int bwm_esp_get_version(uint8_t *buf, uint16_t *buflen); From d4e375f50b34b3da4be477ded43a0c1bb064dd30 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Fri, 4 Sep 2026 11:57:28 +0200 Subject: [PATCH 84/89] Move all bmw commands to subgroups hw bwmautooff hw bwm autooff hw bwmcharge hw bwm charge hw bwmsetcap hw bwm setcap hw bwmupgrade hw bwm upgrade hw bwmvchg hw bwm vchg hw bwmwifi hw bwm wifi Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 79 +++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 2762011b0..a11993698 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -1509,14 +1509,14 @@ static int CmdTearoff(const char *Cmd) { static int CmdBwmAutoOff(const char *Cmd) { CLIParserContext *ctx; - CLIParserInit(&ctx, "hw bwmautooff", + 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 bwmautooff --off --> disable auto power-off\n" - "hw bwmautooff --on --> re-enable auto power-off"); + "hw bwm autooff --off --> disable auto power-off\n" + "hw bwm autooff --on --> re-enable auto power-off"); void *argtable[] = { arg_param_begin, @@ -1556,12 +1556,12 @@ static int CmdBwmAutoOff(const char *Cmd) { static int CmdBWMWifi(const char *Cmd) { CLIParserContext *ctx; - CLIParserInit(&ctx, "hw bwmwifi", + 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.", - "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"); + "hw bwm wifi --ssid Home --pwd secret --> port 7777\n" + "hw bwm wifi --ssid Home --pwd secret --port 9000\n" + "hw bwm wifi --status --> show connection state + IP"); void *argtable[] = { arg_param_begin, @@ -1616,7 +1616,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) { @@ -1699,7 +1699,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; } @@ -1714,13 +1714,13 @@ static int CmdBWMWifi(const char *Cmd) { static int CmdBwmCharge(const char *Cmd) { CLIParserContext *ctx; - CLIParserInit(&ctx, "hw bwmcharge", + 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 bwmcharge -on --> enable charging\n" - "hw bwmcharge --off --> disable charging"); + "hw bwm charge --on --> enable charging\n" + "hw bwm charge --off --> disable charging"); void *argtable[] = { arg_param_begin, @@ -1761,14 +1761,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 +1805,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, @@ -2309,12 +2309,12 @@ static void progressbar(long sent, long total, int style) { // 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; - clearCommandBuffer(); // BEGIN: tell the BWM how many bytes are coming (it erases the target partition) 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); @@ -2376,7 +2376,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms // 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 bwmupgrade -f --delay 10")); + PrintAndLogEx(HINT, "Try a per-write delay: " _YELLOW_("hw bwm upgrade -f --delay 10")); return PM3_EFAILED; } // No answer at all. Over BLE the END auto-reboot drops the link before the ack @@ -2402,11 +2402,11 @@ static int bwm_get_version(char *out, size_t outlen) { static int CmdBWMUpgrade(const char *Cmd) { CLIParserContext *ctx; - CLIParserInit(&ctx, "hw bwmupgrade", + 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 bwmupgrade -f bwm_esp32.bin"); + "hw bwm upgrade -f bwm_esp32.bin"); void *argtable[] = { arg_param_begin, arg_str1("f", "file", "", "ESP32 firmware image (.bin)"), @@ -2463,7 +2463,9 @@ static int CmdBWMUpgrade(const char *Cmd) { } // No resume (DEV.md 8.4): a chunk lost mid-transfer restarts the whole upload. - const int max_attempts = 3; + 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) { PrintAndLogEx(INFO, "restarting OTA from the beginning (attempt " _YELLOW_("%d") "/%d)", attempt, max_attempts); @@ -2521,6 +2523,30 @@ static int CmdBWMUpgrade(const char *Cmd) { 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") " -----------------------"}, @@ -2549,12 +2575,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)"}, - {"bwmupgrade", CmdBWMUpgrade, IfPm5, "Reflash BWM (ESP32) firmware over the BWM link, no header (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} From 22969ec79ad94ab00df97d1fb1fc741de66bfaee Mon Sep 17 00:00:00 2001 From: Will Date: Fri, 4 Sep 2026 08:24:16 -0400 Subject: [PATCH 85/89] Give BWM OTA begin enough time to erase, and stop retuning UART for the transfer Co-authored-by: Cursor --- armsrc/bwm_wifi.c | 48 ++++++++++++++++++++++++++++++++++------------ armsrc/bwm_wifi.h | 12 +++++++++--- client/src/cmdhw.c | 35 +++++++++++++++++++++++++-------- 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/armsrc/bwm_wifi.c b/armsrc/bwm_wifi.c index 9b580c56e..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))); @@ -304,16 +326,20 @@ int bwm_esp_ota_begin(uint32_t total_size) { uint8_t off = 0; (void)bwm_cmd(BWM_CMD_LOG_FORWARD_ENABLE, &off, 1, NULL, NULL, 500); - // Drop the AT32<->ESP link to a slow, forgiving baud for the transfer. An OTA - // is one-shot so speed is irrelevant, and 921600 is marginal against the - // flash-write / BLE contention that drops the odd frame -> random timeouts. - (void)bwm_fwd_negotiate_baud(BWM_OTA_BAUD); + // 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, 15000); + 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) { @@ -322,25 +348,23 @@ int bwm_esp_ota_write(const uint8_t *data, uint16_t len) { // 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, 15000); + return bwm_cmd(BWM_CMD_OTA_WRITE, data, len, NULL, NULL, BWM_OTA_WRITE_TIMEOUT_MS); } int bwm_esp_ota_end(void) { - // OTA_END goes out at the slow OTA baud (both ends still there); only after it - // do we restore the fast link and log forwarding (both set in _begin). int r = bwm_cmd(BWM_CMD_OTA_END, NULL, 0, NULL, NULL, 20000); - (void)bwm_fwd_negotiate_baud(BWM_UART_BAUD_TARGET); 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) { - // OTA gave up mid-transfer: restore the fast link + logs so the module is - // usable again. The ESP's incomplete OTA state is discarded by the next BEGIN. - (void)bwm_fwd_negotiate_baud(BWM_UART_BAUD_TARGET); + // 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; } diff --git a/armsrc/bwm_wifi.h b/armsrc/bwm_wifi.h index f62be2e09..d9528137c 100644 --- a/armsrc/bwm_wifi.h +++ b/armsrc/bwm_wifi.h @@ -75,11 +75,17 @@ int bwm_wifi_forward_status(uint8_t *state, uint32_t *ip_out); // 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 -// Run the OTA at a slow, reliable baud (restored to the fast rate at end/abort). -#ifndef BWM_OTA_BAUD -#define BWM_OTA_BAUD 460000 +// 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); diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index a11993698..258fad109 100644 --- a/client/src/cmdhw.c +++ b/client/src/cmdhw.c @@ -2310,15 +2310,23 @@ static void progressbar(long sent, long total, int style) { 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 (it erases the target partition) + // 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, 20000) == false) || (resp.status != PM3_SUCCESS)) { - PrintAndLogEx(FAILED, "OTA begin failed (is a responsive BWM fitted?)"); - return PM3_EFAILED; + 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); @@ -2336,11 +2344,13 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms 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, 15000); + 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); } @@ -2376,7 +2386,7 @@ static int bwm_ota_once(const uint8_t *fw, size_t fwlen, uint32_t write_delay_ms // 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 10")); + 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 @@ -2410,14 +2420,14 @@ static int CmdBWMUpgrade(const char *Cmd) { 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 10)"), + 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, 10); + uint32_t write_delay_ms = (uint32_t)arg_get_int_def(ctx, 2, 20); CLIParserFree(ctx); if (fnlen == 0) { @@ -2468,7 +2478,16 @@ static int CmdBWMUpgrade(const char *Cmd) { // 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); From 96e13303c86c148d11104ac26d0559e9463c65be Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 5 Sep 2026 08:33:03 +0200 Subject: [PATCH 86/89] Update PM3_FPC_MAX_DATA to 2048 Increase maximum data size for FPC from 240 to 2048 bytes. Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index d5851a7fc..e02e867cc 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -35,10 +35,7 @@ // the bootloader only speaks OLD #define PM3_CMD_DATA_SIZE_OLD 512 -// Over the BWM/FPC link the forward buffers (AT32 DMA ring, ESP UART RX) are -// small, so a full PM3_CMD_DATA_SIZE frame overruns them. Cap the payload the -// device advertises and sends when replying via FPC. USB is unaffected. -#define PM3_FPC_MAX_DATA 240 +#define PM3_FPC_MAX_DATA 2048 typedef struct { uint64_t cmd; From eb5b9eb5619f2088607de1e27b9781c379e2b928 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 5 Sep 2026 09:12:39 +0200 Subject: [PATCH 87/89] Added removed comment again Added removed comment again Signed-off-by: Niel Nielsen --- include/pm3_cmd.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index e02e867cc..452a2cbc7 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -35,6 +35,9 @@ // the bootloader only speaks OLD #define PM3_CMD_DATA_SIZE_OLD 512 +// Over the BWM/FPC link the forward buffers (AT32 DMA ring, ESP UART RX) are +// small, so a full PM3_CMD_DATA_SIZE frame overruns them. Cap the payload the +// device advertises and sends when replying via FPC. USB is unaffected. #define PM3_FPC_MAX_DATA 2048 typedef struct { From ea4d90ebe4e015c3b3aaae00842fdc2205df230a Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 5 Sep 2026 10:09:23 +0200 Subject: [PATCH 88/89] Refactor BWM command handling for clarity and structure Signed-off-by: Niel Nielsen --- client/src/cmdhw.c | 190 ++++++++++++++++++++++++--------------------- 1 file changed, 101 insertions(+), 89 deletions(-) diff --git a/client/src/cmdhw.c b/client/src/cmdhw.c index 258fad109..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 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"); + // 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 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.", - "hw bwm wifi --ssid Home --pwd secret --> port 7777\n" - "hw bwm wifi --ssid Home --pwd secret --port 9000\n" - "hw bwm wifi --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)); @@ -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 bwm wifi --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 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 --on --> enable charging\n" - "hw bwm charge --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(); From 9dfc7aacf418b0bce7d30db2fce9c89e30d9a767 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sat, 5 Sep 2026 10:10:48 +0200 Subject: [PATCH 89/89] Refactor command syntax in PM5-BWM-USAGE.md Updated command syntax and formatting for clarity. Signed-off-by: Niel Nielsen --- doc/md/PM5_Start_Here/PM5-BWM-USAGE.md | 111 +++++++++++++++++-------- 1 file changed, 78 insertions(+), 33 deletions(-) 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