Building a BLE tag asset tracking system is not about choosing the right tag. It is about the infrastructure that receives, filters, transports, and stores the data from hundreds or thousands of tags scattered across a facility. The tag itself is a dumb broadcaster; the intelligence lives in the gateway network, the edge processor, and the cloud backend. This article breaks down the full system architecture from radio reception to dashboard, with hardware selection tables, filtering algorithms, data pipeline schemas, and deployment cost models drawn from production deployments in hospitals, warehouses, and manufacturing floors.

1. System Architecture Overview

A production BLE asset tracking system has four tiers, each with distinct latency, throughput, and reliability requirements:

Tier Components Latency Budget Data Volume Failure Impact
T1: Tag Layer BLE tags (hundreds to thousands) N/A (broadcast only) ~30 bytes/tag/event Single asset goes dark
T2: Gateway Layer BLE scanners (5-50 units) 100-500 ms scan cycle ~50 KB/s peak per gateway Zone goes dark (10-50 tags)
T3: Edge Layer Local server or edge appliance 1-5 s processing ~200 KB/s aggregated Tracking delayed, not lost
T4: Cloud Layer Backend, database, dashboard 1-10 s end-to-end ~500 KB/s sustained Historical data gap

The most common architecture mistake is treating the gateway as a simple relay that forwards raw BLE packets to the cloud. At 500 tags with 1-second advertising intervals and 10 gateways, that is 5,000 packets per second hitting your cloud ingest endpoint, each containing duplicate RSSI readings from overlapping coverage zones. Edge filtering is not an optimization; it is a requirement.

2. Gateway Hardware Selection

The gateway is the most critical hardware decision in the system. It must simultaneously scan BLE advertising channels, run filtering logic, maintain network connectivity, and survive power disruptions. Three hardware families dominate production deployments:

Platform BLE Chip CPU RAM Power Cost (BOM) Best For
ESP32-Based ESP32 (dual-mode) 240 MHz Xtensa x2 520 KB SRAM 2.5W (WiFi+BLE) $3-5 Small sites, WiFi backhaul
Raspberry Pi + USB Dongle nRF52840 dongle 1.4 GHz ARM Cortex-A72 x4 1-4 GB 5-7W $45-60 R&D, prototyping, low-volume
Purpose-Built Gateway nRF52832/52840 + ESP32 ESP32 240MHz + nRF52 520KB + 256KB 2-3W $15-25 Production deployments
Industrial Gateway (Linux) CC2640R2 + WiFi/LTE ARM Cortex-A7 1GHz 512 MB 5-15W $200-400 Harsh environments, cellular

2.1 ESP32 as a Gateway: Capabilities and Limits

The ESP32 is the most common gateway platform for cost-sensitive deployments because it has built-in WiFi and BLE. However, the ESP32 BLE controller shares the 2.4 GHz radio between WiFi and BLE, meaning it cannot scan BLE while transmitting WiFi. This time-division multiplexing introduces scan gaps:

WiFi Activity BLE Scan Gap Packet Loss (1s interval) Packet Loss (500ms)
Idle (beacon only) 0 ms 0% 0%
MQTT publish (every 5s) ~15 ms/gap 0.3% 0.6%
WiFi streaming (continuous) ~50 ms/gap 1.0% 2.0%
OTA update ~200 ms/gap 5-8% 10-15%

For a hospital tracking 200 tags with 1-second intervals, a 1% loss rate means 2 tags missing per scan cycle. This is tolerable for presence detection but problematic for real-time RTLS. The solution is either a dual-radio gateway (ESP32 for WiFi + nRF52 for BLE) or reducing WiFi activity to periodic batch uploads.

2.2 Purpose-Built Gateway Architecture

Production gateways typically pair an nRF52 (dedicated BLE scanner) with an ESP32 (WiFi/CPU). The nRF52 runs continuous active scan and forwards packets over UART/SPI to the ESP32, which handles filtering, buffering, and cloud communication. This dual-chip design eliminates radio contention entirely.

# Gateway firmware data flow (nRF52 + ESP32)

# nRF52 side: continuous BLE scanner
def ble_scan_callback(adv_report):
    packet = {
        "mac": adv_report.peer_addr,
        "rssi": adv_report.rssi,
        "adv_type": adv_report.type,
        "data": adv_report.data,
        "ts": get_timestamp_us(),
    }
    uart_send(json.dumps(packet))  # ~120 bytes/packet

