BLEタグ資産追跡システムの構築は、適切なタグを選ぶことではありません。施設内に散在する数百〜数千のタグからデータを受信、フィルタリング、転送、保存するインフラストラクチャについてです。タグ自体は単なるブロードキャスタであり、知能はゲートウェイネットワーク、エッジプロセッサ、クラウドバックエンドに存在します。本記事では、病院、倉庫、製造現場での本番稼働データに基づき、無線受信からダッシュボードまでのフルシステムアーキテクチャを分解し、ハードウェア選定表、フィルタリングアルゴリズム、データパイプラインスキーマ、デプロイコストモデルを提示します。
1. システムアーキテクチャ概要
本番BLE資産追跡システムは4層構造で、各層に異なるレイテンシ、スループット、信頼性要件があります:
| 層 | コンポーネント | レイテンシ予算 | データ量 | 障害影響 |
|---|---|---|---|---|
| T1: タグ層 | BLEタグ(数百〜数千) | N/A(ブロードキャストのみ) | ~30バイト/タグ/イベント | 単一資産が消失 |
| T2: ゲートウェイ層 | BLEスキャナ(5-50台) | 100-500 msスキャンサイクル | ~50 KB/sピーク/GW | ゾーンが消失(10-50タグ) |
| T3: エッジ層 | ローカルサーバまたはエッジアプライアンス | 1-5 s処理 | ~200 KB/s集約 | 追跡遅延、データ消失なし |
| T4: クラウド層 | バックエンド、DB、ダッシュボード | 1-10 sエンドツーエンド | ~500 KB/s持続 | 履歴データギャップ |
最も一般的なアーキテクチャの間違いは、ゲートウェイを生BLEパケットをクラウドに転送する単なるリレーとして扱うことです。500タグ、1秒間隔、10ゲートウェイの場合、1秒間に5,000パケットがクラウドインジェストエンドポイントに到達し、それぞれにカバレッジゾーンが重複したRSSI読み取りが含まれます。エッジフィルタリングは最適化ではなく要件です。
2. ゲートウェイハードウェア選定
| プラットフォーム | BLEチップ | CPU | RAM | 消費電力 | BOMコスト | 適用途 |
|---|---|---|---|---|---|---|
| ESP32ベース | ESP32(デュアルモード) | 240 MHz Xtensa x2 | 520 KB SRAM | 2.5W(WiFi+BLE) | $3-5 | 小規模サイト、WiFiバックホール |
| Raspberry Pi + USBドングル | nRF52840ドングル | 1.4 GHz ARM A72 x4 | 1-4 GB | 5-7W | $45-60 | R&D、プロトタイピング、小ロット |
| 専用ゲートウェイ | nRF52832/52840 + ESP32 | ESP32 240MHz + nRF52 | 520KB + 256KB | 2-3W | $15-25 | 本番デプロイ |
| 産業用ゲートウェイ(Linux) | CC2640R2 + WiFi/LTE | ARM Cortex-A7 1GHz | 512 MB | 5-15W | $200-400 | 過酷な環境、セルラー |
2.1 ESP32の能力と限界
ESP32はWiFiとBLEを内蔵するため、コスト重視のデプロイで最も一般的です。ただし、ESP32のBLEコントローラは2.4 GHz無線をWiFiとBLEで共有しており、WiFi送信中はBLEスキャンができません。この時分割多重によりスキャンギャップが発生します:
| WiFi活動 | BLEスキャンギャップ | パケットロス(1s間隔) | パケットロス(500ms) |
|---|---|---|---|
| アイドル(ビーコンのみ) | 0 ms | 0% | 0% |
| MQTTパブリッシュ(5s毎) | ~15 ms/ギャップ | 0.3% | 0.6% |
| WiFiストリーミング(連続) | ~50 ms/ギャップ | 1.0% | 2.0% |
| OTAアップデート | ~200 ms/ギャップ | 5-8% | 10-15% |
2.2 専用ゲートウェイアーキテクチャ
本番ゲートウェイは通常、nRF52(専用BLEスキャナ)とESP32(WiFi/CPU)をペアにします。nRF52が連続アクティブスキャンを実行し、UART/SPIでESP32にパケットを転送し、ESP32がフィルタリング、バッファリング、クラウド通信を処理します。このデュアルチップ設計で無線の競合が完全に排除されます。
# nRF52側:連続BLEスキャナ
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))
# ESP32側:受信、フィルタ、バッチアップロード
rx_buffer = []
def uart_rx_callback(data):
pkt = json.loads(data)
if should_forward(pkt):
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 ゲートウェイ配置とカバレッジ
| 精度階層 | ゲートウェイ/100m2 | 典型間隔 | コスト/m2 | 用途 |
|---|---|---|---|---|
| プレゼンス(ルームレベル) | 0.5-1.0 | 10-15 m | $0.15-0.30 | ゾーン別資産プレゼンス |
| 粗位置(3-5 m) | 2-4 | 5-8 m | $0.60-1.20 | 倉庫通路追跡 |
| 高精度位置(1-2 m) | 6-10 | 3-5 m | $1.80-3.00 | 手術器具追跡 |
| サブメートル(0.5 m) | 15-25 | 2-3 m | $4.50-7.50 | 高価値資産監査 |
3. BLEスキャン戦略
3.1 アクティブvsパッシブスキャン
| パラメータ | パッシブスキャン | アクティブスキャン |
|---|---|---|
| SCAN_REQ送信 | いいえ | はい |
| SCAN_RSP受信 | いいえ | はい(最大31バイト追加) |
| 消費電力 | 低(Rxのみ) | 高(Tx + Rx) |
| タグ電力影響 | なし | 全スキャンに応答で+20-40% |
| 取得データ | 31バイト(Advのみ) | 62バイト(Adv + Scan Rsp) |
3.2 スキャンウィンドウと間隔
| スキャン間隔 | スキャンウィンドウ | デューティサイクル | 捕捉率(1sタグ) | 捕捉率(500ms) |
|---|---|---|---|---|
| 連続 | 連続 | 100% | ~99.5% | ~99.0% |
| 1000 ms | 1000 ms | 100% | ~99.5% | ~99.0% |
| 1000 ms | 500 ms | 50% | ~50-65% | ~50-65% |
| 5000 ms | 1000 ms | 20% | ~20-30% | ~20-30% |
3.3 無線レベルの重複フィルタリング
| フィルタモード | 重複報告 | パケット/s(500タグ,1s,10GW) | GW CPU負荷 |
|---|---|---|---|
| フィルタなし | 全て | ~15,000 | 高 |
| コントローラ重複排除 | 1/タグ/サイクル | ~500 | 低 |
| アプリ重複排除 | 1/タグ/ウィンドウ | ~500 | 低-中 |
4. エッジフィルタリング
4.1 マルチゲートウェイ重複排除
DEDUP_WINDOW_MS = 2000
def deduplicate(raw_reports):
by_tag = group_by_mac(raw_reports)
result = []
for mac, reports in by_tag.items():
if len(reports) == 1:
result.append(reports[0])
continue
best = max(reports, key=lambda r: score_report(r))
result.append(best)
return result
def score_report(r):
rssi_score = (r.rssi + 100) / 70
age_s = (now() - r.ts) / 1000
fresh_score = max(0, 1 - age_s / 5)
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平滑化と外れ値除去
| フィルタ | ウィンドウ | 追加レイテンシ | 分散低減 | 実装複雑度 |
|---|---|---|---|---|
| 生(なし) | 1サンプル | 0 s | 0% | なし |
| 移動平均 | 5サンプル | 2-5 s | ~55% | 低 |
| 指数平滑 | 無限(重み付き) | 1-3 s | ~50% | 低 |
| メディアン | 3-7サンプル | 1-3 s | ~65% | 中 |
| カルマン | 適応 | 0.5-2 s | ~70% | 高 |
# 指数平滑(alpha = 0.3)
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 イベントベース転送
| 転送戦略 | パケット/タグ/時 | クラウドBW(500タグ) | 用途 |
|---|---|---|---|
| 毎スキャン(1s) | 3,600 | ~14 MB/h | サブ秒更新RTLS |
| 5s毎(バッチ) | 720 | ~2.8 MB/h | 標準追跡 |
| ゾーン変更のみ | ~5-20 | ~40 KB/h | ルームレベルプレゼンス |
| 閾値突破+ハートビート | ~2-10 | ~20 KB/h | 温度/湿度アラート |
5. データパイプライン設計
5.1 転送プロトコル比較
| プロトコル | オーバーヘッド | 信頼性 | レイテンシ | GW電力 | 適用途 |
|---|---|---|---|---|---|
| HTTP POST (JSON) | ~400バイト | TCP保証 | 100-500 ms | 高 | 小規模、シンプルバックエンド |
| MQTT (QoS 1) | ~20バイト | TCP + ACK | 50-200 ms | 低 | 本番デプロイ |
| UDP (カスタム) | ~8バイト | なし | 10-50 ms | 最低 | リアルタイムRTLS、ローカルLAN |
| LoRaWAN | ~13バイト | 確認ダウンリンク | 1-10 s | N/A | WiFiなしの遠隔サイト |
5.2 MQTTトピック構造
# MQTTトピック階層
# 形式: site/zone/gateway/tag/event
site-001/zone-A/gw-01/+/scan # gw-01の全スキャン
site-001/zone-A/+/tag-005/scan # zone-A内tag-005の全スキャン
site-001/+/+/tag-005/zone_change # tag-005のゾーン変更イベント
site-001/+/+/+/battery_low # サイト全体の電池アラート
site-001/+/+/+/temperature_alert # 温度閾値突破
# ペイロード形式(JSON、~80バイト)
{
"mac": "AA:BB:CC:DD:EE:01",
"rssi": -67,
"gw": "gw-01",
"ts": 1722422400000,
"bat": 85,
"temp": 23.5,
"seq": 4823
}
5.3 データスキーマとストレージ
| ストア | 技術 | データ | 保持 | クエリパターン |
|---|---|---|---|---|
| 時系列(ホット) | InfluxDB / TimescaleDB | 生スキャンイベント | 7-30日 | タグ+時間の範囲クエリ |
| 時系列(コールド) | S3 / Parquet | 集約イベント | 1-5年 | バッチ分析、コンプライアンス |
| リレーショナル | PostgreSQL / MySQL | 資産メタデータ、ゾーン、ユーザー | 永久 | CRUD、結合 |
| キャッシュ | Redis | 最終位置 | TTL 5分 | サブmsルックアップ |
5.4 ストレージサイジング
# 500タグ、5s転送間隔のサイジング
tags = 500
events_per_tag_per_hour = 3600 / 5 # = 720
events_per_hour = tags * 720 # = 360,000
events_per_day = 360,000 * 24 # = 8,640,000
bytes_per_day = 8,640,000 * 80 # = 691 MB/day
bytes_per_year = 691 * 365 # = ~252 GB (生)
compressed_per_year = 252 / 5 # = ~50 GB (Parquet 5:1)
total_storage_year = 50 * 1.4 # = ~70 GB (DBオーバーヘッド込み)
6. エッジでの位置推定
| アルゴリズム | 最小GW数 | 精度 | エッジCPU | キャリブレーション | 適用途 |
|---|---|---|---|---|---|
| 近接(最強RSSI) | 1 | ルームレベル(5-10m) | 最小 | なし | シンプルプレゼンス |
| 重み付き重心 | 3+ | 3-5 m | 低 | GW位置のみ | 開放エリア |
| 三辺測量 | 3+ | 2-4 m | 中 | ゾーン別パスロス指数 | 倉庫、廊下 |
| フィンガープリント(kNN) | 4+ | 1-3 m | 中-高 | RF調査(数時間) | 複雑屋内環境 |
| BLE AoA | 1(アレイアンテナ) | 0.5-1 m | 高 | アンテナ位相校正 | 高価値資産追跡 |
6.1 重み付き重心アルゴリズム
def estimate_position(reports, gateway_positions):
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
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)
7. システム信頼性とフェイルオーバー
7.1 ゲートウェイヘルスモニタリング
| メトリック | 正常範囲 | アラート閾値 | 確認間隔 |
|---|---|---|---|
| パケット/分 | 50-500 | < 10 or > 2000 | 60 s |
| CPU温度 | 40-65 C | > 75 C | 300 s |
| WiFi RSSI | -30 to -65 dBm | < -75 dBm | 60 s |
| 稼働率 | > 99.5% | < 99% | 日次 |
| 最終ハートビート | < 30 s前 | > 120 s前 | 30 s |
7.2 エッジ-クラウド接続断時のバッファ
| 最大ダウンタイム | バッファサイズ(500タグ,5s) | ストレージ |
|---|---|---|
| 15分 | ~4.3 MB | RAM |
| 1時間 | ~17 MB | RAM/SD |
| 4時間 | ~69 MB | SD/eMMC |
| 24時間 | ~415 MB | SSD/eMMC |
8. デプロイコストモデル
10,000 m2施設、500資産の完全BLE資産追跡システム:
| コンポーネント | 数量 | 単価 | 小計 | % |
|---|---|---|---|---|
| BLEタグ(CR2032、3年寿命) | 500 | $8 | $4,000 | 18% |
| 専用ゲートウェイ | 20 | $120 | $2,400 | 11% |
| エッジサーバ | 1 | $800 | $800 | 4% |
| ネットワーク機器+配線 | 1 | $1,500 | $1,500 | 7% |
| クラウドインフラ(1年目) | 1 | $3,600 | $3,600 | 16% |
| ソフトウェアライセンス(1年目) | 1 | $5,000 | $5,000 | 23% |
| 設置+コミッショニング | 1 | $3,000 | $3,000 | 14% |
| 予備タグ+GW(10%) | – | – | $640 | 3% |
| RF調査+校正 | 1 | $1,200 | $1,200 | 5% |
| 1年目合計 | $22,140 | 100% | ||
追跡資産あたりコスト:$22,140 / 500 = $44.28(1年目)、$8,640 / 500 = $17.28/年(経常)。RFID方式($60-120/資産)やWi-Fi RTLS($80-150/資産)と比較して有利です。
9. 本番パフォーマンスベンチマーク
| メトリック | 病院(200タグ,8GW) | 倉庫(500タグ,20GW) | 工場(1000タグ,15GW) |
|---|---|---|---|
| スキャン捕捉率 | 98.2% | 96.5% | 94.1% |
| 位置精度(中央値) | 3.2 m | 4.8 m | 5.5 m |
| エンドツーエンドレイテンシ(p95) | 2.8 s | 4.1 s | 5.3 s |
| GW稼働率 | 99.7% | 99.2% | 98.8% |
| クラウドデータ/月 | 4.2 GB | 18.7 GB | 24.3 GB |
| 誤ゾーン変更率 | 0.8% | 2.1% | 3.5% |
| タグ電池寿命(中央値) | 31ヶ月 | 28ヶ月 | 26ヶ月 |
10. スケーリング考慮事項
| 規模 | タグ | GW | エッジ | クラウド | 課題 |
|---|---|---|---|---|---|
| 小 | 1-100 | 1-5 | 単一GW | 単一VPS | コスト効率 |
| 中 | 100-1,000 | 5-30 | 専用エッジ | VPS+Redis+TSDB | カバレッジギャップ |
| 大 | 1,000-5,000 | 30-100 | 複数エッジ | LBクラスタ | エッジ間調整 |
| エンタープライズ | 5,000-50,000 | 100-500 | 地域エッジクラスタ | K8s+分散TSDB | マルチサイト集約 |
11. 一般的なアーキテクチャの間違い
| 間違い | 症状 | 原因 | 修正 |
|---|---|---|---|
| クラウドが全処理 | 高レイテンシ、高コスト | エッジフィルタなし | エッジで重複排除+平滑化 |
| ゾーン1GWのみ | カバレッジホール | GW数を最小化 | 30-50%カバレッジ重複 |
| 全パケットHTTP | CPUスパイク、パケットロス | TCPハンドシェイク | MQTT永続接続 |
| オフラインバッファなし | ネットワーク断でデータギャップ | Fire-and-forget | SQLiteバッファ+リプレイ |
| 生RSSIで位置 | 5-10mランダム跳躍 | 平滑化なし | 指数平滑(alpha=0.3) |
| ドエルタイマなし | 境界で誤ゾーン変更 | カバレッジ重複フリッカ | 30s最小ドエル |
| GW過剰プロビジョニング | 冗長データ、高コスト | 多いほど良い誤解 | 30-50%重複を目標 |
| タグ在庫管理なし | ゴーストタグ | 死タグ未削除 | 24h沈黙で自動アーカイブ |
13. まとめ
本番BLE資産追跡システムの成功または失敗はインフラストラクチャで決まり、タグではありません。BLEタグはスタック中最も安価でシンプルなコンポーネントです。ゲートウェイネットワーク、エッジフィルタリングパイプライン、データ転送、クラウドバックエンドが、システムが2秒レイテンシで3メートル精度を提供するか、30秒レイテンシで10メートル精度にとどまるかを決定します。アーキテクチャ原則は明確です:エッジで早期かつ積極的にフィルタし、転送にはMQTTを使用し、オフライン動作のためにバッファし、ゾーンごとに位置推定を校正します。デモで動くシステムと本番で動くシステムの違いは、完全にこの4層のエンジニアリングにあります。
