- Read button (desktop and web): reads the tag now, bypassing the status cache, and puts its first link or text record into the input field so it can be copied to another tag. POST /api/read in the web version. - Raw memory: every 4-byte page from page 0 (UID, lock bytes, CC, data) in hex and ASCII, collapsed by default. - Content now shows the records and the note together (a locked tag showed only "read-only" before); read failures are flagged with read_failed instead of matching message text. - .gitea/workflows/release.yml: on a v* tag, tests and clippy, then Linux (ubuntu-22.04, glibc 2.35) and Windows (MinGW cross-build) packages attached to the tag's release with its CHANGELOG.md section. - README: reading, why the reader program runs on the reader's machine, releases.
This commit is contained in:
1 parent
cb1b1b04c8
commit
8b95d0d5d7
10 files changed
+610
-165
No files matched your search
+113
-55
@@ -54,7 +54,11 @@
|
||||
padding: 9px 18px; font: inherit; font-weight: 600; color: #fff;
|
||||
background: var(--accent); border: 0; border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
button.secondary { color: var(--fg); background: transparent; border: 1px solid var(--border); font-weight: 500; }
|
||||
button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
details { margin: 8px 4px 0; font-size: 13px; }
|
||||
summary { color: var(--muted); cursor: pointer; }
|
||||
pre { max-height: 180px; overflow: auto; margin: 6px 0 0; padding: 8px 10px; font-size: 12px; border: 1px solid var(--border); border-radius: 8px; }
|
||||
.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); }
|
||||
@@ -71,24 +75,78 @@
|
||||
<dt>Memory</dt><dd id="memory"></dd>
|
||||
<dt>Content</dt><dd id="content"></dd>
|
||||
</dl>
|
||||
<details id="raw-box" hidden><summary id="raw-title">Raw memory</summary><pre class="mono" id="raw"></pre></details>
|
||||
<form id="form">
|
||||
<input id="text" placeholder="https://buildfor.life or any text" autocomplete="off" autofocus>
|
||||
<button id="read" type="button" class="secondary" disabled>Read</button>
|
||||
<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>
|
||||
<p class="hint">Links (https://, www., tel:, mailto:, geo: …) are written as a link, anything else as plain text. Read puts the tag's content into the field, so you can copy it to another tag.</p>
|
||||
<div class="msg" id="msg" role="status"></div>
|
||||
</main>
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
let busy = false, canWrite = false;
|
||||
let busy = false, canWrite = false, present = false;
|
||||
|
||||
function setStatus(level, text) {
|
||||
$("dot").className = "dot " + level;
|
||||
$("status").textContent = text;
|
||||
}
|
||||
|
||||
function setMsg(level, text) {
|
||||
$("msg").className = "msg " + level;
|
||||
$("msg").textContent = text;
|
||||
}
|
||||
|
||||
// One line per 4-byte page: page number, hex bytes, printable ASCII.
|
||||
function pageDump(raw) {
|
||||
const lines = [];
|
||||
for (let i = 0; i < raw.length; i += 4) {
|
||||
const page = raw.slice(i, i + 4);
|
||||
const hex = page.map(b => b.toString(16).toUpperCase().padStart(2, "0")).join(" ");
|
||||
const ascii = page.map(b => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : ".")).join("");
|
||||
lines.push(`${String(i / 4).padStart(3)} ${hex.padEnd(11)} ${ascii}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function fillText(t) {
|
||||
const r = t.records[0];
|
||||
return r && (r.kind === "URL" || r.kind === "Text") ? r.value : null;
|
||||
}
|
||||
|
||||
function renderTag(t) {
|
||||
$("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();
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (!t.records.length && t.formatted && !t.read_failed) c.append("empty");
|
||||
if (t.note) {
|
||||
if (t.records.length) c.append(document.createElement("br"));
|
||||
c.append(Object.assign(document.createElement("span"), { className: "note", textContent: t.note }));
|
||||
}
|
||||
$("raw-box").hidden = !t.raw.length;
|
||||
$("raw-title").textContent = `Raw memory (${t.raw.length / 4} pages)`;
|
||||
const dump = pageDump(t.raw);
|
||||
if ($("raw").textContent !== dump) $("raw").textContent = dump;
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
$("tag").hidden = s.state !== "card";
|
||||
present = s.state === "card";
|
||||
$("tag").hidden = !present;
|
||||
if (!present) $("raw-box").hidden = true;
|
||||
canWrite = false;
|
||||
if (s.state === "no_reader" || s.state === "error") {
|
||||
setStatus("err", s.message);
|
||||
@@ -98,74 +156,74 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
renderTag(t);
|
||||
}
|
||||
$("program").disabled = busy || !canWrite;
|
||||
$("read").disabled = busy || !present;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
render(await (await fetch("/api/status")).json());
|
||||
} catch {
|
||||
setStatus("err", "Cannot reach the local NFC service — is nfc-writer-web running?");
|
||||
$("tag").hidden = $("raw-box").hidden = true;
|
||||
$("program").disabled = $("read").disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (!busy) await refresh();
|
||||
setTimeout(poll, 800);
|
||||
}
|
||||
|
||||
$("form").addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
if (busy || !canWrite) return;
|
||||
// Run one reader job: disable the buttons, show progress on `button`, then refresh the status.
|
||||
async function job(button, label, url, body, done) {
|
||||
busy = true;
|
||||
$("program").disabled = true;
|
||||
$("program").textContent = "Programming…";
|
||||
$("msg").className = "msg";
|
||||
$("msg").textContent = "";
|
||||
$("program").disabled = $("read").disabled = true;
|
||||
const idle = button.textContent;
|
||||
button.textContent = label;
|
||||
setMsg("", "");
|
||||
try {
|
||||
const res = await fetch("/api/program", {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: $("text").value }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
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}`;
|
||||
}
|
||||
const reply = await res.json();
|
||||
if (res.ok) done(reply);
|
||||
else setMsg("err", reply.error || `Error ${res.status}`);
|
||||
} catch (err) {
|
||||
$("msg").className = "msg err";
|
||||
$("msg").textContent = "Request failed: " + err;
|
||||
setMsg("err", "Request failed: " + err);
|
||||
}
|
||||
busy = false;
|
||||
$("program").textContent = "Program";
|
||||
try { render(await (await fetch("/api/status")).json()); } catch {}
|
||||
button.textContent = idle;
|
||||
await refresh();
|
||||
}
|
||||
|
||||
$("form").addEventListener("submit", e => {
|
||||
e.preventDefault();
|
||||
if (busy || !canWrite) return;
|
||||
job($("program"), "Programming…", "/api/program", { text: $("text").value }, r =>
|
||||
setMsg("ok", `✓ Wrote ${r.record.kind}: ${r.record.value} (${r.bytes} of ${r.capacity} bytes, verified)`));
|
||||
});
|
||||
|
||||
$("read").addEventListener("click", () => {
|
||||
if (busy || !present) return;
|
||||
job($("read"), "Reading…", "/api/read", {}, t => {
|
||||
renderTag(t);
|
||||
const n = t.records.length, fill = fillText(t);
|
||||
if (fill !== null) {
|
||||
$("text").value = fill;
|
||||
setMsg("ok", `✓ Read ${t.uid}: ${n} record${n === 1 ? "" : "s"}. The first one is now in the field: put another tag on and press Program to copy it.`);
|
||||
} else if (n) {
|
||||
setMsg("ok", `✓ Read ${t.uid}: ${n} record(s), none of them a link or text.`);
|
||||
} else if (t.note) {
|
||||
setMsg("err", `Read ${t.uid} (${t.kind}): UID only, no NDEF content.`);
|
||||
} else {
|
||||
setMsg("ok", `✓ Read ${t.uid}: the tag is empty.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
poll();
|
||||
|
||||
Reference in new issue
Block a user