RFID System
Moisture Sensing

Moisture Sensing

Parlevel can read the fluid fill level of tagged bottles in real time using Magnus S3 sensor tags (AZN3120-AFR). The sensor stores a 9-bit value in the tag's Reserved memory bank that changes with the liquid level inside the bottle.

How it works

  1. The U300 reads the EPC of all tags in range during its normal inventory cycle
  2. Every ~3 seconds, Parlevel reads the Reserved bank word 12 from each sensor tag via /banks/read/batch
  3. The raw 9-bit SC value (0–511) is mapped to a calibrated volume (mL), then to a level band (Full, 3/4, 1/2, 1/4, Empty)
  4. The level is shown as a badge on the product card in the zone view

SC value → level

The mapping uses a calibration curve stored in the rfid_tag_families Supabase table. For AZN3120-AFR:

SC valueFill levelApprox. volume
~490–511Full700 mL
~420–4893/4~525 mL
~320–4191/2~350 mL
~180–3191/4~175 mL
~0–179Empty< 50 mL

Exact thresholds are stored in level_bands in the tag family row and can be adjusted without a code change.

Poll pipeline

GET /api/rfid/u300/moisture/live

        ├── 1. Fetch live tags from relay (/inventory)

        ├── 2. skipTidReads=true → all tags treated as AZN3120-AFR
        │         (no TID bank reads needed)

        ├── 3. Batch SC reads → POST /banks/read/batch
        │         (one stop/start inventory cycle for all tags)

        ├── 4. classifyFromHistory() per EPC
        │         (requires minReads=3 stable readings before emitting a level)

        └── 5. Returns { epc, level, readCount, isMagnus, tidPending }

Reading history and stability

To avoid flickering from noisy reads, Parlevel keeps a rolling history of SC values per EPC (default window: last 10 reads). A level is only emitted once minReads (default 3) consistent readings have been collected.

The useLiveFluidLevels React hook polls the endpoint every 3 seconds and maintains per-EPC history across polls using a useRef map.

skipTidReads

When skipTidReads: true (the default), Parlevel skips TID bank reads entirely and treats every tag in range as the configured tag family (AZN3120-AFR by default).

Set skipTidReads: false only if you mix sensor tags with plain inventory (barcode-only) tags on the same antenna — in that case TID reads are needed to distinguish tag types.

UI badges

BadgeMeaning
Full / 3/4 / 1/2 / 1/4 / EmptyStable level reading
Reading… (pulsing dot)Tag is in range, SC read in progress, not yet stable
(no badge)Tag is not a sensor tag, or not currently in antenna range

Badges disappear automatically when a tag leaves antenna range. History for out-of-range tags is pruned on the next poll.