# ESP32 side: receive, filter, batch upload
rx_buffer = []
def uart_rx_callback(data):
    pkt = json.loads(data)
    if should_forward(pkt):  # edge filtering
        rx_buffer.append(pkt)
    if len(rx_buffer) >= BATCH_SIZE or time_since_last_upload > UPLOAD_INTERVAL:
        upload_batch(rx_buffer)
        rx_buffer.clear()

2.3 Gateway Placement and Coverage

Gateway density is the single largest cost driver. Too few gateways leave coverage holes; too many create redundant data and unnecessary filtering load. The optimal density depends on the required location accuracy and the physical environment:

Accuracy Tier Gateways per 100 m2 Typical Spacing Cost per m2 Use Case
Presence (room-level) 0.5-1.0 10-15 m $0.15-0.30 Asset presence per zone
Coarse positioning (3-5 m) 2-4 5-8 m $0.60-1.20 Warehouse aisle tracking
Fine positioning (1-2 m) 6-10 3-5 m $1.80-3.00 Surgical instrument tracking
Sub-meter (0.5 m) 15-25 2-3 m $4.50-7.50 High-value asset audit

These numbers assume open-plan environments. Walls, metal shelving, and equipment racks reduce effective range by 30-50%. A warehouse with 8 m steel racks may need 2x the gateway density of an open hospital corridor for the same accuracy.

3. BLE Scanning Strategy

The gateway must decide how to scan BLE advertising channels. The BLE spec defines three advertising channels (37, 38, 39) that tags hop across. A scanner can either listen on one channel or cycle through all three. The scan window and interval parameters control how much time the radio spends listening versus sleeping.

3.1 Active vs. Passive Scan

Parameter Passive Scan Active Scan
Sends SCAN_REQ No Yes
Receives SCAN_RSP No Yes (up to 31 bytes extra)
Power consumption Lower (Rx only) Higher (Tx + Rx)
Tag power impact None +20-40% if tag responds to all scans
Data available 31 bytes (adv packet only) 62 bytes (adv + scan response)
Latency to first detection ~1 adv interval ~1 adv interval

For asset tracking with custom payloads in the manufacturer-specific data, passive scan is sufficient. Active scan is necessary only when the tag splits data between the advertising packet and scan response (e.g., iBeacon UUID in adv + sensor data in scan response).

3.2 Scan Window and Interval

The scan interval defines how often the scanner starts a scan cycle. The scan window defines how long each cycle lasts. If window equals interval, the scanner runs continuously (100% duty cycle). Production gateways typically use 80-100% duty cycle because the power budget is not battery-constrained.

Scan Interval Scan Window Duty Cycle Capture Rate (1s tag interval) Capture Rate (500ms)
Continuous Continuous 100% ~99.5% ~99.0%
1000 ms 1000 ms 100% ~99.5% ~99.0%
1000 ms 500 ms 50% ~50-65% ~50-65%
2000 ms 1000 ms 50% ~50-65% ~50-65%
5000 ms 1000 ms 20% ~20-30% ~20-30%

The capture rate at 50% duty cycle is not exactly 50% because of the probabilistic overlap between the scan window and the tag’s advertising events. With three advertising channels and random phase, the actual capture rate follows a birthday-problem distribution. For production deployments, continuous scan (100% duty cycle) is strongly recommended unless the gateway is battery-powered.

3.3 Duplicate Filtering at the Radio Level

The BLE controller reports each advertising packet it receives, including duplicates from the same tag within the same scan window. A tag advertising at 100 ms generates 10 packets per second per gateway. With 500 tags and 10 gateways, that is 50,000 packets per second across the system. The controller can optionally suppress duplicates:

Filter Mode Duplicates Reported Packets/s (500 tags, 1s, 10 GW) Gateway CPU Load
No filtering All ~15,000 High
Controller-level dedup (per scan cycle) 1 per tag per cycle ~500 Low
Application-level dedup (sliding window) 1 per tag per window ~500 Low-Medium

The nRF52 BLE API supports duplicate filtering via sd_ble_gap_scan_start() with scan_params.filter_policy set to BLE_GAP_SCAN_FP_DUPLICATE. This reduces the interrupt load dramatically but means the application only gets one RSSI sample per tag per scan cycle (typically 1 second).

