RFID EPC tags
RFID tags' EPC numbers by GS1's Tag Data Standard: SGTIN-96 from a GTIN and serial, SSCC-96, SGLN-96, GRAI-96 and GIAI-96 encoded to the hex a tag holds, decoded back with their URIs and the GTIN, SSCC or GLN inside, and a reader's inventory.
Version 1.0.0 · 8 functions · 0 crates
Functions
What use epc.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use epc_sgtin, epc_sscc from epc (Quillo 0.6).
epc_sgtin(gtin: str, company_prefix_length: int, serial: int, filter: int) → stror an errorAn SGTIN-96 from a GTIN (8 to 14 digits, its check digit right), the length of its GS1 company prefix, and a serial (0 to 274,877,906,943). filter: 1 for a point-of-sale item, 2 a full case, 3 a load, 0 anything else. Gives 24 hex digits.
epc_sscc(sscc: str, company_prefix_length: int, filter: int) → stror an errorAn SSCC-96 from an SSCC (18 digits, its check digit right) and its company prefix length. Gives 24 hex digits.
epc_sgln(gln: str, company_prefix_length: int, extension: int, filter: int) → stror an errorAn SGLN-96 from a GLN (13 digits), its company prefix length and an extension (0 for the place itself). Gives 24 hex digits.
epc_grai(company_prefix: str, asset_type: str, serial: int, filter: int) → stror an errorA GRAI-96 (a returnable asset: a crate, a keg, a pallet) from its company prefix, asset type and serial. Gives 24 hex digits.
epc_giai(company_prefix: str, asset_reference: str, filter: int) → stror an errorA GIAI-96 (a fixed asset: a machine, a laptop) from its company prefix and a numeric asset reference. Gives 24 hex digits.
epc_decode(hex: str) → anyor an errorA 96-bit EPC (24 hex digits, as a reader reports it) decoded: { scheme, filter, company_prefix, reference, serial, gtin | sscc | gln, pure_uri, tag_uri }.
epc_from_uri(uri: str) → stror an errorA tag URI (urn:epc:tag:sgtin-96:3.0614141.812345.6789, and the sscc, sgln, grai and giai forms) as its 24 hex digits.
epc_inventory(hexes: list) → listMany reads decoded at once (a reader's inventory): each { hex, ... } as epc_decode gives it, or { hex, error }; repeats counted in reads.
Get it
$ quillo install epc # epc.qwer into this folder$ quillo install epc --example # and its example, epc.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/epc.qwer$ curl -O https://quillolang.org/plugins/files/epc.qweThen, in your program, all of it or only what you call:
use epc.qweruse epc_sgtin, epc_sscc from epc // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A clothing store's RFID: the EPC to write on each item's tag from its GTIN and a serial, the// pallet's SSCC and the shop floor's GLN as tags, and a reader's inventory decoded back.//// quillo run epc.qwe then open http://localhost:8080/encode and http://localhost:8080/inventoryuse "epc.qwer"route GET /encode = { shirt: epc_sgtin("80614141123458", 7, 6789, 1)?, from_uri: epc_from_uri("urn:epc:tag:sgtin-96:3.0614141.812345.6789")?, pallet: epc_sscc("106141411234567897", 7, 2)?, floor: epc_sgln("0614141123452", 7, 5678, 0)?, keg: epc_grai("0614141", "12345", 400, 0)?, laptop: epc_giai("0614141", "5678", 0)?, bad: epc_sgtin("80614141123459", 7, 1, 1).error,}route GET /decode = epc_decode("3074257BF7194E4000001A85")?route GET /inventory = epc_inventory(["3074257BF7194E4000001A85", "3074257BF7194E4000001A85", "3174257BF4499602D2000000", "E2801160600002084F9B2B5E"])$ quillo run epc.qwe$ curl 'localhost:8080/encode'$ curl 'localhost:8080/decode'$ curl 'localhost:8080/inventory'Source
//! RFID tags' EPC numbers by GS1's Tag Data Standard: SGTIN-96 (a product's GTIN and serial), SSCC-96 (a pallet), SGLN-96 (a place), GRAI-96 (a returnable asset) and GIAI-96 (a fixed asset) encoded to the hex an RFID printer or reader writes, and decoded back, with pure identity and tag URIs and the GTIN, SSCC or GLN inside.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};/// (header, scheme, [(company prefix bits, digits, reference bits, digits)] by partition, serial bits).struct Scheme { header: u8, name: &'static str, parts: [(u32, usize, u32, usize); 7], serial_bits: u32,}const SCHEMES: &[Scheme] = &[ Scheme { header: 0x30, name: "sgtin", parts: [(40, 12, 4, 1), (37, 11, 7, 2), (34, 10, 10, 3), (30, 9, 14, 4), (27, 8, 17, 5), (24, 7, 20, 6), (20, 6, 24, 7)], serial_bits: 38 }, Scheme { header: 0x31, name: "sscc", parts: [(40, 12, 18, 5), (37, 11, 21, 6), (34, 10, 24, 7), (30, 9, 28, 8), (27, 8, 31, 9), (24, 7, 34, 10), (20, 6, 38, 11)], serial_bits: 0 }, Scheme { header: 0x32, name: "sgln", parts: [(40, 12, 1, 0), (37, 11, 4, 1), (34, 10, 7, 2), (30, 9, 11, 3), (27, 8, 14, 4), (24, 7, 17, 5), (20, 6, 21, 6)], serial_bits: 41 }, Scheme { header: 0x33, name: "grai", parts: [(40, 12, 4, 0), (37, 11, 7, 1), (34, 10, 10, 2), (30, 9, 14, 3), (27, 8, 17, 4), (24, 7, 20, 5), (20, 6, 24, 6)], serial_bits: 38 }, Scheme { header: 0x34, name: "giai", parts: [(40, 12, 42, 13), (37, 11, 45, 14), (34, 10, 48, 15), (30, 9, 52, 16), (27, 8, 55, 17), (24, 7, 58, 18), (20, 6, 62, 19)], serial_bits: 0 },];fn check_digit(digits: &str) -> u32 { let s: u32 = digits.chars().rev().enumerate().map(|(i, c)| c.to_digit(10).unwrap_or(0) * if i % 2 == 0 { 3 } else { 1 }).sum(); (10 - s % 10) % 10}/// Bits written most significant first into a 96-bit number.struct W { v: u128, n: u32,}impl W { fn put(&mut self, x: u128, bits: u32) { self.v = (self.v << bits) | (x & ((1u128 << bits) - 1)); self.n += bits; } fn hex(self) -> String { format!("{:024X}", self.v << (96 - self.n)) }}fn get(v: u128, start: u32, len: u32) -> u128 { (v >> (96 - start - len)) & ((1u128 << len) - 1)}fn digits(s: &str) -> Result<String, String> { let d: String = s.chars().filter(|c| !c.is_whitespace() && *c != '-').collect(); if d.is_empty() || !d.chars().all(|c| c.is_ascii_digit()) { return Err(format!("{:?}: digits only", s)); } Ok(d)}fn partition(scheme: &Scheme, cp_len: i64) -> Result<(usize, (u32, usize, u32, usize)), String> { scheme.parts.iter().enumerate().find(|(_, p)| p.1 as i64 == cp_len).map(|(i, p)| (i, *p)).ok_or_else(|| "a GS1 company prefix is 6 to 12 digits".to_string())}fn filter_of(f: i64) -> Result<u128, String> { if !(0..=7).contains(&f) { return Err("the filter value is 0 to 7 (1 point of sale item, 2 case, 3 load, ...)".into()); } Ok(f as u128)}/// An SGTIN-96 from a GTIN (8 to 14 digits, its check digit right), the length of its GS1 company prefix, and a serial (0 to 274,877,906,943). `filter`: 1 for a point-of-sale item, 2 a full case, 3 a load, 0 anything else. Gives 24 hex digits.pub fn epc_sgtin(gtin: &str, company_prefix_length: i64, serial: i64, filter: i64) -> Result<String, String> { let g = format!("{:0>14}", digits(gtin)?); if g.len() != 14 || check_digit(&g[..13]).to_string() != g[13..] { return Err(format!("{:?} is not a GTIN with a right check digit", gtin)); } if serial < 0 || serial >= 1 << 38 { return Err("an SGTIN-96 serial is 0 to 274877906943".into()); } let s = &SCHEMES[0]; let (p, (cb, cd, rb, _)) = partition(s, company_prefix_length)?; // The item reference is the indicator digit, then the GTIN's digits after the company prefix. let cp: u128 = g[1..1 + cd].parse().unwrap(); let item: u128 = format!("{}{}", &g[..1], &g[1 + cd..13]).parse().unwrap(); let mut w = W { v: 0, n: 0 }; w.put(0x30, 8); w.put(filter_of(filter)?, 3); w.put(p as u128, 3); w.put(cp, cb); w.put(item, rb); w.put(serial as u128, 38); Ok(w.hex())}/// An SSCC-96 from an SSCC (18 digits, its check digit right) and its company prefix length. Gives 24 hex digits.pub fn epc_sscc(sscc: &str, company_prefix_length: i64, filter: i64) -> Result<String, String> { let d = digits(sscc)?; if d.len() != 18 || check_digit(&d[..17]).to_string() != d[17..] { return Err(format!("{:?} is not an SSCC with a right check digit", sscc)); } let s = &SCHEMES[1]; let (p, (cb, cd, rb, _)) = partition(s, company_prefix_length)?; let cp: u128 = d[1..1 + cd].parse().unwrap(); let serial: u128 = format!("{}{}", &d[..1], &d[1 + cd..17]).parse().unwrap(); let mut w = W { v: 0, n: 0 }; w.put(0x31, 8); w.put(filter_of(filter)?, 3); w.put(p as u128, 3); w.put(cp, cb); w.put(serial, rb); w.put(0, 24); Ok(w.hex())}/// An SGLN-96 from a GLN (13 digits), its company prefix length and an extension (0 for the place itself). Gives 24 hex digits.pub fn epc_sgln(gln: &str, company_prefix_length: i64, extension: i64, filter: i64) -> Result<String, String> { let d = digits(gln)?; if d.len() != 13 || check_digit(&d[..12]).to_string() != d[12..] { return Err(format!("{:?} is not a GLN with a right check digit", gln)); } if extension < 0 || extension >= 1 << 41 { return Err("an SGLN-96 extension is 0 to 2199023255551".into()); } let s = &SCHEMES[2]; let (p, (cb, cd, rb, _)) = partition(s, company_prefix_length)?; let mut w = W { v: 0, n: 0 }; w.put(0x32, 8); w.put(filter_of(filter)?, 3); w.put(p as u128, 3); w.put(d[..cd].parse().unwrap(), cb); w.put(if cd < 12 { d[cd..12].parse().unwrap() } else { 0 }, rb); w.put(extension as u128, 41); Ok(w.hex())}/// A GRAI-96 (a returnable asset: a crate, a keg, a pallet) from its company prefix, asset type and serial. Gives 24 hex digits.pub fn epc_grai(company_prefix: &str, asset_type: &str, serial: i64, filter: i64) -> Result<String, String> { let cp = digits(company_prefix)?; let s = &SCHEMES[3]; let (p, (cb, _, rb, rd)) = partition(s, cp.len() as i64)?; let at = if rd == 0 { String::new() } else { digits(asset_type)? }; if at.len() != rd { return Err(format!("with a {}-digit company prefix the asset type has {} digits", cp.len(), rd)); } if serial < 0 || serial >= 1 << 38 { return Err("a GRAI-96 serial is 0 to 274877906943".into()); } let mut w = W { v: 0, n: 0 }; w.put(0x33, 8); w.put(filter_of(filter)?, 3); w.put(p as u128, 3); w.put(cp.parse().unwrap(), cb); w.put(if rd == 0 { 0 } else { at.parse().unwrap() }, rb); w.put(serial as u128, 38); Ok(w.hex())}/// A GIAI-96 (a fixed asset: a machine, a laptop) from its company prefix and a numeric asset reference. Gives 24 hex digits.pub fn epc_giai(company_prefix: &str, asset_reference: &str, filter: i64) -> Result<String, String> { let cp = digits(company_prefix)?; let r = digits(asset_reference)?; let s = &SCHEMES[4]; let (p, (cb, _, rb, _)) = partition(s, cp.len() as i64)?; let rv: u128 = r.parse().map_err(|_| "the asset reference is too long".to_string())?; if rv >= 1u128 << rb || r.starts_with('0') && r.len() > 1 { return Err(format!("with a {}-digit company prefix the asset reference fits {} bits and has no leading zero", cp.len(), rb)); } let mut w = W { v: 0, n: 0 }; w.put(0x34, 8); w.put(filter_of(filter)?, 3); w.put(p as u128, 3); w.put(cp.parse().unwrap(), cb); w.put(rv, rb); Ok(w.hex())}/// A 96-bit EPC (24 hex digits, as a reader reports it) decoded: `{ scheme, filter, company_prefix, reference, serial, gtin | sscc | gln, pure_uri, tag_uri }`.pub fn epc_decode(hex: &str) -> Result<serde_json::Value, String> { let h: String = hex.chars().filter(|c| !c.is_whitespace()).collect(); let h = h.trim_start_matches("0x").trim_start_matches("0X"); if h.len() != 24 || !h.chars().all(|c| c.is_ascii_hexdigit()) { return Err("a 96-bit EPC is 24 hex digits".into()); } let v = u128::from_str_radix(h, 16).unwrap(); let header = get(v, 0, 8) as u8; let s = SCHEMES.iter().find(|s| s.header == header).ok_or_else(|| format!("header {:02X} is not SGTIN-96, SSCC-96, SGLN-96, GRAI-96 or GIAI-96", header))?; let filter = get(v, 8, 3) as u8; let p = get(v, 11, 3) as usize; if p > 6 { return Err("partition 7 does not exist: not a valid EPC".into()); } let (cb, cd, rb, rd) = s.parts[p]; let cp = format!("{:0w$}", get(v, 14, cb), w = cd); let rv = get(v, 14 + cb, rb); let reference = if rd == 0 { String::new() } else if s.name == "giai" { rv.to_string() } else { format!("{:0w$}", rv, w = rd) }; if cp.len() != cd || reference.len() > rd.max(1) && s.name != "giai" { return Err("the company prefix or reference has more digits than its partition allows".into()); } let serial = if s.serial_bits > 0 { Some(get(v, 14 + cb + rb, s.serial_bits)) } else { None }; let mut out = json!({ "scheme": format!("{}-96", s.name), "filter": filter, "partition": p, "company_prefix": cp, "reference": reference, "serial": serial.map(|x| x.to_string()) }); let (pure, tag) = match s.name { "sgtin" => { let g13 = format!("{}{}{}", &reference[..1], cp, &reference[1..]); out["gtin"] = json!(format!("{}{}", g13, check_digit(&g13))); (format!("urn:epc:id:sgtin:{}.{}.{}", cp, reference, serial.unwrap()), format!("urn:epc:tag:sgtin-96:{}.{}.{}.{}", filter, cp, reference, serial.unwrap())) } "sscc" => { let s17 = format!("{}{}{}", &reference[..1], cp, &reference[1..]); out["sscc"] = json!(format!("{}{}", s17, check_digit(&s17))); (format!("urn:epc:id:sscc:{}.{}", cp, reference), format!("urn:epc:tag:sscc-96:{}.{}.{}", filter, cp, reference)) } "sgln" => { let g12 = format!("{}{}", cp, reference); out["gln"] = json!(format!("{}{}", g12, check_digit(&g12))); (format!("urn:epc:id:sgln:{}.{}.{}", cp, reference, serial.unwrap()), format!("urn:epc:tag:sgln-96:{}.{}.{}.{}", filter, cp, reference, serial.unwrap())) } "grai" => (format!("urn:epc:id:grai:{}.{}.{}", cp, reference, serial.unwrap()), format!("urn:epc:tag:grai-96:{}.{}.{}.{}", filter, cp, reference, serial.unwrap())), _ => (format!("urn:epc:id:giai:{}.{}", cp, reference), format!("urn:epc:tag:giai-96:{}.{}.{}", filter, cp, reference)), }; out["pure_uri"] = json!(pure); out["tag_uri"] = json!(tag); Ok(out)}/// A tag URI (`urn:epc:tag:sgtin-96:3.0614141.812345.6789`, and the sscc, sgln, grai and giai forms) as its 24 hex digits.pub fn epc_from_uri(uri: &str) -> Result<String, String> { let rest = uri.trim().strip_prefix("urn:epc:tag:").ok_or("a tag URI starts urn:epc:tag:")?; let (scheme, body) = rest.split_once(':').ok_or("no scheme")?; let p: Vec<&str> = body.split('.').collect(); let f = |i: usize| -> Result<i64, String> { p.get(i).ok_or("a part is missing".to_string())?.parse::<i64>().map_err(|_| format!("{:?} is not a number", p[i])) }; match scheme { "sgtin-96" if p.len() == 4 => { let (cp, item) = (p[1], p[2]); let g13 = format!("{}{}{}", &item[..1.min(item.len())], cp, item.get(1..).unwrap_or("")); epc_sgtin(&format!("{}{}", g13, check_digit(&g13)), cp.len() as i64, f(3)?, f(0)?) } "sscc-96" if p.len() == 3 => { let s17 = format!("{}{}{}", &p[2][..1.min(p[2].len())], p[1], p[2].get(1..).unwrap_or("")); epc_sscc(&format!("{}{}", s17, check_digit(&s17)), p[1].len() as i64, f(0)?) } "sgln-96" if p.len() == 4 => { let g12 = format!("{}{}", p[1], p[2]); epc_sgln(&format!("{}{}", g12, check_digit(&g12)), p[1].len() as i64, f(3)?, f(0)?) } "grai-96" if p.len() == 4 => epc_grai(p[1], p[2], f(3)?, f(0)?), "giai-96" if p.len() == 3 => epc_giai(p[1], p[2], f(0)?), _ => Err(format!("{:?}: sgtin-96, sscc-96, sgln-96, grai-96 or giai-96 with its parts", scheme)), }}/// Many reads decoded at once (a reader's inventory): each `{ hex, ... }` as `epc_decode` gives it, or `{ hex, error }`; repeats counted in `reads`.pub fn epc_inventory(hexes: Vec<String>) -> Vec<serde_json::Value> { let mut seen: Vec<(String, Value)> = vec![]; for h in hexes { let k = h.trim().to_uppercase(); if let Some(e) = seen.iter_mut().find(|(x, _)| *x == k) { e.1["reads"] = json!(e.1["reads"].as_i64().unwrap_or(1) + 1); continue; } let mut v = epc_decode(&k).unwrap_or_else(|e| json!({ "error": e })); v["hex"] = json!(k); v["reads"] = json!(1); seen.push((k, v)); } seen.into_iter().map(|(_, v)| v).collect()}Related
barcode.qwer · 1.0.0EAN-13, EAN-8, UPC-A, Code 128 and Code 39 barcodes as SVG or PNG, with the digits underneath and check digits worked out.6 views · 5 downloadsG-code for 3D printers gcode.qwer · 1.0.0G-code from 3D printers and CNC machines read: print time with acceleration, filament length, weight and cost, the print's size, layers and their heights, temperatures and the slicer's notes, and a layer drawn as SVG.6 views · 7 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 downloadsNMEA GPS nmea.qwer · 1.0.0NMEA 0183 sentences from GPS receivers read: position, time, speed, course, altitude, satellites and fix quality, with checksums checked.7 views · 8 downloadsOBD-II obd2.qwer · 1.0.0OBD-II answers from an ELM327 adapter decoded: speed, RPM, temperatures, load, fuel level and voltage, trouble codes such as P0133, and the PIDs a car supports.8 views · 8 downloadsReceipt printers escpos.qwer · 1.0.0Receipts for ESC/POS thermal printers: aligned and bold lines, two-column rows, QR codes and barcodes, the cutter and the cash drawer, as base64, a file, or sent to port 9100.7 views · 6 downloadsThread 0 posts
On RFID EPC tags: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.