Optimal BLE Beacon Placement for Floor Coverage
Where the beacons go decides whether trilateration can work at all, long before any RSSI is smoothed or any solve is run; this page covers planning placement so the BLE beacon positioning solver is well-conditioned everywhere a person can stand.
What “Good Coverage” Means for Trilateration
Coverage for positioning is not the same as coverage for connectivity. A device only needs one beacon in range to detect proximity, but trilateration needs at least three, and it needs them in good geometry. Three placement objectives follow directly from the maths of the least-squares solve.
First, three-plus visibility everywhere routable. At every point a person can walk, the receiver must hear at least three beacons above the ranging floor. Anywhere that drops to two is a coverage hole where positioning collapses to ambiguity.
Second, non-collinear geometry. Three beacons in a straight line — the default when they are screwed to one corridor wall — give a degenerate solve: the position is well-constrained along the line and almost unconstrained across it, so the fix smears sideways. Good placement spreads visible beacons so they subtend a wide angle at any receiver, which is what keeps geometric dilution of precision (GDOP) low.
Third, spacing matched to range and overlap. Beacon spacing follows from the usable range at your tx_power and path-loss exponent, discounted for walls. You want enough overlap that the three-visibility rule holds in the gaps, but not so dense that advertising channels congest and battery-replacement labour balloons. Mounting height matters too: 2.5-3 m on a wall or ceiling clears most body-shadowing, which is the largest dynamic attenuation a walking crowd introduces.
Minimal Working Example
The script below samples a grid of points across a shapely floor polygon, counts how many beacons fall within range at each point, and flags any interior point that sees fewer than three as a coverage hole. It also runs a rough geometry check — the angular spread of the visible beacons — so near-collinear configurations are flagged even where the count is satisfied.
import logging
import numpy as np
from shapely.geometry import Point, Polygon
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def audit_coverage(
floor: Polygon,
beacons: np.ndarray, # shape (m, 2), beacon xy in the floor's local frame
range_m: float = 12.0,
min_visible: int = 3,
grid_m: float = 1.5,
) -> list[tuple[float, float]]:
"""Return interior sample points that fail the >=3 well-spread-beacon rule."""
if beacons.shape[0] < min_visible:
raise ValueError(f"only {beacons.shape[0]} beacons; cannot ever satisfy min_visible")
minx, miny, maxx, maxy = floor.bounds
holes: list[tuple[float, float]] = []
xs = np.arange(minx, maxx, grid_m)
ys = np.arange(miny, maxy, grid_m)
for x in xs:
for y in ys:
p = Point(x, y)
if not floor.contains(p):
continue
d = np.linalg.norm(beacons - np.array([x, y]), axis=1)
visible = beacons[d <= range_m]
if len(visible) < min_visible or _poorly_spread(visible, x, y):
holes.append((float(x), float(y)))
logger.info("%d/%d interior samples fail coverage", len(holes), len(xs) * len(ys))
return holes
def _poorly_spread(visible: np.ndarray, x: float, y: float) -> bool:
"""Flag near-collinear geometry: max gap between sorted bearings > 180 deg."""
if len(visible) < 3:
return True
bearings = np.sort(np.arctan2(visible[:, 1] - y, visible[:, 0] - x))
gaps = np.diff(np.concatenate([bearings, [bearings[0] + 2 * np.pi]]))
return bool(np.max(gaps) > np.pi) # all beacons within a half-plane => bad GDOP
floor = Polygon([(0, 0), (30, 0), (30, 20), (0, 20)])
beacons = np.array([[3, 3], [27, 4], [15, 18], [6, 16], [24, 15]], dtype=float)
holes = audit_coverage(floor, beacons)
If _poorly_spread flags a point, all its visible beacons sit within a half-plane — the receiver has no beacon “behind” it — which is exactly the collinear-style geometry that makes a fix unstable even with a strong signal.
Placement Parameter Reference
| Parameter | Symbol | Typical value | Notes |
|---|---|---|---|
| Target visible-beacon count | min_visible |
≥ 3 (aim for 4) | Three is the floor; four adds redundancy and a usable residual |
| Usable range | range_m |
8 – 15 m | Effective, wall-discounted range at your tx_power and n |
| Nominal spacing | — | 8 – 12 m | Grid or staggered; tighten in partitioned areas |
| Mounting height | — | 2.5 – 3.0 m | Wall or ceiling; clears body-shadowing from crowds |
| Grid resolution | grid_m |
1.0 – 2.0 m | Audit sample step; finer catches smaller holes, costs runtime |
| Bearing-spread limit | — | max gap ≤ 180° | Geometry gate; a larger gap means all beacons on one side |
Common Errors & Fixes
Coverage looks complete but fixes are unstable in corridors. Beacons were mounted only along the perimeter walls, so in any corridor the three visible beacons are nearly collinear. The count passes, the geometry fails. Stagger beacons to alternating sides and add occasional interior or opposite-wall anchors so every routable point has a beacon subtending a wide angle:
bad = np.array([[0, 2], [10, 2], [20, 2]]) # one wall -> collinear
good = np.array([[0, 2], [10, 18], [20, 2], [30, 18]]) # staggered -> well spread
Positioning dies exactly where two zones meet. Beacons were clustered densely inside rooms and left thin at thresholds and junctions — the very places people navigate through. Clustering wastes redundancy where it is not needed and starves the transitions. Space beacons against the coverage audit, not against room boundaries, so the three-visibility rule holds continuously across doorways.
Real-world range is far shorter than the plan assumed. The audit used a free-space range_m while walls, glass, and metal shelving attenuate hard. A beacon that reaches 15 m in an open hall may reach 6 m through two partitions. Discount range_m for the wall count typical of the space — or subtract range where the segment from beacon to sample crosses a wall polygon — before trusting the coverage map. Ranging quality then depends on calibrating that attenuation into the path-loss model.
Integration Point
Placement is upstream of everything else in the positioning chain: the geometry planned here sets the ceiling on what trilateration can achieve, because no solver recovers a coordinate its anchors cannot constrain. The coverage audit consumes the same floor polygon that comes out of automated floor-plan parsing and vectorization, so placement planning and map production share one geometry source and one coordinate frame. Once beacons are mounted and surveyed, their positions and calibrated tx_power feed the BLE beacon positioning solve, and the per-beacon range assumptions here are only as good as the ranging model tuned in estimating distance from BLE RSSI with a path-loss model.
Frequently Asked Questions
How far apart should BLE beacons be for positioning?
Space them so every routable point hears at least three well-spread beacons, which usually lands around 8-12 m in open areas and tighter through partitions. The exact number falls out of your usable range — the wall-discounted distance at which RSSI is still above the ranging floor — not a fixed rule of thumb. Plan the spacing, then verify it with a coverage audit over the actual floor polygon rather than trusting the nominal figure.
Why does perimeter-only placement give poor fixes?
Because in interior corridors the only visible beacons sit along one or two outer walls, leaving them nearly collinear from the receiver’s viewpoint. Trilateration then constrains position well along that line and poorly across it, so the fix smears sideways however clean the signal is. The bearing-spread check flags this: when all visible beacons fall within a half-plane, no anchor sits behind the receiver and the geometry is degenerate. Stagger beacons and add opposite-side anchors to fix it.
Do walls change how many beacons I need?
Yes, substantially. Walls, glass, and metal attenuate BLE, shrinking usable range and raising the path-loss exponent, so a partitioned floor needs more beacons than an open one of the same area. Discount range_m for the walls a signal must cross — or explicitly subtract range where the beacon-to-point segment intersects a wall polygon — before trusting a coverage map, otherwise the plan looks complete while real fixes fail near partitions.
Related
- BLE Beacon Positioning & Trilateration — the solve whose accuracy this placement determines.
- Estimating Distance from BLE RSSI with a Path-Loss Model — calibrating the range each placed beacon actually reaches.
- Automated Floor-Plan Parsing & Vectorization — the source of the floor polygon the coverage audit runs over.
This page is a companion to BLE Beacon Positioning & Trilateration, part of the Indoor Positioning & Fingerprinting reference.