4. Edge Filtering: Reducing Data Before the Cloud

Even with radio-level dedup, overlapping gateway coverage means the same tag is reported by multiple gateways. Edge filtering is the process of selecting the best report and discarding the rest. This typically happens on the edge server (T3) or a designated gateway acting as an aggregator.

4.1 Multi-Gateway Deduplication

When 3 gateways report the same tag within a 1-second window, only one record should reach the cloud. The selection algorithm must consider RSSI, timestamp, and gateway reliability:

# Edge deduplication algorithm
DEDUP_WINDOW_MS = 2000  # 2-second sliding window

def deduplicate(raw_reports):
    # Select best report per tag within dedup window
    by_tag = group_by_mac(raw_reports)
    result = []
    for mac, reports in by_tag.items():
        if len(reports) == 1:
            result.append(reports[0])
            continue
        # Score each report: weighted RSSI + freshness + reliability
        best = max(reports, key=lambda r: score_report(r))
        result.append(best)
    return result

def score_report(r):
    # RSSI: higher is better (closer), normalize -100 to -30
    rssi_score = (r.rssi + 100) / 70  # 0.0 to 1.0
    # Freshness: newer is better, penalize >5s old
    age_s = (now() - r.ts) / 1000
    fresh_score = max(0, 1 - age_s / 5)
    # Reliability: weighted by gateway uptime percentage
    gw_reliability = gateway_stats[r.gw_id].uptime_pct
    return rssi_score * 0.6 + fresh_score * 0.2 + gw_reliability * 0.2

4.2 RSSI Smoothing and Outlier Rejection

Raw RSSI from a single packet is noisy. Multipath reflections, body absorption, and interference cause +/-10 dB swings. For location estimation, the edge should apply smoothing before forwarding:

Filter Window Size Latency Added RSSI Variance Reduction Implementation Complexity
Raw (no filter) 1 sample 0 s 0% None
Moving average 5 samples 2-5 s ~55% Low (FIFO buffer)
Exponential smoothing Infinite (weighted) 1-3 s ~50% Low (1 multiply)
Median filter 3-7 samples 1-3 s ~65% Medium (sort)
Kalman filter Adaptive 0.5-2 s ~70% High (matrix math)

Exponential smoothing (alpha = 0.3) is the production default for most deployments because it requires only one previous value and one multiply:

# Exponential RSSI smoothing (alpha = 0.3)
# New value = alpha * raw + (1 - alpha) * previous
ALPHA = 0.3
smoothed_rssi = {}

def smooth_rssi(mac, raw_rssi):
    if mac not in smoothed_rssi:
        smoothed_rssi[mac] = raw_rssi
    else:
        smoothed_rssi[mac] = ALPHA * raw_rssi + (1 - ALPHA) * smoothed_rssi[mac]
    return round(smoothed_rssi[mac], 1)

4.3 Event-Based Forwarding

Instead of forwarding every scan cycle, the edge can forward only when meaningful events occur. This reduces cloud traffic by 80-95% while preserving tracking accuracy:

Forwarding Strategy Packets/Tag/Hour Cloud BW (500 tags) Use Case
Every scan (1s) 3,600 ~14 MB/h RTLS with sub-second updates
Every 5s (batch) 720 ~2.8 MB/h Standard tracking
Zone change only ~5-20 ~40 KB/h Room-level presence
Threshold breach + heartbeat ~2-10 ~20 KB/h Temperature/humidity alerts

5. Data Pipeline Design

The data pipeline transports filtered tag data from the edge to the cloud backend. Three transport protocols are commonly used, each with different reliability, latency, and complexity tradeoffs:

5.1 Transport Protocol Comparison

Protocol Overhead/Packet Reliability Latency Power (Gateway) Best For
HTTP POST (JSON) ~400 bytes (headers) TCP (guaranteed) 100-500 ms High (connection setup) Small deployments, simple backends
MQTT (QoS 1) ~20 bytes (header) TCP + ACK 50-200 ms Low (persistent connection) Production deployments
UDP (custom) ~8 bytes (header) None (best-effort) 10-50 ms Lowest Real-time RTLS, local LAN
LoRaWAN (gateway-to-cloud) ~13 bytes (header) Confirmed downlink 1-10 s N/A Remote sites without WiFi

