Raspberry Pi Pico WでBLEビーコン(FSC-BP108)をスキャンして近接を検出する方法

目次

はじめに

身の回りのモノや場所を「電波の名札」で識別できたら便利だと思いませんか?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できます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次