Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions data/tag-allocations/blocks.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
block_id,first,last,ordered_qty,buffer_pct,tag_type,last_used,status
PO7299-L1,10000001,10002625,2500,5,Avery Miniweb,,issued
PO7299-L2,10002626,10005250,2500,5,Dogbone,,issued
PO7299-L3,10005251,10007875,2500,5,Avery Web,,issued
PO7299-L4,10007876,10012600,4500,5,Tageos EOS 261 U9 white PET,,issued
4 changes: 4 additions & 0 deletions data/tag-allocations/namespaces.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
code,status,note
000,standard,TrakRF standard pool
001,reserved,customer namespace (owner in private mapping)
0A5,reserved,documentation example on the tag numbering page; never assign
2 changes: 2 additions & 0 deletions docs/integrations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ None of the integration surfaces below are documented yet for self-service. Earl

## What's documented today

**[Tag numbering and encoding](./tag-numbering)** — how TrakRF numbers the tags it supplies: the EPC layout, encoding instructions for print bureaus, label rules, and first-article checks.

For REST API integration (the primary customer-facing integration surface), start at **[Getting started → Using the API](/docs/getting-started/api)** and the **[interactive API reference](/api)**.
35 changes: 35 additions & 0 deletions docs/integrations/tag-allocations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
# Generated by scripts/allocate-tags.mjs from data/tag-allocations/. Do not edit:
# pnpm lint fails if this page and the data disagree.
sidebar_position: 5
title: Tag allocation register
description: Every block of TrakRF standard-pool tag numbers issued so far, and every namespace code assigned.
---

# Tag allocation register