MQTT is the industry default for BLE asset tracking because it maintains a persistent TCP connection (eliminating HTTP connection setup overhead), supports QoS levels for guaranteed delivery, and has a lightweight publish/subscribe model that maps naturally to multi-zone tracking (each zone is a topic).

5.2 MQTT Topic Structure

# MQTT topic hierarchy for asset tracking
# Format: site/zone/gateway/tag/event

site-001/zone-A/gw-01/+/scan      # All scans from gw-01
site-001/zone-A/+/tag-005/scan    # All scans of tag-005 in zone-A
site-001/+/+/tag-005/zone_change  # Zone change events for tag-005
site-001/+/+/+/battery_low         # Battery alerts across site
site-001/+/+/+/temperature_alert   # Temperature threshold breaches

# Payload format (JSON, ~80 bytes)
{
    "mac": "AA:BB:CC:DD:EE:01",
    "rssi": -67,
    "gw": "gw-01",
    "ts": 1722422400000,
    "bat": 85,
    "temp": 23.5,
    "seq": 4823
}

5.3 Data Schema and Storage

The cloud backend typically uses a time-series database for raw scan data and a relational database for asset metadata:

Store Technology Data Retention Query Pattern
Time-series (hot) InfluxDB / TimescaleDB Raw scan events 7-30 days Range queries by tag + time
Time-series (cold) S3 / Parquet Aggregated events 1-5 years Batch analytics, compliance
Relational PostgreSQL / MySQL Asset metadata, zones, users Permanent CRUD, joins
Cache Redis Last-known location TTL 5 min Sub-ms lookup

5.4 Storage Sizing

# Storage sizing calculation for 500 tags, 5s forwarding interval
tags = 500
events_per_tag_per_hour = 3600 / 5  # = 720
events_per_hour = tags * events_per_tag_per_hour  # = 360,000
events_per_day = events_per_hour * 24  # = 8,640,000

bytes_per_event = 80  # JSON payload
bytes_per_day = events_per_day * bytes_per_event  # = 691 MB/day
bytes_per_month = bytes_per_day * 30  # = ~20.7 GB/month (raw)
bytes_per_year = bytes_per_day * 365  # = ~252 GB/year (raw)

# With compression (Parquet, ~5:1 ratio):
compressed_per_year = bytes_per_year / 5  # = ~50 GB/year

# Database overhead (indexes, WAL, etc.): +40%
total_storage_year = compressed_per_year * 1.4  # = ~70 GB/year

6. Location Estimation at the Edge

For systems that need more than room-level presence, the edge performs location estimation using RSSI from multiple gateways. The three most common algorithms have different accuracy, complexity, and gateway requirements:

Algorithm Min Gateways Accuracy Edge CPU Calibration Best For
Proximity (strongest RSSI) 1 Room-level (5-10 m) Minimal None Simple presence
Weighted centroid 3+ 3-5 m Low Gateway positions only Open areas
Trilateration (log-distance) 3+ 2-4 m Medium Path-loss exponent per zone Warehouses, corridors
Fingerprinting (kNN) 4+ 1-3 m Medium-High Full RF survey (hours) Complex indoor environments
BLE Angle of Arrival (AoA) 1 (with antenna array) 0.5-1 m High Antenna phase calibration High-value asset tracking

6.1 Weighted Centroid Algorithm

The weighted centroid is the production default because it requires no calibration beyond gateway positions and provides 3-5 m accuracy in open areas:

# Weighted centroid location estimation
# Weight = 10^(RSSI/10), giving closer gateways exponentially more weight

def estimate_position(reports, gateway_positions):
    # reports: [{"gw_id": "gw-01", "rssi": -65}, ...]
    # gateway_positions: {"gw-01": (x, y), ...}
    total_weight = 0
    weighted_x = 0
    weighted_y = 0

    for r in reports:
        gw_id = r["gw_id"]
        if gw_id not in gateway_positions:
            continue
        # Convert RSSI to weight (closer = higher weight)
        weight = 10 ** (r["rssi"] / 10)
        gx, gy = gateway_positions[gw_id]
        weighted_x += weight * gx
        weighted_y += weight * gy
        total_weight += weight

    if total_weight == 0:
        return None

    return (weighted_x / total_weight, weighted_y / total_weight)

