Decisions
Measure before tuning
For a long time I judged the positions by watching the map. Now a seeded replay of the simulator compares every update with where the tag really was, and every variant sees the same signals. The first run was humbling: the linearised trilateration I started with lost to a plain weighted centroid from 3 dB of noise up and put a quarter of its answers outside the building at 6 dB. Each receiver's RSSI is now smoothed in dB and the position is fitted in log-distance space, where the noise actually is, clamped to the floor plan.
- Median position error at 6 dB of noise went from 3.59 m to 2.04 m, measured against the simulator's truth.measured · README.md
- At the simulator's default noise of 1.2 dB, the median error is 1.03 m and the 95th percentile 1.62 m, down from 1.95 m.measured · README.md
- Each receiver's signal strength is smoothed in dB, the position is fitted in log-distance space when three or more receivers hear a tag, and a light moving average keeps markers from jumping.design · README.md
Fit the error nobody calibrates
Nobody knows their building's path-loss exponent exactly. With four or more receivers the fit also estimates one scale on the distances, which is what a wrong exponent does to them. Smoothing the readings without it hurt that case, 3.03 m to 5.20 m median, because the fit settled confidently on the biased answer. That scenario is the one the scale models, so the benchmark also has one it doesn't: a fixed offset per receiver, where the new estimator beats the old one but the scale term barely helps.
- When the building's path-loss exponent is 3.0 and the solver assumes 2.5, the 95th percentile error is 2.4 m, where the old solver's was 20.1 m with a third of its answers outside the building.measured · README.md
Recognize a tag exactly once
Several receivers can hear a new tag in the same instant, so several messages race to create it. A message that finds no entity for the fingerprint tries to insert one, and the insert does nothing if another message got there first, so the unique constraint alone decides. Only the message whose insert went in announces the new tag, and one entity comes out no matter how many arrive together.
- The first sighting of a tag creates its entity exactly once, because the insert skips a fingerprint that already exists, so the unique constraint alone decides.design · apps/api/src/entities/entity.store.ts
Keep a day, back up what matters
Position history is simulator traffic, so the nightly backup leaves it out and the table keeps one day in hourly chunks. I added the retention after finding a 3.65 GB table on the server and a history view that never finished loading. It now loads the last 10 minutes or hour.
- Position history keeps one day in hourly chunks. Without retention it had grown to a 3.65 GB table in under four days and the history view never loaded.measured · README.md
- Backups leave the position history out on purpose, because it is simulator traffic that refills in minutes and keeping it made a 3 GB volume out of a few kilobytes of real data.measured · README.md