Every block of [standard-pool](./tag-numbering#how-the-numbers-are-issued) tag numbers TrakRF has issued, and every namespace code assigned. Numbers are drawn from one global counter, in order, and never reissued.

## Standard-pool blocks

The next block starts at **10012601**.

| Block | First | Last | Ordered | Buffer | Tag type | Status | Last used |
| --------- | -------- | -------- | ------- | ------ | --------------------------- | ------ | --------- |
| PO7299-L1 | 10000001 | 10002625 | 2500 | 125 | Avery Miniweb | issued | — |
| PO7299-L2 | 10002626 | 10005250 | 2500 | 125 | Dogbone | issued | — |
| PO7299-L3 | 10005251 | 10007875 | 2500 | 125 | Avery Web | issued | — |
| PO7299-L4 | 10007876 | 10012600 | 4500 | 225 | Tageos EOS 261 U9 white PET | issued | — |

- **Ordered** is the first part of the block; **Buffer** is the overrun allowance at its end.
- **Status**: `issued` (with the bureau), `closed` (last number used reported, the rest retired), or `void` (cancelled, never reissued).

## Namespaces

| Code | Status | Note |
| ----- | -------- | ------------------------------------------------------------- |
| `000` | standard | TrakRF standard pool |
| `001` | reserved | customer namespace (owner in private mapping) |
| `0A5` | reserved | documentation example on the tag numbering page; never assign |

Customer namespaces control their own sequencing, so they have no blocks here. A code is never reassigned, even after a customer leaves.
197 changes: 197 additions & 0 deletions docs/integrations/tag-numbering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
---
sidebar_position: 4
title: Tag numbering and encoding
description: How TrakRF numbers the RFID tags it supplies — the 96-bit EPC layout, customer namespaces, encoding instructions for print bureaus, label rules, and acceptance checks.
---

# Tag numbering and encoding

This page is the reference for how TrakRF numbers the RFID tags it supplies. It is written for
print and encoding bureaus, partners, and anyone receiving TrakRF-encoded tags.

## Summary

A tag numbered from TrakRF's standard pool carries one number, written two ways:

- **On the label**: an 8-digit sequence number, for example `31570344`.
- **In the chip**: a 96-bit EPC made of a fixed 12-character prefix, four reserved zeros, and the
same 8 digits.

| Printed on the label | Encoded in the chip (EPC, hex) |
| -------------------- | ------------------------------ |
| `31570344` | `355E4ACFD000000031570344` |

The last 8 characters of the EPC are always the printed number. The sequence digits are written
into the EPC as decimal digits, one digit per hex character, so nothing after the prefix is ever
`A`–`F`.

Customers with their own numbering get a [customer namespace](#customer-namespaces) instead.

## EPC layout

The EPC is 24 hex characters (96 bits). Positions count from the left, starting at 1.

| Characters | Field | Value |
| ---------- | ------------------------ | --------------------------------------------------------------- |
| 1–2 | Header | `35` (GID-96) |
| 3–9 | General Manager Number | `5E4ACFD` (98872573 decimal) |
| 10–12 | Namespace code | `000` = TrakRF standard pool; `001`–`FFF` = customer namespaces |
| 13–16 | Reserved (standard pool) | `0000` |
| 17–24 | Sequence (standard pool) | 8 decimal digits, `10000001`–`99999999` |

In GID-96 terms, characters 10–15 are the Object Class field and characters 16–24 are the serial
field. The namespace code occupies the top of the Object Class.

Characters 1–12, `355E4ACFD000`, are the standard-pool prefix. A customer namespace replaces `000`
with its own code.

### How the numbers are issued

- Sequence numbers come from one global counter, starting at `10000001`. A number is never
reused.
- The standard pool (namespace `000`) covers TrakRF's own numbering for every customer, order, and
demo roll, with no customer code in the EPC. Customers with their own numbering use a
[customer namespace](#customer-namespaces).
- Each order is allocated a contiguous block: the ordered quantity plus an overrun buffer (5% by
default, adjusted to the bureau's stated tolerance). Blocks needn't start or end on round
numbers. Buffer numbers that go unused are retired, not reissued.
- Within a roll, tags run from the lowest number to the highest.
- The [tag allocation register](./tag-allocations) lists every block issued so far.

### What a standard decoder shows

The layout is a valid GID-96 encoding, so standard EPC decoders read it. For the example above
they show:

```text
urn:epc:id:gid:98872573.0.827786052
```

The serial a decoder shows (`827786052`) is not the printed number. GID-96 reads the last 9 hex
characters (`031570344`) as one binary value. The printed number is those same characters read
as decimal digits. Match tags to labels on the last 8 characters of the EPC, never on the
decoded serial.

## Customer namespaces

A customer namespace prefix is `355E4ACFD` plus a 3-character namespace code, `001`–`FFF`. The
customer controls characters 13–24 (48 bits) and can use any sequencing in them: hex or decimal,
starting from zero or one.

For example, with the illustrative namespace code `0A5` (not a real customer) and a one-based hex
sequence:

| EPC | Standard decoder shows |
| -------------------------- | ---------------------------------- |
| `355E4ACFD0A5000000000001` | `urn:epc:id:gid:98872573.675840.1` |
| `355E4ACFD0A5000000000002` | `urn:epc:id:gid:98872573.675840.2` |

A standard decoder shows the Object Class as the namespace code × 4096 plus the value of the first
3 characters of the customer's tail, read as hex. That's expected, for example:

```text
355E4ACFD0A5A00000000001 → urn:epc:id:gid:98872573.678400.1
```

Match on the EPC characters, not the decoded fields.

- **Customer-required rightmost characters**: when a customer's systems key on their last N
characters, those characters are encoded unchanged at the right. Any padding goes between the
prefix and their value, never inside it.
- **No all-zero prefixes by default.** Zero-padded EPCs collide with other vendors' tags and
clutter shared reader fields, so for a customer with its own numbering TrakRF proposes a
namespace. A zero-padded or other customer-mandated prefix is used only when the customer
explicitly requires it. Those tags fall outside both the standard pool and the namespaces.
- **Codes are never reassigned.** TrakRF records each namespace code in its own register and
doesn't reassign it, even after a customer leaves.

## Encoding instructions for bureaus

### All orders

- Write the **EPC bank (MB01)**: a **96-bit EPC**, with the EPC length in the PC word set to
**6 words**. Leave the other PC bits at the chip default.
- **No lock and no access password.**
- **On an encode failure, re-encode the same value on the next good tag.** Never skip a number.
- **Return an encode log for every run**, with one row per encoded tag: EPC and TID, plus the
data-file row number where there is a data file.

### Standard pool (namespace `000`)

TrakRF supplies a data file with one row per tag. Encode from that file only, writing the
`epc_hex` value from each row.

**Don't use a start-and-increment rule.** The sequence is stored as decimal digits, and encoder
software counts in binary, so it would produce `A`–`F` characters. Take each EPC from its row.

The file columns are:

```text
order,line,roll,tag_type,sequence,epc_hex,barcode,human_readable,overrun
```

Rows with `overrun=yes` are the buffer. Encode in file order and use buffer rows only to cover
overruns. Report the last number used back to TrakRF.

### Customer namespace

The order specifies either a data file, or a start value with a hex increment.

## Label rules

| Element | Content |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Standard pool: human-readable text | The 8-digit sequence |
| Standard pool: barcode | Code 128, holding the same 8 digits |
| Standard pool: QR code | Only when the order asks for one: the full 24-character EPC |
| Header text | Specified per order |
| Customer namespace order | Human-readable, barcode and QR content as specified per order — typically the customer's required characters, not the full EPC |

## Acceptance checks

### First article, before the full run

1. The EPC decodes as GID-96 with General Manager Number `98872573`.
2. The last 8 characters of the EPC equal the printed number. For a customer namespace, the
customer's required rightmost characters equal the printed value.
3. The barcode scans to the same 8 digits. For a customer namespace, it scans to the customer's
printed value.

Worked through for the standard-pool example:

| Check | Result |
| ------------------ | ------------------------------------------------------------------ |
| Decodes as GID-96 | `355E4ACFD000000031570344` → `urn:epc:id:gid:98872573.0.827786052` |
| Last 8 = printed | `…31570344` = `31570344` |
| Barcode = 8 digits | Code 128 `31570344` |

### After the run, from the encode log

- No two tags in the run carry the same EPC. Across runs, the bureau doesn't need to check against
earlier orders: TrakRF's allocation register draws every standard-pool block once, so no number is
issued twice.
- The last number used is reported to TrakRF.

## How TrakRF shows tag IDs

TrakRF stores and matches on the full EPC. Today the app shows a TrakRF tag as its full EPC, for
example `355E4ACFD000000031570344`, and you find a tag by its full EPC — for instance by pasting
it into [Locate](/docs/app-tour/locate). The app trims leading zeros from tag values, which
doesn't change a TrakRF EPC because it starts with `3`.

**Planned:** a per-organization prefix list, with `355E4ACFD000` as the default. When an EPC
starts with a listed prefix, the app removes the prefix and strips leading zeros for display, so a
standard-pool tag shows as its printed 8 digits. Searching by those 8 digits or by the full EPC
will find the same tag. A customer namespace prefix is added to that organization's list, so its
tags show as the customer's characters with leading zeros stripped. This page will be updated
when that ships.

## Status and caveats

- **`98872573` is a General Manager Number TrakRF assigned itself.** It was not issued by GS1.
- **The scheme is for closed-loop use.** Don't publish these IDs into open GS1 or EPCIS supply
chains.
- **A move to a RAIN CIN or GS1 prefix is planned.** The sequence pool stays global across any
prefix change, so an 8-digit number never exists under two prefixes.

Questions about an order or a data file: [email support](mailto:support@trakrf.id).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "docusaurus build",
"serve": "docusaurus serve --host 0.0.0.0",
"typecheck": "tsc",
"lint": "prettier --check . && node scripts/check-contributing.mjs",
"lint": "prettier --check . && node scripts/check-contributing.mjs && node scripts/check-tag-numbering.mjs && node scripts/check-tag-allocations.mjs",
"lint:fix": "prettier --write .",
"clear": "docusaurus clear",
"swizzle": "docusaurus swizzle",
Expand Down
128 changes: 128 additions & 0 deletions scripts/allocate-tags.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env node
// Issues TrakRF standard-pool tag numbers. Every allocation goes through this
// script and a pull request; nobody edits blocks.csv by hand to issue numbers.
//
// allocate --block-id PO1234-L1 --qty 2500 [--buffer 5] --tag-type "Avery Web"
// export --block-id PO1234-L1 [--out file.csv] per-tag bureau file
// close --block-id PO1234-L1 --last-used 10002590
// page re-render the register page
//
// --data DIR works on another copy of the register (tests); the public page is
// only re-rendered for the real one.

import { writeFile } from "node:fs/promises";
import {
EXPORT_COLUMNS,
SEQ_MAX,
bufferFor,
defaultDataDir,
exportRows,
nextFirst,
readRegister,
toCsv,
validate,
writeBlocks,
writePage,
} from "./lib/tag-allocations.mjs";

const [command, ...rest] = process.argv.slice(2);
const opts = {};
for (let i = 0; i < rest.length; i += 2) {
if (!rest[i].startsWith("--")) fail(`unexpected argument ${rest[i]}`);
opts[rest[i].slice(2)] = rest[i + 1];
}
const dataDir = opts.data ?? defaultDataDir;

function fail(message) {
console.error(`allocate-tags: ${message}`);
process.exit(1);
}
function need(name) {
if (opts[name] === undefined || opts[name] === "")
fail(`--${name} is required`);
return opts[name];
}

const register = await readRegister(dataDir);
const existing = validate(register);
if (existing.length)
fail(`register is inconsistent, fix it first:\n ${existing.join("\n ")}`);
const findBlock = (id) =>
register.blocks.find((b) => b.block_id === id) ?? fail(`no block ${id}`);

async function save() {
const errors = validate(register);
if (errors.length)
fail(
`refusing to write an inconsistent register:\n ${errors.join("\n ")}`,
);
await writeBlocks(register.blocks, dataDir);
if (dataDir === defaultDataDir) await writePage(register);
}

switch (command) {
case "allocate": {
const id = need("block-id");
const qty = Number(need("qty"));
const pct = Number(opts.buffer ?? 5);
const tagType = need("tag-type");
if (!Number.isInteger(qty) || qty < 1)
fail("--qty must be a positive integer");
if (!Number.isInteger(pct) || pct < 0)
fail("--buffer must be a whole percentage");
if (register.blocks.some((b) => b.block_id === id))
fail(`block ${id} already exists`);
const first = nextFirst(register.blocks);
const last = first + qty + bufferFor(qty, pct) - 1;
if (last > SEQ_MAX) fail(`block would end at ${last}, past ${SEQ_MAX}`);
register.blocks.push({
block_id: id,
first: String(first),
last: String(last),
ordered_qty: String(qty),
buffer_pct: String(pct),
tag_type: tagType,
last_used: "",
status: "issued",
});
await save();
console.log(
`${id}: ${first}–${last} (${qty} ordered + ${last - first + 1 - qty} buffer)`,
);
break;
}
case "export": {
const csv = toCsv(EXPORT_COLUMNS, exportRows(findBlock(need("block-id"))));
if (opts.out) await writeFile(opts.out, csv);
else process.stdout.write(csv);
break;
}
case "close": {
const block = findBlock(need("block-id"));
const lastUsed = need("last-used");
if (block.status !== "issued")
fail(`${block.block_id} is ${block.status}, not issued`);
if (
!/^[0-9]{8}$/.test(lastUsed) ||
+lastUsed < +block.first ||
+lastUsed > +block.last
)
fail(`--last-used must be within ${block.first}–${block.last}`);
block.last_used = lastUsed;
block.status = "closed";
await save();
console.log(
`${block.block_id}: closed at ${lastUsed}; ${+block.last - +lastUsed} numbers retired`,
);
break;
}
case "page":
if (dataDir !== defaultDataDir) fail("page renders the real register only");
await writePage(register);
console.log("docs/integrations/tag-allocations.md re-rendered");
break;
default:
fail(
"usage: allocate | export | close | page (see the header of this script)",
);
}
Loading
Loading