NFC NDEF messages
What NFC tags hold: URI, text (UTF-8 or UTF-16), MIME, smart poster, Android application and external records built and parsed, the URI prefix table applied by longest match, long records, chunked records joined, Type 2 tag TLVs found in a memory dump, and damaged messages refused by byte.
Version 1.0.0 · 7 functions · 2 crates
Functions
What use nfc_ndef.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use ndef_build, ndef_build_chunked from nfc_ndef (Quillo 0.6).
ndef_build(records: list) → stror an errorBuilds an NDEF message from records (see the top of the file); answers hex. A payload of 256 bytes or more gets a long record by itself.
ndef_build_chunked(records: list, chunk_size: int) → stror an errorBuilds a message with every payload over chunk_size bytes split into chunked records, as a phone sending over peer-to-peer may.
ndef_parse(message: any) → anyor an errorReads an NDEF message (hex or bytes) into records, chunks joined, smart posters opened: { records, count, chunked }. A damaged message is an error naming the byte.
ndef_inspect(message: any) → listor an errorEach record's header as it is on the tag, for looking at one: flags, TNF, type, lengths. Chunks are shown one by one.
ndef_uri_prefix(uri: str) → anyThe URI prefix table's code for a URI, and what is left after it: { code, prefix, rest }.
ndef_tlv_wrap(message: any) → stror an errorWraps a message in the TLV a Type 2 tag stores: 0x03, the length (three bytes past 254), the message, 0xFE.
ndef_tlv_unwrap(memory: any) → str?or an errorFinds the NDEF message in a tag's memory dump (TLVs), skipping NULL, lock and memory control TLVs; answers its hex, or null when the tag holds none.
Get it
$ quillo install nfc_ndef # nfc_ndef.qwer into this folder$ quillo install nfc_ndef --example # and its example, nfc_ndef.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/nfc_ndef.qwer$ curl -O https://quillolang.org/plugins/files/nfc_ndef.qweThen, in your program, all of it or only what you call:
use nfc_ndef.qweruse ndef_build, ndef_build_chunked from nfc_ndef // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
hex = "0.4"serde_json = { version = "1", features = ["preserve_order"] }Example
// What goes on an NFC tag: a link with its short prefix code, a poster with titles in two// languages and an app to open, a contact card, and what a phone reads back; chunked records// joined, and a damaged tag refused. No reader needed: a message is bytes, and the hex here can// be written with any tag app.//// quillo run nfc_ndef.qwe then open http://localhost:8080/link, /poster, /chunks and /damageduse "nfc_ndef.qwer"route GET /link { let msg = ndef_build([{ type: "uri", uri: "https://www.example.com/menu" }])? { hex: msg, code: ndef_uri_prefix("https://www.example.com/menu"), phone: ndef_uri_prefix("tel:+15551234"), unknown_scheme: ndef_uri_prefix("geo:37.78,-122.40"), back: ndef_parse(msg)?.records, }}route GET /text { let msg = ndef_build([{ type: "text", text: "Hallo Welt", lang: "de" }, { type: "text", text: "こんにちは", lang: "ja", encoding: "utf-16" }])? { hex: msg, back: ndef_parse(msg)?.records }}// A poster on a shop window: a link, two titles, the app it should open, and a card.route GET /poster { let msg = ndef_build([ { type: "smart_poster", uri: "https://example.com/sale", title: { en: "Winter sale", fr: "Soldes d'hiver" }, action: "open", size: 2048, content_type: "text/html" }, { type: "android_app", package: "com.example.shop" }, { type: "mime", mime: "text/vcard", text: "BEGIN:VCARD\nVERSION:3.0\nFN:Ada\nEND:VCARD\n", id: "card" }, { type: "external", domain_type: "example.com:stock", hex: "01ff" }, ])? let back = ndef_parse(msg)? { count: back.count, records: back.records, headers: ndef_inspect(msg)? | map((h) => [h.tnf, h.type, h.mb, h.me]) }}// A large payload gets a long record by itself, and may come in chunks.route GET /chunks { let body = repeat("0123456789", 40) let whole = ndef_build([{ type: "mime", mime: "text/plain", text: body }])? let parts = ndef_build_chunked([{ type: "mime", mime: "text/plain", text: body }, { type: "uri", uri: "tel:112" }], 150)? let back = ndef_parse(parts)? { short_record: ndef_inspect(whole)?[0].sr, chunks: ndef_inspect(parts)? | map((h) => [h.tnf, h.cf, h.payload_length]), joined: len(back.records[0].text), same: back.records[0].text == body, chunked: back.chunked, after: back.records[1].uri, }}// As it sits on a Type 2 tag: lock TLV, the message, terminator.route GET /tag { let msg = ndef_build([{ type: "uri", uri: "https://nfc-forum.org" }])? let wrapped = ndef_tlv_wrap(msg)? let memory = "0103a00c340000" + wrapped + "0000" { wrapped: wrapped, found: ndef_tlv_unwrap(memory)? == msg, blank: ndef_tlv_unwrap("0000fe0000")?, cut: ndef_tlv_unwrap("03ff0100").error }}route GET /damaged = { cut: ndef_parse("d1010f55").error, no_end: ndef_parse("9101015500").error, not_first: ndef_parse("51010155").error, reserved_prefix: ndef_parse("d101025530ff").error, lone_chunk: ndef_parse("b6000101").error, unended_chunks: ndef_parse("b5000161" + "560001").error, bad_action: ndef_build([{ type: "smart_poster", uri: "x:y", action: "burn" }]).error, bad_kind: ndef_build([{ type: "wifi" }]).error,}$ quillo run nfc_ndef.qwe$ curl 'localhost:8080/link'$ curl 'localhost:8080/text'$ curl 'localhost:8080/poster'$ curl 'localhost:8080/chunks'$ curl 'localhost:8080/tag'$ curl 'localhost:8080/damaged'Source
//! NDEF, the message format on NFC tags: URI, text, MIME, smart poster, Android application and external records built and parsed, with the URI prefix table, short and long records, chunked records joined, and the TLV wrapper a Type 2 tag stores them in.//! version: 1.0.0//!//! [dependencies]//! hex = "0.4"//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};// A message is hex here, the way readers show tags; a parse also takes a list of bytes. A record in// a program is a map:// { type: "uri", uri: "https://example.com/" }// { type: "text", text: "Hello", lang: "en", encoding: "utf-16" (default utf-8) }// { type: "mime", mime: "text/vcard", text: "..." or hex: "..." }// { type: "smart_poster", uri, title (a string, or a map of lang to title), action: "open", "save" or "edit", size, content_type }// { type: "android_app", package: "com.example.app" }// { type: "external", domain_type: "example.com:thing", hex or text }// { type: "absolute_uri", uri, hex or text }, { type: "empty" }, { type: "unknown", hex }// Any record may have an `id`.const PREFIXES: [&str; 36] = [ "", "http://www.", "https://www.", "http://", "https://", "tel:", "mailto:", "ftp://anonymous:anonymous@", "ftp://ftp.", "ftps://", "sftp://", "smb://", "nfs://", "ftp://", "dav://", "news:", "telnet://", "imap:", "rtsp://", "urn:", "pop:", "sip:", "sips:", "tftp:", "btspp://", "btl2cap://", "btgoep://", "tcpobex://", "irdaobex://", "file://", "urn:epc:id:", "urn:epc:tag:", "urn:epc:pat:", "urn:epc:raw:", "urn:epc:", "urn:nfc:",];const EMPTY: u8 = 0;const WELL_KNOWN: u8 = 1;const MIME: u8 = 2;const ABSOLUTE: u8 = 3;const EXTERNAL: u8 = 4;const UNKNOWN: u8 = 5;const UNCHANGED: u8 = 6;struct Rec { tnf: u8, kind: Vec<u8>, id: Vec<u8>, payload: Vec<u8>,}fn bytes_of(v: &Value) -> Result<Vec<u8>, String> { match v { Value::Array(a) => a.iter().map(|x| x.as_u64().filter(|n| *n < 256).map(|n| n as u8).ok_or("a byte list holds numbers from 0 to 255".to_string())).collect(), Value::String(s) => { let t: String = s.chars().filter(|c| !c.is_whitespace() && *c != ':').collect(); hex::decode(&t).map_err(|_| "not hex".to_string()) } _ => Err("give the bytes as hex or a list of numbers".into()), }}fn s<'a>(r: &'a Value, k: &str) -> Option<&'a str> { r.get(k).and_then(|v| v.as_str())}fn body_of(r: &Value) -> Result<Vec<u8>, String> { if let Some(h) = s(r, "hex") { return hex::decode(h.replace(' ', "")).map_err(|_| "`hex` is not hex".to_string()); } Ok(s(r, "text").unwrap_or("").as_bytes().to_vec())}fn uri_payload(uri: &str) -> Vec<u8> { let best = PREFIXES.iter().enumerate().filter(|(_, p)| !p.is_empty() && uri.starts_with(**p)).max_by_key(|(_, p)| p.len()); match best { Some((i, p)) => { let mut b = vec![i as u8]; b.extend(uri[p.len()..].as_bytes()); b } None => { let mut b = vec![0]; b.extend(uri.as_bytes()); b } }}fn text_payload(text: &str, lang: &str, utf16: bool) -> Result<Vec<u8>, String> { if lang.is_empty() || lang.len() > 63 || !lang.is_ascii() { return Err(format!("`{}` is not a language code: it must be 1 to 63 ASCII characters", lang)); } let mut b = vec![(lang.len() as u8) | if utf16 { 0x80 } else { 0 }]; b.extend(lang.as_bytes()); if utf16 { b.extend([0xfe, 0xff]); for u in text.encode_utf16() { b.extend(u.to_be_bytes()); } } else { b.extend(text.as_bytes()); } Ok(b)}fn record_of(r: &Value) -> Result<Rec, String> { let ty = s(r, "type").ok_or("a record needs `type`")?; let need = |k: &str| s(r, k).ok_or(format!("a {} record needs `{}`", ty, k)); let (tnf, kind, payload): (u8, Vec<u8>, Vec<u8>) = match ty { "uri" => (WELL_KNOWN, b"U".to_vec(), uri_payload(need("uri")?)), "text" => { let utf16 = match s(r, "encoding").unwrap_or("utf-8") { "utf-8" => false, "utf-16" => true, e => return Err(format!("`{}` is not a text record's encoding: utf-8 or utf-16", e)), }; (WELL_KNOWN, b"T".to_vec(), text_payload(need("text")?, s(r, "lang").unwrap_or("en"), utf16)?) } "mime" => { let m = need("mime")?; if !m.is_ascii() || !m.contains('/') { return Err(format!("`{}` is not a MIME type", m)); } (MIME, m.as_bytes().to_vec(), body_of(r)?) } "smart_poster" => { let mut inner = vec![Rec { tnf: WELL_KNOWN, kind: b"U".to_vec(), id: vec![], payload: uri_payload(need("uri")?) }]; match r.get("title") { Some(Value::String(t)) => inner.push(Rec { tnf: WELL_KNOWN, kind: b"T".to_vec(), id: vec![], payload: text_payload(t, s(r, "lang").unwrap_or("en"), false)? }), Some(Value::Object(m)) => { for (lang, t) in m { inner.push(Rec { tnf: WELL_KNOWN, kind: b"T".to_vec(), id: vec![], payload: text_payload(t.as_str().ok_or("a title must be text")?, lang, false)? }); } } _ => {} } if let Some(a) = s(r, "action") { let code = ["open", "save", "edit"].iter().position(|x| *x == a).ok_or(format!("`{}` is not an action: open, save or edit", a))?; inner.push(Rec { tnf: WELL_KNOWN, kind: b"act".to_vec(), id: vec![], payload: vec![code as u8] }); } if let Some(n) = r.get("size").and_then(|v| v.as_u64()) { let n = u32::try_from(n).map_err(|_| "`size` must fit 32 bits")?; inner.push(Rec { tnf: WELL_KNOWN, kind: b"s".to_vec(), id: vec![], payload: n.to_be_bytes().to_vec() }); } if let Some(t) = s(r, "content_type") { inner.push(Rec { tnf: WELL_KNOWN, kind: b"t".to_vec(), id: vec![], payload: t.as_bytes().to_vec() }); } (WELL_KNOWN, b"Sp".to_vec(), write_all(&inner, 0)?) } "android_app" => (EXTERNAL, b"android.com:pkg".to_vec(), need("package")?.as_bytes().to_vec()), "external" => { let t = need("domain_type")?.to_lowercase(); if !t.contains(':') || !t.is_ascii() { return Err(format!("`{}` is not an external type: it is domain:type", t)); } (EXTERNAL, t.into_bytes(), body_of(r)?) } "absolute_uri" => (ABSOLUTE, need("uri")?.as_bytes().to_vec(), body_of(r)?), "well_known" => (WELL_KNOWN, need("name")?.as_bytes().to_vec(), body_of(r)?), "empty" => (EMPTY, vec![], vec![]), "unknown" => (UNKNOWN, vec![], body_of(r)?), t => return Err(format!("`{}` is not a record type: uri, text, mime, smart_poster, android_app, external, absolute_uri, well_known, empty or unknown", t)), }; if kind.len() > 255 { return Err("a record's type is at most 255 bytes".into()); } let id = s(r, "id").unwrap_or("").as_bytes().to_vec(); if id.len() > 255 { return Err("a record's id is at most 255 bytes".into()); } Ok(Rec { tnf, kind, id, payload })}fn write_one(out: &mut Vec<u8>, first: bool, last: bool, chunk: bool, tnf: u8, kind: &[u8], id: &[u8], payload: &[u8]) { let sr = payload.len() < 256; let mut h = tnf; if first { h |= 0x80; } if last { h |= 0x40; } if chunk { h |= 0x20; } if sr { h |= 0x10; } if !id.is_empty() { h |= 0x08; } out.push(h); out.push(kind.len() as u8); if sr { out.push(payload.len() as u8); } else { out.extend((payload.len() as u32).to_be_bytes()); } if !id.is_empty() { out.push(id.len() as u8); } out.extend(kind); out.extend(id); out.extend(payload);}fn write_all(recs: &[Rec], chunk_size: usize) -> Result<Vec<u8>, String> { let mut out = vec![]; let n = recs.len(); for (i, r) in recs.iter().enumerate() { let (first, last) = (i == 0, i + 1 == n); if chunk_size == 0 || r.payload.len() <= chunk_size { write_one(&mut out, first, last, false, r.tnf, &r.kind, &r.id, &r.payload); continue; } if r.tnf == EMPTY { return Err("an empty record cannot be chunked".into()); } // The first chunk has the type and id; the others say UNCHANGED, with neither. let parts: Vec<&[u8]> = r.payload.chunks(chunk_size).collect(); for (k, p) in parts.iter().enumerate() { let end = k + 1 == parts.len(); if k == 0 { write_one(&mut out, first, false, true, r.tnf, &r.kind, &r.id, p); } else { write_one(&mut out, false, last && end, !end, UNCHANGED, &[], &[], p); } } } Ok(out)}fn read_all(b: &[u8]) -> Result<(Vec<Rec>, usize), String> { if b.is_empty() { return Err("an empty NDEF message".into()); } let mut recs: Vec<Rec> = vec![]; let mut i = 0; let mut in_chunks = false; let mut chunked = 0; let mut first = true; loop { let at = i; let h = *b.get(i).ok_or(format!("the message ends at byte {} with no record marked last (ME)", i))?; i += 1; let (mb, me, cf, sr, il, tnf) = (h & 0x80 != 0, h & 0x40 != 0, h & 0x20 != 0, h & 0x10 != 0, h & 0x08 != 0, h & 7); if first != mb { return Err(format!("record at byte {}: {}", at, if first { "the first record is not marked MB" } else { "MB set on a record inside the message" })); } first = false; let cut = || format!("record at byte {} runs past the end of the message", at); let tl = *b.get(i).ok_or_else(cut)? as usize; i += 1; let pl = if sr { let v = *b.get(i).ok_or_else(cut)? as usize; i += 1; v } else { let v = b.get(i..i + 4).ok_or_else(cut)?; i += 4; u32::from_be_bytes([v[0], v[1], v[2], v[3]]) as usize }; let il = if il { let v = *b.get(i).ok_or_else(cut)? as usize; i += 1; v } else { 0 }; let total = tl.checked_add(il).and_then(|x| x.checked_add(pl)).ok_or_else(cut)?; if i.checked_add(total).map_or(true, |e| e > b.len()) { return Err(cut()); } let kind = b[i..i + tl].to_vec(); i += tl; let id = b[i..i + il].to_vec(); i += il; let payload = &b[i..i + pl]; i += pl; if in_chunks { if tnf != UNCHANGED || tl != 0 || il != 0 { return Err(format!("record at byte {}: a middle or last chunk must say UNCHANGED, with no type or id", at)); } recs.last_mut().expect("a chunk has a first").payload.extend(payload); in_chunks = cf; } else { if tnf == UNCHANGED { return Err(format!("record at byte {}: UNCHANGED outside a chunked record", at)); } if tnf == 7 { return Err(format!("record at byte {}: TNF 7 is reserved", at)); } if tnf == EMPTY && (tl != 0 || il != 0 || pl != 0) { return Err(format!("record at byte {}: an empty record holds something", at)); } if tnf == UNKNOWN && tl != 0 { return Err(format!("record at byte {}: an unknown record has no type", at)); } recs.push(Rec { tnf, kind, id, payload: payload.to_vec() }); in_chunks = cf; if cf { chunked += 1; } } if me { if in_chunks { return Err(format!("record at byte {}: the message ends inside a chunked record", at)); } return Ok((recs, chunked)); } }}fn text_of(p: &[u8]) -> Result<Value, String> { let st = *p.first().ok_or("an empty text record")?; let n = (st & 0x3f) as usize; if 1 + n > p.len() { return Err("a text record's language runs past its end".into()); } let lang = String::from_utf8_lossy(&p[1..1 + n]).to_string(); let body = &p[1 + n..]; let (text, enc) = if st & 0x80 != 0 { if body.len() % 2 != 0 { return Err("UTF-16 text with an odd number of bytes".into()); } let (le, data) = match body { [0xff, 0xfe, r @ ..] => (true, r), [0xfe, 0xff, r @ ..] => (false, r), r => (false, r), }; let u: Vec<u16> = data.chunks(2).map(|c| if le { u16::from_le_bytes([c[0], c[1]]) } else { u16::from_be_bytes([c[0], c[1]]) }).collect(); (String::from_utf16(&u).map_err(|_| "bad UTF-16 in a text record")?, "utf-16") } else { (String::from_utf8(body.to_vec()).map_err(|_| "bad UTF-8 in a text record")?, "utf-8") }; Ok(json!({ "type": "text", "lang": lang, "encoding": enc, "text": text }))}fn uri_of(p: &[u8]) -> Result<String, String> { let code = *p.first().ok_or("an empty URI record")? as usize; let prefix = PREFIXES.get(code).ok_or(format!("URI prefix code 0x{:02x} is reserved", code))?; Ok(format!("{}{}", prefix, std::str::from_utf8(&p[1..]).map_err(|_| "a URI record is not UTF-8")?))}fn text_or_hex(m: &mut Value, p: &[u8]) { match std::str::from_utf8(p) { Ok(t) if !t.chars().any(|c| c.is_control() && !c.is_whitespace()) => m["text"] = json!(t), _ => m["hex"] = json!(hex::encode(p)), }}fn describe(r: &Rec, depth: usize) -> Result<Value, String> { let mut m = match (r.tnf, r.kind.as_slice()) { (WELL_KNOWN, b"U") => json!({ "type": "uri", "uri": uri_of(&r.payload)? }), (WELL_KNOWN, b"T") => text_of(&r.payload)?, (WELL_KNOWN, b"Sp") => { if depth > 4 { return Err("smart posters nested too deep".into()); } let (inner, _) = read_all(&r.payload).map_err(|e| format!("inside a smart poster: {}", e))?; let mut m = json!({ "type": "smart_poster", "uri": Value::Null }); let mut titles = serde_json::Map::new(); for x in &inner { match (x.tnf, x.kind.as_slice()) { (WELL_KNOWN, b"U") => m["uri"] = json!(uri_of(&x.payload)?), (WELL_KNOWN, b"T") => { let t = text_of(&x.payload)?; titles.insert(t["lang"].as_str().unwrap_or("").to_string(), t["text"].clone()); } (WELL_KNOWN, b"act") => m["action"] = json!(match x.payload.first() { Some(0) => "open", Some(1) => "save", Some(2) => "edit", _ => "unknown" }), (WELL_KNOWN, b"s") if x.payload.len() == 4 => m["size"] = json!(u32::from_be_bytes([x.payload[0], x.payload[1], x.payload[2], x.payload[3]])), (WELL_KNOWN, b"t") => m["content_type"] = json!(String::from_utf8_lossy(&x.payload)), _ => {} } } if m["uri"].is_null() { return Err("a smart poster with no URI record".into()); } if !titles.is_empty() { m["title"] = titles.values().next().cloned().unwrap_or(Value::Null); m["titles"] = Value::Object(titles); } m } (EXTERNAL, b"android.com:pkg") => json!({ "type": "android_app", "package": String::from_utf8_lossy(&r.payload) }), (EXTERNAL, k) => { let mut m = json!({ "type": "external", "domain_type": String::from_utf8_lossy(k) }); text_or_hex(&mut m, &r.payload); m } (MIME, k) => { let mut m = json!({ "type": "mime", "mime": String::from_utf8_lossy(k) }); text_or_hex(&mut m, &r.payload); m } (ABSOLUTE, k) => { let mut m = json!({ "type": "absolute_uri", "uri": String::from_utf8_lossy(k) }); text_or_hex(&mut m, &r.payload); m } (EMPTY, _) => json!({ "type": "empty" }), (UNKNOWN, _) => json!({ "type": "unknown", "hex": hex::encode(&r.payload) }), (_, k) => { let mut m = json!({ "type": "well_known", "name": String::from_utf8_lossy(k) }); text_or_hex(&mut m, &r.payload); m } }; if !r.id.is_empty() { m["id"] = json!(String::from_utf8_lossy(&r.id)); } Ok(m)}/// Builds an NDEF message from records (see the top of the file); answers hex. A payload of 256 bytes or more gets a long record by itself.pub fn ndef_build(records: Vec<serde_json::Value>) -> Result<String, String> { if records.is_empty() { return Err("a message needs at least one record; use { type: \"empty\" } for an empty tag".into()); } let recs = records.iter().map(record_of).collect::<Result<Vec<_>, _>>()?; Ok(hex::encode(write_all(&recs, 0)?))}/// Builds a message with every payload over `chunk_size` bytes split into chunked records, as a phone sending over peer-to-peer may.pub fn ndef_build_chunked(records: Vec<serde_json::Value>, chunk_size: i64) -> Result<String, String> { if chunk_size < 1 { return Err("chunk_size must be at least 1".into()); } if records.is_empty() { return Err("a message needs at least one record".into()); } let recs = records.iter().map(record_of).collect::<Result<Vec<_>, _>>()?; Ok(hex::encode(write_all(&recs, chunk_size as usize)?))}/// Reads an NDEF message (hex or bytes) into records, chunks joined, smart posters opened: `{ records, count, chunked }`. A damaged message is an error naming the byte.pub fn ndef_parse(message: serde_json::Value) -> Result<Value, String> { let b = bytes_of(&message)?; let (recs, chunked) = read_all(&b)?; let list = recs.iter().map(|r| describe(r, 0)).collect::<Result<Vec<_>, _>>()?; Ok(json!({ "count": list.len(), "chunked": chunked, "records": list }))}/// Each record's header as it is on the tag, for looking at one: flags, TNF, type, lengths. Chunks are shown one by one.pub fn ndef_inspect(message: serde_json::Value) -> Result<Vec<Value>, String> { let b = bytes_of(&message)?; let mut out = vec![]; let mut i = 0; while i < b.len() { let h = b[i]; let sr = h & 0x10 != 0; let il = h & 0x08 != 0; let mut at = i + 1; let tl = *b.get(at).ok_or("cut off")? as usize; at += 1; let pl = if sr { *b.get(at).ok_or("cut off")? as usize } else { let v = b.get(at..at + 4).ok_or("cut off")?; u32::from_be_bytes([v[0], v[1], v[2], v[3]]) as usize }; at += if sr { 1 } else { 4 }; let idl = if il { *b.get(at).ok_or("cut off")? as usize } else { 0 }; at += il as usize; let t = b.get(at..at + tl).ok_or("cut off")?; out.push(json!({ "at": i, "mb": h & 0x80 != 0, "me": h & 0x40 != 0, "cf": h & 0x20 != 0, "sr": sr, "tnf": h & 7, "type": String::from_utf8_lossy(t), "id_length": idl, "payload_length": pl })); i = at + tl + idl + pl; } Ok(out)}/// The URI prefix table's code for a URI, and what is left after it: `{ code, prefix, rest }`.pub fn ndef_uri_prefix(uri: &str) -> Value { let p = uri_payload(uri); json!({ "code": p[0], "prefix": PREFIXES[p[0] as usize], "rest": String::from_utf8_lossy(&p[1..]) })}/// Wraps a message in the TLV a Type 2 tag stores: 0x03, the length (three bytes past 254), the message, 0xFE.pub fn ndef_tlv_wrap(message: serde_json::Value) -> Result<String, String> { let m = bytes_of(&message)?; if m.len() > 0xfffe { return Err("a TLV holds at most 65534 bytes".into()); } let mut out = vec![3u8]; if m.len() < 255 { out.push(m.len() as u8); } else { out.push(0xff); out.extend((m.len() as u16).to_be_bytes()); } out.extend(&m); out.push(0xfe); Ok(hex::encode(out))}/// Finds the NDEF message in a tag's memory dump (TLVs), skipping NULL, lock and memory control TLVs; answers its hex, or null when the tag holds none.pub fn ndef_tlv_unwrap(memory: serde_json::Value) -> Result<Option<String>, String> { let b = bytes_of(&memory)?; let mut i = 0; while i < b.len() { match b[i] { 0x00 => i += 1, 0xfe => return Ok(None), t => { let n0 = *b.get(i + 1).ok_or("a TLV is cut off")? as usize; let (n, head) = if n0 == 0xff { let v = b.get(i + 2..i + 4).ok_or("a TLV is cut off")?; (((v[0] as usize) << 8) | v[1] as usize, 4) } else { (n0, 2) }; let body = b.get(i + head..i + head + n).ok_or(format!("the TLV at byte {} says {} bytes, past the end of the memory", i, n))?; if t == 3 { return Ok(Some(hex::encode(body))); } i += head + n; } } } Ok(None)}Related
mbus.qwer · 1.0.0Heat, water and gas meters on M-Bus through a TCP gateway: long frames with lengths, checksum and stop byte checked, variable data records decoded with units, storage numbers, tariffs, BCD and dates, multi-telegram readouts with the FCB toggled, meters selected by secondary address with wildcards, a bus scanned, and frames built by hand.3 views · 8 downloadsModbus frames modbus.qwer · 1.0.0Modbus RTU and TCP frames built and read without a device: register and coil requests, answers and exceptions explained, CRC-16 and MBAP headers, registers turned into integers, floats and text in any word order and back, and a TCP client for a real PLC or meter.8 views · 10 downloadsThread 0 posts
On NFC NDEF messages: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.