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.

This page is a companion to BLE Beacon Positioning & Trilateration, part of the Indoor Positioning & Fingerprinting reference.