Resolving Floor Ambiguity with Beacon Hints
Part of Indoor Floor-Level Detection. Buildings with repeating floor plates produce fingerprints that a radio estimator cannot separate, however well it is tuned. The fix is not a better estimator — it is one piece of evidence that only exists on one floor.
Why Aliasing Is Not a Tuning Problem
A fingerprint is a vector of access-point signal strengths. On a tower whose floors are built to the same plate, with access points mounted in the same positions on each floor, the vector measured at the second-floor lift lobby and the vector measured at the fourth-floor lift lobby differ by fractions of a decibel — well inside the noise of a single scan.
That is not an estimator failure. There is genuinely almost no information in the measurement that
distinguishes the two locations, so no weighting scheme, no larger k, and no amount of survey
depth will separate them reliably. Adding evidence is the only approach that works.
Level-Tagged Beacons
A level-tagged beacon is an ordinary BLE beacon whose identifier is registered against exactly one level, deployed so that it is heard on that level and not on adjacent ones. Hearing it is proof of level in a way no fingerprint match is.
Making it work is mostly a placement problem: BLE penetrates floor slabs poorly but not not at all,
and a beacon at high tx_power mounted near a stairwell void will be heard a floor away.
import logging
from dataclasses import dataclass
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class LevelHint:
level: float
beacon_id: str
rssi: float
def level_from_beacons(scan: dict[str, float], registry: dict[str, float],
*, floor_rssi: float = -85.0) -> LevelHint | None:
"""Strongest level-tagged beacon above the floor threshold, or None."""
seen = [(bid, rssi) for bid, rssi in scan.items()
if bid in registry and rssi >= floor_rssi]
if not seen:
return None
best_id, best_rssi = max(seen, key=lambda kv: kv[1])
levels = {registry[bid] for bid, _ in seen}
if len(levels) > 1:
# heard beacons from two levels: trust the strongest, but say so
logger.info("level-tagged beacons from %s heard; taking %+g at %.0f dBm",
sorted(levels), registry[best_id], best_rssi)
return LevelHint(registry[best_id], best_id, best_rssi)
The floor_rssi threshold is what keeps a through-slab detection from being treated as proof. A
beacon heard at −92 dBm is almost certainly on another floor; one heard at −65 dBm is on this one.
Setting the threshold at −85 dBm and validating it by walking the floor above each beacon is a
half-day of commissioning that removes the entire class of false level changes.
Precedence, Not Averaging
The instinct with several sources is to combine them probabilistically. For level detection that is wrong, because the sources are not measurements of the same quantity with different noise. A beacon identifier is proof — the device is within radio range of a beacon that exists on exactly one floor. A fingerprint match is an inference from an aliasing-prone measurement. Averaging a proof with an inference produces something weaker than the proof.
So the sources are ordered, and the first one that can answer does. The one place combination matters is what happens after: a beacon hint does not merely set the level, it re-anchors the barometer to that level, which is what lets the cheap source carry the estimate until the next beacon is heard.
Placement and Commissioning
| Decision | Recommendation | Why |
|---|---|---|
| Beacons per level | 1-3 | Lift lobby, stair head, main entrance to the plate |
tx_power |
low (−12 to −4 dBm) | Deliberately short range; through-slab detection is the enemy |
| Mounting | ceiling, mid-plate | Away from stair voids and lift shafts |
| Advertising interval | 300-500 ms | Detected within a couple of seconds of arrival |
| Registry | beacon_id → level |
Served with the map metadata, not hard-coded |
| Validation | walk the floor above | The only way to confirm the threshold holds |
The tx_power recommendation runs against the instinct to maximise coverage. A level-tagged beacon
does not need to cover its floor — the barometer does that — it needs to be unambiguous where it is
heard. A short-range beacon at the lift lobby, heard for the ten seconds a user spends waiting, is
worth more than a long-range one heard weakly on three floors.
The registry belongs with the map metadata for the same reason the level elevation table does: it changes when the building changes, and a client that has to be redeployed to learn about a new beacon will be out of date within a quarter.
Common Errors & Fixes
Level flips as a user walks past a stairwell. A beacon on the floor below is being heard
through the void. Lower its tx_power, move it away from the void, and raise floor_rssi. Confirm
by walking the adjacent floors with a scanner rather than by adjusting thresholds blind.
Beacon hints stop arriving after a year. Battery. A coin-cell beacon at a 300 ms interval lasts roughly 18-30 months, and the failure is silent — the level simply falls back to the barometer and drifts. Monitoring the hint rate per building, as wayfinding API observability describes, turns a dead beacon into a maintenance ticket rather than a slow degradation.
Two levels’ beacons heard at once in a lift lobby. Expected, and handled by taking the strongest. What matters is logging it, because a persistent two-level overlap means one of the beacons is mounted too close to the shaft.
The registry and the map disagree about level indices. The beacon registry says “level 4” and the map’s ordinal for that storey is 3, usually because the registry was written against building signage and the map against level mapping. Derive the registry from the map’s level indices rather than entering them by hand.
Integration Point
Beacon hints enter the same fusion the topic page describes, at the top of the precedence order, and their most valuable effect is indirect: every hint re-anchors the barometric detector, resetting its drift budget. A building with beacons at every lift lobby effectively never has a stale barometric anchor, because users pass a lobby whenever they change floor.
The beacon registry is served alongside the level list from the map metadata endpoint that client SDKs already fetch, so no new integration point is introduced on the client.
Frequently Asked Questions
How many beacons does a building need for this?
One to three per level, which is far fewer than positioning would need. These beacons are not doing trilateration — they answer a single yes/no question about which floor a device is on — so they only need to be heard somewhere a user reliably passes. The lift lobby is the obvious first choice because everyone who changes floor goes through one, and it is exactly where the barometric anchor most needs refreshing. A second at the stair head and a third at the main entrance to the plate covers the rest.
Can WiFi access points serve as level hints instead?
Sometimes, and less reliably. If the estate’s access points are managed and their BSSIDs are known per floor, a BSSID heard strongly is level evidence in exactly the same way a beacon identifier is. The problems are that WiFi propagates through slabs considerably better than BLE at typical transmit powers, so through-floor detections are common, and that access points get moved and swapped by network teams without anyone telling the mapping team. Where the estate is willing to maintain the registry, it is free evidence worth using as a tier below dedicated beacons.
What if the building has no beacons and identical floors?
Lean on the barometer and be honest about confidence. With a valid anchor the barometer resolves floors perfectly well; the difficulty is establishing the first anchor, since the radio cannot supply one on aliasing plates. Two practical approaches: anchor at the building entrance, which is unambiguous because there is only one ground floor, and treat a session that has not passed the entrance as level-unknown rather than guessing. The second is to use lift and stair ride detection to count transitions from that entrance anchor, which is weaker but free.
Related
- Indoor Floor-Level Detection — the fusion this evidence enters at the top of.
- Barometric Floor Detection in Python — the cheap source these hints re-anchor.
- Optimal BLE Beacon Placement for Floor Coverage — placement for positioning, which has the opposite range requirement.
This page is a companion to Indoor Floor-Level Detection, part of the Indoor Positioning: Beacon & WiFi Fingerprinting section.