はじめに
身の回りのモノや場所を「電波の名札」で識別できたら便利だと思いませんか?BLEビーコンはまさにそれを実現する小さな発信機です。
今回は 防水仕様のFeasyBeacon FSC-BP108 を例に、Raspberry Pi Pico W で受信して処理する仕組みを紹介します。
Beaconとは何か?基本の仕組みと規格
Beaconは BLE(Bluetooth Low Energy)のアドバタイズ信号を周期的に送信する装置です。
普通のBluetooth機器が「接続して通信」するのに対し、Beaconは「接続せずに情報を放送する」点が特徴です。
主な規格
- iBeacon(Apple)
UUID + MAJOR + MINOR で構成され、場所やオブジェクトを一意に識別。 - Eddystone(Google)
URLを直接送れる(Eddystone-URL)ほか、UIDやテレメトリ(電池残量や温度)も送信可能。 - AltBeacon
オープンな規格で、iBeaconの特許制約を避けたもの。
Beaconは「誰がそこにいる/何がそこにある」を電波でシンプルに伝える仕組です。
FSC-BP108の特徴
FSC-BP108 は以下のような特徴があります。
| 項目 | 特徴 |
|---|---|
| 規格 | iBeacon / Eddystone / AltBeacon 同時発信 |
| 防水防塵 | IP67、屋外設置も可能 |
| 電池寿命 | 数年単位(設定次第) |
| 設定 | スマホアプリからUUIDや出力強度を変更可能 |
| サイズ | 小型で貼り付けや持ち運びに向く |
実際に屋外の資産管理や店舗の近接通知など、プロ用途でも多く使われています。
Pico Wで受信する仕組み
Raspberry Pi Pico W は Wi-Fi だけでなく Bluetooth Low Energy にも対応しています。
このBLE機能を使い、ビーコンの電波をスキャン → フィルタ → 判定 → アクション という流れを組むことができます
動作原理のフロー
Beacon (FSC-BP108)
↓ 周期的に電波広告 (iBeacon / Eddystone)
Pico W でスキャン
↓ UUIDやURLを解析
↓ RSSIの移動平均 + ヒステリシスで「近い / 遠い」を安定判定
Pico Wでアクション
LED点灯 / LINE通知 / MQTT送信 など自由に
このフローのポイントは以下です。
・RSSI(電波強度)を距離の目安にする
・移動平均+ヒステリシスで誤判定を減らす
・フィルタ条件(UUID、MAC、URL)で対象ビーコンを特定
サンプルコードの解説
以下のコードは、指定したUUIDのiBeaconを検出し、近づくとLEDを点灯する例です。
まずはBeaconの識別IDをチェックします。
# main.py (MicroPython for Raspberry Pi Pico W)
# iBeaconのUUID / MAJOR / MINOR / RSSIをログ出力する
import bluetooth, struct, time
ble = bluetooth.BLE()
ble.active(True)
def parse_ibeacon(payload):
"""
iBeaconのManufacturer Dataを解析して (uuid, major, minor, txpower) を返す。
見つからなければ None。
"""
i = 0
while i + 1 < len(payload):
length = payload[i]
if length == 0:
break
ftype = payload[i+1]
field = payload[i+2:i+1+length]
# Manufacturer Data (0xFF)
if ftype == 0xFF and len(field) >= 25:
company_id, = struct.unpack("<H", field[0:2])
if company_id == 0x004C and field[2] == 0x02 and field[3] == 0x15:
uuid = field[4:20].hex()
major, minor, txp = struct.unpack(">HHb", field[20:25])
return uuid, major, minor, txp
i += 1 + length
return None
def irq(event, data):
if event == bluetooth._IRQ_SCAN_RESULT:
addr_type, addr, adv_type, rssi, adv_data = data
result = parse_ibeacon(adv_data)
if result:
uuid, major, minor, txp = result
mac = ":".join("{:02X}".format(b) for b in addr)
print("MAC:", mac,
"UUID:", uuid,
"MAJOR:", major,
"MINOR:", minor,
"RSSI:", rssi)
elif event == bluetooth._IRQ_SCAN_DONE:
# 継続スキャン
ble.gap_scan(0, 300, 300)
# BLEスキャン開始
ble.irq(irq)
ble.gap_scan(0, 300, 300)
print("Scanning for iBeacon... (Ctrl+C to stop)")
while True:
time.sleep_ms(500)以下が、main.pyです。
# main.py (MicroPython for Raspberry Pi Pico W)
# 目的:
# - iBeacon をスキャンして UUID / MAJOR / MINOR / RSSI を取得
# - 指定 UUID(+必要なら MAJOR/MINOR) のビーコンだけを特定
# - RSSI の移動平均+ヒステリシスで「近い/遠い」を安定判定し LED を制御
import time
# ===== 互換: bluetooth / ubluetooth どちらでも動作 =====
try:
import bluetooth as bt
except ImportError:
import ubluetooth as bt
ble = bt.BLE()
ble.active(True)
# ===== IRQ 定数フォールバック(FW差異対策)=====
try:
_IRQ_SCAN_RESULT = bt._IRQ_SCAN_RESULT
except AttributeError:
_IRQ_SCAN_RESULT = 5 # scan result
try:
_IRQ_SCAN_DONE = bt._IRQ_SCAN_DONE
except AttributeError:
_IRQ_SCAN_DONE = 6 # scan done
# ====== 設定(ここを自分の値に)======
# ハイフン無し32桁の16進文字列。小文字/大文字は不問
TARGET_IBEACON_UUID = "fda50693a4e24fb1afcfc6eb07647825".lower()
TARGET_IBEACON_MAJOR = None # 例: 10065 (使わないなら None)
TARGET_IBEACON_MINOR = None # 例: 26049 (使わないなら None)
# RSSI 平滑&判定
MA_WINDOW = 5 # 移動平均の窓
NEAR_THRESH = -65 # 近い: 移動平均がこの値以上(dBm)
FAR_THRESH = -72 # 遠い: 移動平均がこの値以下(dBm)
STABLE_HITS = 3 # 同一状態が何連続で出たら確定とみなすか
# スキャン設定(duration=0 は連続スキャン)
SCAN_INTERVAL_MS = 300
SCAN_WINDOW_MS = 300
# ======================================
# ===== オンボードLED(無ければコメントアウト)=====
try:
from machine import Pin
led = Pin("LED", Pin.OUT)
def set_led(on: bool):
led.value(1 if on else 0)
except Exception:
def set_led(on: bool):
pass # LEDなし環境でも動作
# ===== iBeacon パーサ =====
def parse_ibeacon(adv_payload: bytes):
"""
Advertising payload から iBeacon を探し、
見つかれば (uuid_hex, major, minor, txpower) を返す。無ければ None。
"""
if not isinstance(adv_payload, (bytes, bytearray)):
adv_payload = bytes(adv_payload)
i = 0
L = len(adv_payload)
while i + 1 < L:
length = adv_payload[i]
if length == 0:
break
ftype = adv_payload[i + 1]
field = adv_payload[i + 2 : i + 1 + length] # [len][type][field...]
# Manufacturer Specific Data (0xFF)
if ftype == 0xFF and len(field) >= 25:
# company_id: little-endian (Apple = 0x004C)
company_id = field[0] | (field[1] << 8)
# iBeacon signature: type=0x02, len=0x15 の並び
if company_id == 0x004C and field[2] == 0x02 and field[3] == 0x15:
uuid_hex = field[4:20].hex().lower()
major = (field[20] << 8) | field[21]
minor = (field[22] << 8) | field[23]
txp = field[24] if field[24] < 128 else field[24] - 256 # int8
return (uuid_hex, major, minor, txp)
i += 1 + length
return None
def mac_str(addr_bytes):
return ":".join("{:02X}".format(b) for b in addr_bytes)
# ===== 近接判定用ステート =====
rssi_hist = []
state = "far" # "near" / "far"
stable = 0 # 同一状態が何連続か
def update_ma(rssi: int) -> float:
rssi_hist.append(rssi)
if len(rssi_hist) > MA_WINDOW:
rssi_hist.pop(0)
return sum(rssi_hist) / len(rssi_hist)
def desired_state(ma_rssi: float) -> str:
if ma_rssi >= NEAR_THRESH:
return "near"
if ma_rssi <= FAR_THRESH:
return "far"
return state # ヒステリシス領域は現状維持
def apply_state(new_state: str):
# ここを書き換えれば通知/MQTT/ログ保存などに差し替え可能
set_led(new_state == "near")
# ===== IRQ ハンドラ =====
def irq(event, data):
global state, stable
try:
if event == _IRQ_SCAN_RESULT:
addr_type, addr, adv_type, rssi, adv_data = data
parsed = parse_ibeacon(adv_data)
if not parsed:
return
uuid, major, minor, txp = parsed
# ==== UUID(+必要なら MAJOR/MINOR) で断定 ====
if uuid != TARGET_IBEACON_UUID:
return
if (TARGET_IBEACON_MAJOR is not None) and (major != TARGET_IBEACON_MAJOR):
return
if (TARGET_IBEACON_MINOR is not None) and (minor != TARGET_IBEACON_MINOR):
return
# ログ(観測履歴の確認に便利)
print("HIT",
"MAC:", mac_str(addr),
"UUID:", uuid,
"MAJOR:", major, "MINOR:", minor,
"RSSI:", rssi)
# ==== 近接判定(移動平均+ヒステリシス)====
ma = update_ma(rssi)
want = desired_state(ma)
if want == state:
stable = min(stable + 1, 1000)
else:
state = want
stable = 1
if stable >= STABLE_HITS:
apply_state(state)
elif event == _IRQ_SCAN_DONE:
# 連続スキャン再開
ble.gap_scan(0, SCAN_INTERVAL_MS, SCAN_WINDOW_MS)
except Exception as e:
# IRQ内例外は握りつぶさずログ
print("IRQ error:", repr(e))
# ===== スキャン開始 =====
ble.irq(irq)
ble.gap_scan(0, SCAN_INTERVAL_MS, SCAN_WINDOW_MS)
print("Scanning iBeacon... (Ctrl+C to stop)")
while True:
time.sleep_ms(500)応用アイデアと使用シーン
ビーコン+Pico W の組み合わせで、以下のような仕組みが作れます。
| 活用シーン | 仕組み | 例 |
| スマートホーム | 近づいたらON/離れたらOFF | 机に座ったらライトON、離席でOFF |
| 忘れ物防止 | ものにBeaconをつけて距離監視 | 工具箱や自転車が離れたら通知 |
| 屋内ナビゲーション | 複数Beacon+RSSIで位置を推定 | 博物館の展示場所に応じて説明表示 |
| 資産トラッキング | 倉庫の荷物や機材にタグ付け | 出入り口で自動記録 |
まとめ
- Beaconは「電波の名札」。BLEを使って自分の存在を知らせる小さな発信機です。
- FSC-BP108 は防水・長寿命・複数規格対応で実用性が高い。
- Pico W で受信すれば、「近づいたら反応」「離れたら通知」といった小さな自動化が手軽に実現できます。
身近なモノや場所に“見える化”を加えることで、ちょっと未来感のある便利な仕組みをDIYできます。
コメント