NFC tag writer: local web page for ACR1252U (NDEF URL/text on Type 2 tags)
This commit is contained in:
commit
449cdb7af5
8 files changed
+1496
No files matched your search
@@ -0,0 +1,174 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NFC tag writer</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #f5f6f8; --card: #fff; --fg: #1d2330; --muted: #6b7280; --border: #d9dde3;
|
||||
--accent: #2563eb; --ok: #15803d; --err: #b91c1c; --warn: #b45309;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0f1218; --card: #171b23; --fg: #e5e7eb; --muted: #9aa3b2; --border: #2a303c;
|
||||
--accent: #3b82f6; --ok: #22c55e; --err: #f87171; --warn: #fbbf24;
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
body {
|
||||
margin: 0; padding: 48px 16px; display: flex; justify-content: center;
|
||||
font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
background: var(--bg); color: var(--fg);
|
||||
}
|
||||
main {
|
||||
width: 100%; max-width: 560px; padding: 20px 22px;
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: 12px;
|
||||
}
|
||||
h1 { font-size: 18px; margin: 0 0 14px; }
|
||||
.status {
|
||||
display: flex; align-items: center; gap: 8px; padding: 8px 10px;
|
||||
font-size: 14px; border: 1px solid var(--border); border-radius: 8px;
|
||||
}
|
||||
.dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--muted); }
|
||||
.dot.ok { background: var(--ok); }
|
||||
.dot.warn { background: var(--warn); }
|
||||
.dot.err { background: var(--err); }
|
||||
dl {
|
||||
display: grid; grid-template-columns: max-content 1fr; gap: 3px 14px;
|
||||
margin: 10px 4px 0; font-size: 13px;
|
||||
}
|
||||
dt { color: var(--muted); }
|
||||
dd { margin: 0; overflow-wrap: anywhere; }
|
||||
.mono { font-family: ui-monospace, Consolas, monospace; }
|
||||
.note { color: var(--warn); }
|
||||
form { display: flex; gap: 8px; margin-top: 18px; }
|
||||
input {
|
||||
flex: 1; min-width: 0; padding: 9px 11px; font: inherit; color: inherit;
|
||||
background: transparent; border: 1px solid var(--border); border-radius: 8px;
|
||||
}
|
||||
input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
|
||||
button {
|
||||
padding: 9px 18px; font: inherit; font-weight: 600; color: #fff;
|
||||
background: var(--accent); border: 0; border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.hint { margin: 6px 2px 0; font-size: 12px; color: var(--muted); }
|
||||
.msg { min-height: 1.4em; margin-top: 12px; font-size: 14px; }
|
||||
.msg.ok { color: var(--ok); }
|
||||
.msg.err { color: var(--err); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>NFC tag writer</h1>
|
||||
<div class="status"><span class="dot" id="dot"></span><span id="status">Connecting…</span></div>
|
||||
<dl id="tag" hidden>
|
||||
<dt>UID</dt><dd class="mono" id="uid"></dd>
|
||||
<dt>Type</dt><dd id="kind"></dd>
|
||||
<dt>Memory</dt><dd id="memory"></dd>
|
||||
<dt>Content</dt><dd id="content"></dd>
|
||||
</dl>
|
||||
<form id="form">
|
||||
<input id="text" placeholder="https://buildfor.life or any text" autocomplete="off" autofocus>
|
||||
<button id="program" type="submit" disabled>Program</button>
|
||||
</form>
|
||||
<p class="hint">URLs (https://, www., tel:, mailto:, geo: …) are written as a link, anything else as plain text.</p>
|
||||
<div class="msg" id="msg" role="status"></div>
|
||||
</main>
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
let busy = false, canWrite = false;
|
||||
|
||||
function setStatus(level, text) {
|
||||
$("dot").className = "dot " + level;
|
||||
$("status").textContent = text;
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
$("tag").hidden = s.state !== "card";
|
||||
canWrite = false;
|
||||
if (s.state === "no_reader" || s.state === "error") {
|
||||
setStatus("err", s.message);
|
||||
} else if (s.state === "no_card") {
|
||||
setStatus("warn", s.reader + " — place a tag on the reader");
|
||||
} else {
|
||||
const t = s.tag;
|
||||
canWrite = t.formatted && t.writable && t.capacity != null && !t.note;
|
||||
setStatus(canWrite ? "ok" : "warn", s.reader + " — tag present");
|
||||
$("uid").textContent = t.uid;
|
||||
$("kind").textContent = t.kind;
|
||||
$("memory").textContent = t.capacity != null
|
||||
? `${t.used ?? "?"} of ${t.capacity} bytes used${t.writable ? "" : ", read-only"}`
|
||||
: "—";
|
||||
const c = $("content");
|
||||
c.replaceChildren();
|
||||
if (t.note) {
|
||||
c.append(Object.assign(document.createElement("span"), { className: "note", textContent: t.note }));
|
||||
} else if (!t.records.length) {
|
||||
c.textContent = "empty";
|
||||
} else {
|
||||
t.records.forEach((r, i) => {
|
||||
if (i) c.append(document.createElement("br"));
|
||||
c.append(`${r.kind}: `);
|
||||
if (r.kind === "URL" && /^https?:\/\//i.test(r.value)) {
|
||||
c.append(Object.assign(document.createElement("a"), { href: r.value, target: "_blank", textContent: r.value }));
|
||||
} else {
|
||||
c.append(r.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
$("program").disabled = busy || !canWrite;
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
if (!busy) {
|
||||
try {
|
||||
render(await (await fetch("/api/status")).json());
|
||||
} catch {
|
||||
setStatus("err", "Cannot reach the local NFC service — is nfc-writer.exe running?");
|
||||
$("tag").hidden = true;
|
||||
$("program").disabled = true;
|
||||
}
|
||||
}
|
||||
setTimeout(poll, 800);
|
||||
}
|
||||
|
||||
$("form").addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
if (busy || !canWrite) return;
|
||||
busy = true;
|
||||
$("program").disabled = true;
|
||||
$("program").textContent = "Programming…";
|
||||
$("msg").className = "msg";
|
||||
$("msg").textContent = "";
|
||||
try {
|
||||
const res = await fetch("/api/program", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: $("text").value }),
|
||||
});
|
||||
const body = await res.json();
|
||||
if (res.ok) {
|
||||
$("msg").className = "msg ok";
|
||||
$("msg").textContent = `✓ Wrote ${body.record.kind}: ${body.record.value} (${body.bytes} of ${body.capacity} bytes, verified)`;
|
||||
} else {
|
||||
$("msg").className = "msg err";
|
||||
$("msg").textContent = body.error || `Error ${res.status}`;
|
||||
}
|
||||
} catch (err) {
|
||||
$("msg").className = "msg err";
|
||||
$("msg").textContent = "Request failed: " + err;
|
||||
}
|
||||
busy = false;
|
||||
$("program").textContent = "Program";
|
||||
try { render(await (await fetch("/api/status")).json()); } catch {}
|
||||
});
|
||||
|
||||
poll();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user