# Example: tag detected by 3 gateways
reports = [
    {"gw_id": "gw-01", "rssi": -55},  # Close (weight = 316)
    {"gw_id": "gw-02", "rssi": -75},  # Far (weight = 0.032)
    {"gw_id": "gw-03", "rssi": -65},  # Medium (weight = 3.16)
]
positions = {
    "gw-01": (10, 20),
    "gw-02": (30, 40),
    "gw-03": (50, 10),
}
# Result: ~(10.9, 20.1) - heavily weighted toward gw-01

7. System Reliability and Failover

7.1 Gateway Health Monitoring

Each gateway must report health metrics so the edge can detect failures and trigger alerts:

Metric Normal Range Alert Threshold Check Interval
Packets/minute 50-500 (depends on tag count) < 10 or > 2000 60 s
CPU temperature 40-65 C > 75 C 300 s
WiFi RSSI -30 to -65 dBm < -75 dBm 60 s
Uptime > 99.5% < 99% Daily
Last heartbeat < 30 s ago > 120 s ago 30 s

7.2 Edge-to-Cloud Connectivity Loss

When the edge loses cloud connectivity (WiFi outage, ISP failure), it must buffer data locally and replay when connectivity returns. The buffer sizing depends on the maximum acceptable downtime:

Max Downtime Buffer Size (500 tags, 5s interval) Storage Medium RAM vs Disk
15 min ~4.3 MB RAM In-memory queue
1 hour ~17 MB RAM or SD card In-memory or file
4 hours ~69 MB SD card / eMMC SQLite + WAL
24 hours ~415 MB SSD / eMMC SQLite + compression
# Edge buffering with SQLite for offline-first operation
import sqlite3, json, time

class EdgeBuffer:
    def __init__(self, db_path="/data/edge_buffer.db"):
        self.conn = sqlite3.connect(db_path)
        self.conn.execute(
            "CREATE TABLE IF NOT EXISTS events ("
            "id INTEGER PRIMARY KEY AUTOINCREMENT, "
            "ts INTEGER NOT NULL, "
            "mac TEXT NOT NULL, "
            "data TEXT NOT NULL, "
            "uploaded INTEGER DEFAULT 0)"
        )
        self.conn.execute("CREATE INDEX IF NOT EXISTS idx_uploaded ON events(uploaded)")

    def store(self, events):
        for e in events:
            self.conn.execute(
                "INSERT INTO events (ts, mac, data) VALUES (?, ?, ?)",
                (e["ts"], e["mac"], json.dumps(e))
            )
        self.conn.commit()

    def upload_pending(self, mqtt_client):
        rows = self.conn.execute(
            "SELECT id, data FROM events WHERE uploaded=0 ORDER BY id LIMIT 1000"
        ).fetchall()
        for row_id, data_json in rows:
            mqtt_client.publish("site-001/events", data_json, qos=1)
            self.conn.execute("UPDATE events SET uploaded=1 WHERE id=?", (row_id,))
        self.conn.commit()
        # Periodic cleanup: delete uploaded records older than 7 days
        self.conn.execute(
            "DELETE FROM events WHERE uploaded=1 AND ts < ?", (time.time() - 604800,)
        )

8. Deployment Cost Model

A complete BLE asset tracking system for a 10,000 m2 facility with 500 tracked assets:

Component Qty Unit Cost Subtotal % of Total
BLE tags (CR2032, 3-year life) 500 $8 $4,000 18%
Purpose-built gateways 20 $120 $2,400 11%
Edge server (industrial mini-PC) 1 $800 $800 4%
Network switches + cabling 1 $1,500 $1,500 7%
Cloud infrastructure (year 1) 1 $3,600 $3,600 16%
Software licenses (year 1) 1 $5,000 $5,000 23%
Installation + commissioning 1 $3,000 $3,000 14%
Spare tags + gateways (10%) - - $640 3%
RF survey + calibration 1 $1,200 $1,200 5%
Year 1 Total $22,140 100%
Recurring Cost Annual Notes
Cloud hosting $3,600 VPS + DB + storage
Software maintenance $1,500 30% of license
Tag replacement (battery EOL) $1,300 ~170 tags/year at 3-year life
Gateway replacement (failure) $240 ~2 gateways/year at 10% failure
Support contract $2,000 SLA 4-hour response
Annual Recurring $8,640

