feat: resilient boot + full polish pass
Firmware:
- nfc_engine: add nfc_engine_try_init() (non-fatal, sets s_pn532_ready),
nfc_engine_try_reattach() (soft re-init, skips bus re-init),
nfc_engine_is_ready() accessor
- main: replace ESP_ERROR_CHECK(nfc_engine_init) with nfc_engine_try_init;
device boots and serves web UI even with no PN532 connected
- app_net: add reconnect_task — every 5s retries nfc_engine_try_reattach()
and broadcasts {"channel":"pn532","payload":{"connected":true}} over WS
- app_net: scan_loop_task skips polling when !nfc_engine_is_ready()
- app_net/api_status: always emit pn532Connected bool; null-guard pn532 fw object
- find_sector_hit / program_classic_snapshot_locked: null-guard cJSON array items
- session_capture: abort if xSemaphoreCreateMutex() returns NULL
Web:
- NfcWsContext: track pn532Connected state from WS pn532 channel + status fetch on connect
- App.tsx: live HeaderBadge (LIVE/NO RF/WAIT) replacing static text
- Dashboard: READY/SEARCHING pill with fw version when available
- api.ts: add pn532Connected to Status type
- toast.tsx: fix ID collision (Date.now + Math.random)
- Capture: surface status fetch errors
- ReadAnalyze: add error feedback for readUl when no data returned
- WriteClone: busy state on both write buttons
- RawConsole: toast when frame returns error not response
- Emulate: validate hex before send (non-empty, even length, hex chars only)
- Brute: warn and skip invalid custom key lines
Made-with: Cursor
This commit is contained in:
@@ -368,6 +368,7 @@ static cJSON *find_sector_hit(const cJSON *attack, int sector)
|
||||
int n = cJSON_GetArraySize(hits);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON *it = cJSON_GetArrayItem(hits, i);
|
||||
if (!it) continue;
|
||||
cJSON *s = cJSON_GetObjectItemCaseSensitive(it, "sector");
|
||||
if (cJSON_IsNumber(s) && (int)cJSON_GetNumberValue(s) == sector) {
|
||||
return it;
|
||||
@@ -587,6 +588,7 @@ static cJSON *program_classic_snapshot_locked(const nfc_tag_info_t *tag, const c
|
||||
int sectors_n = cJSON_GetArraySize(sectors);
|
||||
for (int si = 0; si < sectors_n; si++) {
|
||||
cJSON *sec = cJSON_GetArrayItem(sectors, si);
|
||||
if (!sec) { skipped++; continue; }
|
||||
cJSON *blocks = cJSON_GetObjectItemCaseSensitive(sec, "blocksHex");
|
||||
cJSON *first = cJSON_GetObjectItemCaseSensitive(sec, "firstBlock");
|
||||
cJSON *count = cJSON_GetObjectItemCaseSensitive(sec, "blockCount");
|
||||
@@ -661,16 +663,22 @@ static esp_err_t api_status(httpd_req_t *req)
|
||||
wifi_mode_t mode;
|
||||
esp_wifi_get_mode(&mode);
|
||||
cJSON_AddNumberToObject(o, "wifiMode", mode);
|
||||
bool pn532_ok = nfc_engine_is_ready();
|
||||
cJSON_AddBoolToObject(o, "pn532Connected", pn532_ok);
|
||||
if (pn532_ok) {
|
||||
uint8_t ic = 0, hi = 0, lo = 0;
|
||||
nfc_access_lock();
|
||||
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
||||
cJSON *pn = cJSON_CreateObject();
|
||||
if (pn) {
|
||||
cJSON_AddNumberToObject(pn, "ic", ic);
|
||||
cJSON_AddNumberToObject(pn, "fwHi", hi);
|
||||
cJSON_AddNumberToObject(pn, "fwLo", lo);
|
||||
cJSON_AddItemToObject(o, "pn532", pn);
|
||||
}
|
||||
}
|
||||
nfc_access_unlock();
|
||||
}
|
||||
cJSON_AddBoolToObject(o, "scanning", s_scan);
|
||||
cJSON_AddBoolToObject(o, "targetActive", s_target_active);
|
||||
size_t cap_u = 0;
|
||||
@@ -1710,12 +1718,33 @@ static esp_err_t ws_handler(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void reconnect_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
if (!nfc_engine_is_ready()) {
|
||||
nfc_access_lock();
|
||||
bool ok = nfc_engine_try_reattach();
|
||||
nfc_access_unlock();
|
||||
if (ok) {
|
||||
ESP_LOGI(TAG, "PN532 reattached — broadcasting status");
|
||||
app_net_broadcast_json("pn532", "{\"connected\":true}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void scan_loop_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
nfc_tag_info_t last;
|
||||
memset(&last, 0, sizeof(last));
|
||||
while (1) {
|
||||
if (!nfc_engine_is_ready()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
continue;
|
||||
}
|
||||
if (!s_scan) {
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
continue;
|
||||
@@ -2049,5 +2078,8 @@ esp_err_t app_net_init(void)
|
||||
s_server = NULL;
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
if (xTaskCreate(reconnect_task, "pn532_reconnect", 4096, NULL, 3, NULL) != pdPASS) {
|
||||
ESP_LOGW(TAG, "reconnect task create failed — hot-plug retry disabled");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,19 @@ typedef struct {
|
||||
bool key_b;
|
||||
} nfc_mifare_key_t;
|
||||
|
||||
/** Legacy: full init including transport — aborts on failure via caller's ESP_ERROR_CHECK. */
|
||||
esp_err_t nfc_engine_init(void);
|
||||
|
||||
/** Non-fatal first-boot init. Returns true if PN532 is present and configured. */
|
||||
bool nfc_engine_try_init(void);
|
||||
|
||||
/** Soft re-attach after transport is already open — skips bus re-init, tries chip commands.
|
||||
* Call from a background retry loop; always safe to call even if already ready. */
|
||||
bool nfc_engine_try_reattach(void);
|
||||
|
||||
/** Returns true when the PN532 was successfully initialised (or re-attached). */
|
||||
bool nfc_engine_is_ready(void);
|
||||
|
||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out);
|
||||
bool nfc_tag_is_mifare_classic(const nfc_tag_info_t *tag);
|
||||
bool nfc_tag_is_mifare_classic_4k(const nfc_tag_info_t *tag);
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
static const char *TAG = "nfc_engine";
|
||||
|
||||
static uint8_t s_tg = 1;
|
||||
static volatile bool s_pn532_ready = false;
|
||||
|
||||
bool nfc_engine_is_ready(void) { return s_pn532_ready; }
|
||||
|
||||
static void hint_type(nfc_tag_info_t *t)
|
||||
{
|
||||
@@ -102,9 +105,51 @@ esp_err_t nfc_engine_init(void)
|
||||
if (pn532_rf_max_retries() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "RF max retries config failed");
|
||||
}
|
||||
s_pn532_ready = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool nfc_engine_try_init(void)
|
||||
{
|
||||
esp_err_t e = pn532_core_init();
|
||||
if (e != ESP_OK) {
|
||||
ESP_LOGW(TAG, "PN532 not found (%s) — AP running, will retry every 5 s", esp_err_to_name(e));
|
||||
s_pn532_ready = false;
|
||||
return false;
|
||||
}
|
||||
e = pn532_sam_config_normal();
|
||||
if (e != ESP_OK) {
|
||||
ESP_LOGW(TAG, "SAM config: %s", esp_err_to_name(e));
|
||||
}
|
||||
uint8_t ic = 0, hi = 0, lo = 0;
|
||||
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "PN532 fw ic=0x%02x %u.%u", ic, hi, lo);
|
||||
}
|
||||
if (pn532_rf_max_retries() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "RF max retries config failed");
|
||||
}
|
||||
s_pn532_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool nfc_engine_try_reattach(void)
|
||||
{
|
||||
/* Transport already open — just ping the chip and re-apply configuration. */
|
||||
esp_err_t e = pn532_sam_config_normal();
|
||||
if (e != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
uint8_t ic = 0, hi = 0, lo = 0;
|
||||
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "PN532 reattached ic=0x%02x %u.%u", ic, hi, lo);
|
||||
}
|
||||
if (pn532_rf_max_retries() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "RF max retries config failed");
|
||||
}
|
||||
s_pn532_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out)
|
||||
{
|
||||
if (!out) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "nfc_engine/session_capture.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_log.h"
|
||||
#include <string.h>
|
||||
|
||||
#define SESSION_CAPTURE_BYTES (48 * 1024)
|
||||
@@ -15,6 +16,10 @@ static SemaphoreHandle_t s_mu;
|
||||
void session_capture_init(void)
|
||||
{
|
||||
s_mu = xSemaphoreCreateMutex();
|
||||
if (!s_mu) {
|
||||
ESP_LOGE("session_capture", "mutex create failed — aborting");
|
||||
abort();
|
||||
}
|
||||
session_capture_clear();
|
||||
s_deep = false;
|
||||
}
|
||||
|
||||
1
firmware/data/assets/index-Bb0xTRCn.css
Normal file
1
firmware/data/assets/index-Bb0xTRCn.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Audiowide&family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=Orbitron:wght@500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script type="module" crossorigin src="/assets/index-9oJp152C.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-hoMg1Qkq.css">
|
||||
<script type="module" crossorigin src="/assets/index-BmIGATlK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bb0xTRCn.css">
|
||||
</head>
|
||||
<body class="bg-bubble-950 text-slate-200 antialiased selection:bg-bubble-accent/40 selection:text-bubble-950">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -12,8 +12,12 @@ void app_main(void)
|
||||
(void)esp_ota_mark_app_valid_cancel_rollback();
|
||||
board_rgb_led_quiet();
|
||||
ESP_LOGI(TAG, "PN532 NFC Toolkit starting");
|
||||
ESP_ERROR_CHECK(nfc_engine_init());
|
||||
nfc_engine_try_init(); /* non-fatal: logs warning if PN532 absent, AP starts regardless */
|
||||
session_capture_init();
|
||||
ESP_ERROR_CHECK(app_net_init());
|
||||
ESP_LOGI(TAG, "Open AP SSID PN532-Toolkit — http://192.168.4.1");
|
||||
if (nfc_engine_is_ready()) {
|
||||
ESP_LOGI(TAG, "PN532 ready · AP SSID PN532-Toolkit → http://192.168.4.1");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "PN532 not found at boot — AP running, retrying · http://192.168.4.1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NavLink, Route, Routes } from "react-router-dom";
|
||||
import BrowserLogBar from "./BrowserLogBar";
|
||||
import FlashBackdrop from "./FlashBackdrop";
|
||||
import ScanCashFlourish from "./ScanCashFlourish";
|
||||
import { NfcWsProvider } from "./NfcWsContext";
|
||||
import { NfcWsProvider, useNfcWs } from "./NfcWsContext";
|
||||
import { ToastHost } from "./toast";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import ReadAnalyze from "./pages/ReadAnalyze";
|
||||
@@ -17,6 +17,29 @@ import Brute from "./pages/Brute";
|
||||
import Emulate from "./pages/Emulate";
|
||||
import KeyLab from "./pages/KeyLab";
|
||||
|
||||
function HeaderBadge() {
|
||||
const { wsOk, pn532Connected } = useNfcWs();
|
||||
return (
|
||||
<span className="hidden font-mono text-[9px] text-bubble-mint/40 md:inline lg:text-[10px]">
|
||||
<span className={wsOk ? "text-bubble-accent/90" : "animate-pulse text-bubble-rose/60"}>●</span>{" "}
|
||||
RF_STACK{" "}
|
||||
<span
|
||||
className={
|
||||
!wsOk
|
||||
? "animate-pulse text-bubble-rose/80"
|
||||
: pn532Connected
|
||||
? "font-bold text-bubble-mint"
|
||||
: "animate-pulse text-amber-400"
|
||||
}
|
||||
>
|
||||
{!wsOk ? "WAIT" : pn532Connected ? "LIVE" : "NO RF"}
|
||||
</span>
|
||||
<span className="mx-1.5 text-bubble-mint/25">│</span>
|
||||
<span className="text-bubble-mint/50">ws://stream</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const nav = [
|
||||
["/", "Dash"],
|
||||
["/capture", "Read-all"],
|
||||
@@ -70,12 +93,7 @@ export default function App() {
|
||||
MAXIMAL
|
||||
</span>
|
||||
</motion.div>
|
||||
<span className="hidden font-mono text-[9px] text-bubble-mint/40 md:inline lg:text-[10px]">
|
||||
<span className="text-bubble-accent/90">●</span> RF_STACK{" "}
|
||||
<span className="text-bubble-rose/80">LIVE</span>
|
||||
<span className="mx-1.5 text-bubble-mint/25">│</span>
|
||||
<span className="text-bubble-mint/50">ws://stream</span>
|
||||
</span>
|
||||
<HeaderBadge />
|
||||
</div>
|
||||
<nav className="flex max-w-full flex-wrap justify-end gap-1 text-[10px] font-mono sm:gap-1.5 sm:text-[11px]">
|
||||
{nav.map(([to, label]) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
||||
import type { Tag } from "./api";
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { apiGet, type Status, type Tag } from "./api";
|
||||
import { useToolkitWs } from "./useWebSocket";
|
||||
|
||||
export type BrowserLogEntry = {
|
||||
@@ -12,6 +12,7 @@ export type CashVariant = "tag" | "vault";
|
||||
|
||||
type NfcCtx = {
|
||||
wsOk: boolean;
|
||||
pn532Connected: boolean;
|
||||
lastTag: Tag | null;
|
||||
tagPresent: boolean;
|
||||
log: BrowserLogEntry[];
|
||||
@@ -89,6 +90,7 @@ function isCaptureRecorded(payload: unknown): boolean {
|
||||
export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
const [lastTag, setLastTag] = useState<Tag | null>(null);
|
||||
const [tagPresent, setTagPresent] = useState(false);
|
||||
const [pn532Connected, setPn532Connected] = useState(false);
|
||||
const [log, setLog] = useState<BrowserLogEntry[]>(() => loadPersisted());
|
||||
const [cashWave, setCashWave] = useState(0);
|
||||
const [cashVariant, setCashVariant] = useState<CashVariant>("tag");
|
||||
@@ -121,6 +123,11 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
} else if (ch === "capture" && isCaptureRecorded(o.payload)) {
|
||||
bump("vault");
|
||||
} else if (ch === "pn532") {
|
||||
const p = o.payload as { connected?: boolean } | undefined;
|
||||
if (typeof p?.connected === "boolean") {
|
||||
setPn532Connected(p.connected);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
@@ -131,6 +138,19 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const wsOk = useToolkitWs(onMsg);
|
||||
|
||||
/* Fetch initial PN532 state as soon as WS connects (avoids waiting for first broadcast). */
|
||||
useEffect(() => {
|
||||
if (wsOk) {
|
||||
apiGet<Status>("/api/status")
|
||||
.then((s) => {
|
||||
if (typeof s.pn532Connected === "boolean") {
|
||||
setPn532Connected(s.pn532Connected);
|
||||
}
|
||||
})
|
||||
.catch(() => { /* silently ignore — badge will update on next WS event */ });
|
||||
}
|
||||
}, [wsOk]);
|
||||
|
||||
const applyScanPoll = useCallback(
|
||||
(present: boolean, tag?: Tag) => {
|
||||
if (!present) {
|
||||
@@ -165,6 +185,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
const v = useMemo(
|
||||
() => ({
|
||||
wsOk,
|
||||
pn532Connected,
|
||||
lastTag,
|
||||
tagPresent,
|
||||
log,
|
||||
@@ -174,7 +195,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
exportBrowserLog,
|
||||
applyScanPoll,
|
||||
}),
|
||||
[wsOk, lastTag, tagPresent, log, cashWave, cashVariant, clearBrowserLog, exportBrowserLog, applyScanPoll],
|
||||
[wsOk, pn532Connected, lastTag, tagPresent, log, cashWave, cashVariant, clearBrowserLog, exportBrowserLog, applyScanPoll],
|
||||
);
|
||||
|
||||
return <Ctx.Provider value={v}>{children}</Ctx.Provider>;
|
||||
|
||||
@@ -42,6 +42,7 @@ export type Status = {
|
||||
uptimeMs: number;
|
||||
freeHeap: number;
|
||||
wifiMode: number;
|
||||
pn532Connected: boolean;
|
||||
pn532?: { ic: number; fwHi: number; fwLo: number };
|
||||
scanning: boolean;
|
||||
session?: SessionInfo;
|
||||
|
||||
@@ -19,7 +19,14 @@ export default function Brute() {
|
||||
const keysHex = extraKeys
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.replace(/\s/g, "").toUpperCase())
|
||||
.filter((l) => l.length === 12);
|
||||
.filter((l) => l.length === 12 && /^[0-9A-F]+$/.test(l));
|
||||
const invalidCount = extraKeys
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.replace(/\s/g, ""))
|
||||
.filter((l) => l.length > 0 && (l.length !== 12 || !/^[0-9A-Fa-f]+$/.test(l))).length;
|
||||
if (invalidCount > 0) {
|
||||
toast(`${invalidCount} line(s) not valid 12-hex keys — skipped, firmware built-ins still run`);
|
||||
}
|
||||
const body = {
|
||||
readerType: reader,
|
||||
variations,
|
||||
|
||||
@@ -12,8 +12,8 @@ export default function Capture() {
|
||||
const refresh = useCallback(() => {
|
||||
apiGet<Status>("/api/status")
|
||||
.then(setSt)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
.catch(() => toast("Status unreachable", "err"));
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
|
||||
@@ -152,9 +152,22 @@ export default function Dashboard() {
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-slate-500">PN532</span>
|
||||
<span className="text-bubble-accent">
|
||||
{st.pn532 ? `IC${st.pn532.ic} v${st.pn532.fwHi}.${st.pn532.fwLo}` : "n/a"}
|
||||
{st.pn532Connected ? (
|
||||
<span className="flex items-center gap-2">
|
||||
{st.pn532 && (
|
||||
<span className="text-[10px] text-slate-500">
|
||||
IC{st.pn532.ic} v{st.pn532.fwHi}.{st.pn532.fwLo}
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded px-1.5 py-0.5 bg-bubble-mint/20 text-bubble-mint font-bold text-[10px] tracking-widest">
|
||||
READY
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="animate-pulse rounded px-1.5 py-0.5 bg-amber-500/20 text-amber-400 font-bold text-[10px] tracking-widest">
|
||||
SEARCHING
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
|
||||
@@ -11,13 +11,26 @@ export default function Emulate() {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const send = async () => {
|
||||
const cleanHex = hex.replace(/\s/g, "");
|
||||
if (!cleanHex) {
|
||||
toast("Hex payload is empty", "err");
|
||||
return;
|
||||
}
|
||||
if (cleanHex.length % 2 !== 0) {
|
||||
toast("Hex must have an even number of characters", "err");
|
||||
return;
|
||||
}
|
||||
if (!/^[0-9A-Fa-f]+$/.test(cleanHex)) {
|
||||
toast("Hex must contain only 0-9 A-F characters", "err");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setOut("");
|
||||
try {
|
||||
const r = await fetch(apiUrl("/api/nfc/emulate-raw"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ hex: hex.replace(/\s/g, "") }),
|
||||
body: JSON.stringify({ hex: cleanHex }),
|
||||
});
|
||||
const t = await r.text();
|
||||
if (!r.ok) {
|
||||
|
||||
@@ -19,6 +19,8 @@ export default function RawConsole() {
|
||||
push(`RX ${j.response || j.error || "?"}`);
|
||||
if (j.response) {
|
||||
toast("Frame OK");
|
||||
} else if (j.error) {
|
||||
toast(j.error, "err");
|
||||
}
|
||||
} catch (e) {
|
||||
push(`ERR ${String(e)}`);
|
||||
|
||||
@@ -31,10 +31,12 @@ export default function ReadAnalyze() {
|
||||
|
||||
const readUl = async () => {
|
||||
try {
|
||||
const j = await apiPost<{ data?: string }>("/api/ul/read-page", { page: block });
|
||||
const j = await apiPost<{ data?: string; error?: string }>("/api/ul/read-page", { page: block });
|
||||
if (j.data) {
|
||||
setHex(j.data + " (UL page)");
|
||||
toast("UL read OK");
|
||||
} else {
|
||||
toast(j.error ?? "no data returned", "err");
|
||||
}
|
||||
} catch (e) {
|
||||
toast(String(e), "err");
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function WriteClone() {
|
||||
const [labBlob, setLabBlob] = useState<BinaryCardFixture | null>(null);
|
||||
const [ulPage, setUlPage] = useState(4);
|
||||
const [ulData, setUlData] = useState("00000000");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const write = async () => {
|
||||
const cleanKey = key.replace(/\s/g, "");
|
||||
@@ -36,11 +37,14 @@ export default function WriteClone() {
|
||||
if (!confirm("Write will modify tag memory. Continue?")) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await apiPost("/api/mifare/write-block", { block, key: cleanKey, keyB, data: cleanData });
|
||||
toast("Write OK");
|
||||
} catch (e) {
|
||||
toast(String(e), "err");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,11 +57,14 @@ export default function WriteClone() {
|
||||
if (!confirm("Ultralight page write — can brick OTP/lock bytes if misused. Continue?")) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await apiPost("/api/ul/write-page", { page: ulPage, data: h });
|
||||
toast("UL page write OK");
|
||||
} catch (e) {
|
||||
toast(String(e), "err");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,9 +137,10 @@ export default function WriteClone() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={write}
|
||||
className="rounded-2xl bg-gradient-to-r from-bubble-rose to-orange-400 px-6 py-3 font-bold text-white shadow-glow"
|
||||
disabled={busy}
|
||||
className="rounded-2xl bg-gradient-to-r from-bubble-rose to-orange-400 px-6 py-3 font-bold text-white shadow-glow disabled:opacity-50"
|
||||
>
|
||||
Write block
|
||||
{busy ? "Writing…" : "Write block"}
|
||||
</button>
|
||||
|
||||
<div className="border-t border-white/10 pt-8">
|
||||
@@ -162,9 +170,10 @@ export default function WriteClone() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={writeUl}
|
||||
className="mt-4 rounded-2xl border border-bubble-accent/50 bg-bubble-accent/20 px-6 py-3 font-bold text-bubble-accent"
|
||||
disabled={busy}
|
||||
className="mt-4 rounded-2xl border border-bubble-accent/50 bg-bubble-accent/20 px-6 py-3 font-bold text-bubble-accent disabled:opacity-50"
|
||||
>
|
||||
Write UL page
|
||||
{busy ? "Writing…" : "Write UL page"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ const Ctx = createContext<(msg: string, kind?: Toast["kind"]) => void>(() => {})
|
||||
export function ToastHost({ children }: { children: React.ReactNode }) {
|
||||
const [list, setList] = useState<Toast[]>([]);
|
||||
const push = useCallback((msg: string, kind: Toast["kind"] = "info") => {
|
||||
const id = Date.now();
|
||||
const id = Date.now() + Math.random();
|
||||
setList((x) => [...x, { id, msg, kind }]);
|
||||
setTimeout(() => setList((x) => x.filter((t) => t.id !== id)), 4200);
|
||||
}, []);
|
||||
|
||||
Reference in New Issue
Block a user