A browser-based tool for planning Bluetooth Low Energy (BLE) sensor deployments in real buildings. Load a 3D floor plan (GLB/GLTF), automatically detect rooms, and simulate RF coverage with physically-grounded propagation math. A greedy optimizer places the minimum number of sensors needed to hit a target coverage percentage.
| Feature | Description |
|---|---|
| GLB/GLTF floor plan import | Load any 3D building model; the tool rasterizes a top-down slice and detects rooms automatically |
| Manual floor plan | Rectangle fallback when no 3D model is available |
| Room detection | Binary morphology + run-length scan + BFS flood fill extracts individual rooms from the rendered floor plan |
| RF heatmap | Log-distance path loss with per-wall attenuation; rendered at canvas resolution |
| Confidence overlay | Probability that the actual received signal exceeds the target threshold, given shadow fading |
| Sensor placement modes | Place, select, drag, and delete sensors interactively on the canvas |
| Wall drawing | Draw axis-aligned walls with material-specific attenuation; erase walls by clicking them |
| Greedy optimizer | Automatically places the fewest sensors needed to reach a user-defined coverage target |
| Multi-floor stub | Data model supports multiple floors (UI expansion in progress) |
| Export | Download sensor layout as JSON, CSV, or a rendered PNG |
No build step. Open index.html directly in a modern browser (Chrome/Edge recommended for WebGL support).
open index.html
Or serve locally to avoid CORS issues with the Three.js CDN imports:
python3 -m http.server 8080
# → http://localhost:8080RSSI at a point p from a single sensor s is:
RSSI(p, s) = A − 10·n·log₁₀(d) − WAF
| Symbol | Parameter | Typical values |
|---|---|---|
A |
Reference power at 1 m (dBm) | −40 to −80 dBm |
n |
Path-loss exponent | 2 (free space), 2.5–3.5 (office), 4–6 (cluttered) |
d |
3D Euclidean distance in metres (clamped ≥ 0.3 m) | — |
WAF |
Accumulated wall-attenuation factor along the ray | see table below |
When multiple sensors are deployed, each point takes the best (highest) RSSI across all sensors:
RSSI_best(p) = max over all sensors s { A − 10·n·log₁₀(d_s) − WAF_s }
Each wall segment has a material. Every wall segment whose 2D line crosses the straight ray between a sensor and an evaluation point contributes its full attenuation:
| Material | Attenuation |
|---|---|
| Glass | 2 dB |
| Drywall | 3 dB |
| Brick | 8 dB |
| Concrete | 15 dB |
| Metal | 25 dB |
Ray–wall intersection uses the parametric cross-product test (Cramér's rule). An axis-aligned bounding-box pre-filter skips walls that cannot possibly intersect, keeping per-pixel cost sub-linear in wall count.
Real-world BLE signals fluctuate around the path-loss mean due to multipath, body blocking, and environmental changes. The instantaneous RSSI is modelled as:
RSSI_actual ~ N(RSSI_mean, σ²)
The probability that the actual signal exceeds a threshold T is:
P(coverage | p) = Φ( (RSSI_mean − T) / σ )
where Φ is the standard normal CDF, approximated with the Abramowitz & Stegun polynomial (error < 7.5 × 10⁻⁸):
Φ(z) = 1 − φ(z)·t·(a₁ + t(a₂ + t(a₃ + t(a₄ + t·a₅))))
t = 1 / (1 + 0.2316419·|z|)
φ(z) = (1/√2π)·e^(−z²/2)
a = [0.3193815, −0.3565638, 1.7814779, −1.8212560, 1.3302744]
| Parameter | Description | Default |
|---|---|---|
T |
Target RSSI threshold | −85 dBm |
σ |
Shadow-fading standard deviation | 7 dB |
The mean coverage metric is the area-weighted mean of P(coverage) across all evaluation pixels.
A "Human Traffic" slider (0–100 %) scales additional body-blocking attenuation on top of the base path-loss model, simulating the effect of people moving through the space.
When a GLB file is loaded, the following pipeline runs entirely in the browser:
Three.js renders the 3D scene top-down using an OrthographicCamera at resolution 1024 × H (H derived from the model's XZ aspect ratio). Clipping planes restrict rendering to the 15%–85% height band of the model, isolating the structural floor slice and excluding furniture or ceiling geometry.
The alpha channel of the rendered canvas is thresholded at α > 30, producing a binary occupancy map where 1 = wall/structure pixel.
Two passes of 4-connectivity dilation close sub-pixel gaps in walls:
out[x,y] = src[x,y] OR src[x±1,y] OR src[x,y±1]
Applied twice to bridge wall segments that were slightly separated in the render.
Horizontal and vertical runs of consecutive 1 pixels are collected. Runs shorter than max(6, 1.5% of canvas min-dimension) are discarded (short noise artefacts).
Parallel runs within 6 px of each other (same row/column) are grouped. Within each group, segments separated by ≤ 10 px are joined into a single segment.
Wall endpoints that do not meet a junction shoot a perpendicular ray (up to 35% of canvas). If the ray hits another wall, a virtual closing segment is inserted. These appear as dashed magenta lines in the UI.
A BFS flood fill labels every non-wall connected region. Each region accumulates bounding box, area (px²), and an touchesEdge flag.
- The largest edge-touching region is identified as exterior space and discarded.
- All other edge-touching regions are retained (they are rooms against outer walls).
- Regions with area <
max(50, 0.05% × canvas area)are discarded as noise.
The output is a Uint16Array room mask (room ID per pixel) and an array of room descriptors { id, minX, maxX, minY, maxY, area }.
The optimizer finds the minimum-sensor placement that achieves a target mean coverage probability.
- Candidate grid — every 2 px walkable pixel (inside the room mask) is a candidate sensor location.
- Evaluation grid — every 4 px walkable pixel is an evaluation point.
- Locked-sensor seed — pre-compute
bestRSSI[i]for each evaluation point from any manually-placed sensors. - Greedy loop — repeat until coverage ≥ target or sensor budget exhausted:
- For each candidate, count how many currently-uncovered evaluation points it would bring above the coverage threshold (
P(coverage) ≥ 0.5). - Place the candidate with the highest marginal gain.
- Update
bestRSSI[i]with the newly placed sensor's contribution. - Remove the chosen candidate from the pool.
- For each candidate, count how many currently-uncovered evaluation points it would bring above the coverage threshold (
The coverage threshold inside the optimizer uses P(coverage | rssi) ≥ 0.5, i.e., the median signal-quality criterion. The outer meanCoverage panel metric uses the full continuous probability integral.
Each greedy iteration is O(C × E) where C = candidate count and E = evaluation-point count. For a 1024 × 1024 canvas with a dense floor plan, C ≈ 250k and E ≈ 65k. In practice the loop terminates quickly (< 20 sensors for typical rooms) so wall-time is under a second for most buildings.
Walls are stored in metre coordinates. Three source types coexist:
| Source | Color in UI | Description |
|---|---|---|
detected |
Cyan, solid | Extracted automatically from the GLB rasterization |
virtual |
Magenta, dashed | Inferred closing segments from the virtual-wall pass |
manual |
Yellow, solid | Drawn interactively by the user |
Ray–segment intersection uses the parametric form with a 1 × 10⁻⁶ endpoint exclusion tolerance so sensor-at-wall-endpoint cases do not double-count the wall.
index.html Entry point, importmap, UI layout
js/
rf-engine.js Log-distance RSSI, shadow-fading CDF, field rasterizer
wall-store.js Wall segment store, WAF lookup, ray–segment intersection
room-detector.js GLB loader, morphology, flood fill, room extraction
optimizer.js Greedy coverage-maximization sensor placement
renderer.js Canvas compositing (GLB layer + heatmap + walls)
ui.js Event wiring, canvas interaction, mode state machine
tests/
test-rf-engine.mjs
test-wall-store.mjs
test-room-detector.mjs
test-optimizer.mjs
All pure functions in rf-engine.js, wall-store.js, room-detector.js, and optimizer.js are importable in Node.js for unit testing. Browser-only code (WebGL renderer, Three.js GLTFLoader) is isolated behind async factory functions.
node tests/test-rf-engine.mjs
node tests/test-wall-store.mjs
node tests/test-room-detector.mjs
node tests/test-optimizer.mjs| Format | Contents |
|---|---|
| JSON | { sensors, walls, state } — full simulation state |
| CSV | One row per sensor: id, x_m, y_m, floor, manual |
| PNG | Current canvas frame (heatmap + floor plan + sensors) at screen resolution |
- Three.js r160 — GLB loading and WebGL rasterization (CDN, no install)
- Vanilla JS ES modules — no bundler required