Cost per tracked asset: $22,140 / 500 = $44.28 (Year 1), $8,640 / 500 = $17.28/year (recurring). This compares favorably to RFID-based systems ($60-120/asset Year 1) and Wi-Fi-based RTLS ($80-150/asset Year 1).

9. Performance Benchmarks from Production

Metric Hospital (200 tags, 8 GW) Warehouse (500 tags, 20 GW) Factory (1000 tags, 15 GW)
Scan capture rate 98.2% 96.5% 94.1%
Location accuracy (median) 3.2 m 4.8 m 5.5 m
End-to-end latency (p95) 2.8 s 4.1 s 5.3 s
Gateway uptime 99.7% 99.2% 98.8%
Cloud data/month 4.2 GB 18.7 GB 24.3 GB
False zone-change rate 0.8% 2.1% 3.5%
Tag battery life (median) 31 months 28 months 26 months

The factory deployment has the worst performance across all metrics because of RF interference from welding equipment, motor drives, and metal machinery. The false zone-change rate of 3.5% means 35 out of every 1,000 location updates incorrectly place the asset in an adjacent zone. This is mitigated by a 30-second dwell timer: a zone change is only reported if the tag remains in the new zone for at least 30 seconds.

10. Scaling Considerations

Scale Tags Gateways Edge Architecture Cloud Architecture Key Challenge
Small 1-100 1-5 Single gateway (embedded) Single VPS Cost efficiency
Medium 100-1,000 5-30 Dedicated edge server VPS + Redis + TSDB Coverage gaps
Large 1,000-5,000 30-100 Multiple edge servers (per zone) Load-balanced cluster Edge-to-edge coordination
Enterprise 5,000-50,000 100-500 Regional edge clusters Kubernetes + distributed TSDB Multi-site aggregation

The transition from medium to large scale is the most challenging. At 1,000+ tags, a single edge server becomes a bottleneck for location estimation, and you need to partition processing by zone. However, tags that move between zones require handoff logic between edge servers, adding coordination overhead.

11. Common Architecture Mistakes

Mistake Symptom Root Cause Fix
Cloud does all processing High latency, high cloud cost No edge filtering Move dedup + smoothing to edge
Single gateway per zone Coverage holes, no redundancy Minimizing gateway count Overlap coverage by 30-50%
HTTP for every packet Gateway CPU spikes, packet loss TCP handshake overhead Use MQTT persistent connection
No offline buffer Data gaps during network outage Fire-and-forget forwarding SQLite buffer with replay
Raw RSSI for location Location jumps 5-10 m randomly No smoothing filter Exponential smoothing (alpha=0.3)
No dwell timer False zone changes at boundaries Overlapping coverage flicker 30s minimum dwell before report
Over-provisioned gateways Redundant data, high cost More gateways = better assumption Target 30-50% coverage overlap
No tag inventory management Ghost tags in dashboard Dead tags not removed Auto-archive tags silent >24h

12. Deployment Checklist

# Item Pass Criteria
1 RF site survey completed All zones mapped with RSSI heatmap
2 Gateway positions surveyed 30-50% coverage overlap verified
3 Gateway firmware deployed All gateways running same version
4 Edge server installed and tested Dedup + smoothing pipeline operational
5 MQTT broker configured QoS 1, persistent sessions, ACL
6 Cloud database provisioned TSDB + relational + cache running
7 Tag inventory registered All tags in DB with metadata
8 Battery baseline recorded All tag battery levels logged
9 Location calibration done Path-loss exponent per zone set
10 Alert thresholds configured Battery, temperature, zone breach
11 Offline buffer tested Network cut, data preserved, replayed
12 Dashboard deployed Real-time view + history + export
13 Health monitoring active Gateway heartbeat + CPU + RSSI
14 Failover tested Gateway down, system continues
15 Performance baseline captured Capture rate, latency, accuracy logged

13. Summary

A production BLE asset tracking system succeeds or fails on its infrastructure, not its tags. The BLE tag is the cheapest and simplest component in the stack. The gateway network, edge filtering pipeline, data transport, and cloud backend determine whether the system delivers 3-meter accuracy at 2-second latency or 10-meter accuracy at 30-second latency. The architecture principles are straightforward: filter early and aggressively at the edge, use MQTT for transport, buffer for offline operation, and calibrate location estimation per zone. The difference between a system that works in a demo and one that works in production is entirely in the engineering of these four tiers.