M5Stack で iBeacon を飛ばす(PlatformIO環境)

iBeacon をスマホで受信するテストしているときに、iBeacon 自体がないので困ることはありませんか?いや、困ってなくても作ってみるのがお勧めです。

たまに作ろうと思うと、ググってもうまく引っ掛からない(iBeaconを受信するツールはあるのですが、発信するほうはあまりないので)、自分のブログに残しておきます。

platformio.ini

初代の m5stack の platformio,ini は次の通り。m5stick cplus とかを使う場合は適宜かえておきます。

[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = arduino
monitor_speed = 115200

lib_deps = 
    m5stack/M5Stack@^0.3.9

RTOS を使わない場合

RTOS を使わずに framework の android で setup/loop 関数を使って作ります。iBeacon を発信するツールとして使いたいときはこれで十分です。

#include <M5Stack.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <M5Stack.h>

static BLEServer *pServer ;
static BLEAdvertising *advertising ;

#define BEACON_UUID "12345678-1111-2222-3333-56789abcdef0"
#define BEACON_MAJOR 0x0001
#define BEACON_MINOR 0x0001
#define BEACON_POWER 0xC5  // Measured power (at 1 meter distance)

void init_beacon() {
  BLEAdvertisementData ibeaconData;
  // 12345678-1111-2222-3333-56789abcdef0
  char UUID[] = { 
    0x12, 0x34, 0x56, 0x78, 
    0x11, 0x11, 
    0x22, 0x22, 
    0x33, 0x33, 
    0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 
    0x00};
    int major = BEACON_MAJOR ;
    int minor = BEACON_MINOR ;
    int txPower = BEACON_POWER;

    ibeaconData.setFlags(0x1A);
    std::string strServiceData = "";
    strServiceData += (char)0x4c;  // apple 
    strServiceData += (char)0x00;  // apple 
    strServiceData += (char)0x02;  // apple 
    strServiceData += (char)0x15;  // apple
    strServiceData += UUID ;
    strServiceData += (char)(major >> 8);    // major
    strServiceData += (char)(major & 0xFF);  // major
    strServiceData += (char)(minor >> 8);    // minor
    strServiceData += (char)(minor & 0xFF);  // minor
    strServiceData += (char)txPower;  // 
    ibeaconData.setManufacturerData(strServiceData);

    // アドバタイズの設定
    advertising = BLEDevice::getAdvertising();
    advertising->setAdvertisementData(ibeaconData);
    advertising->setScanResponse(false);

}

// Arduinoのsetup関数
void setup() {
    // M5Stack の初期化
    M5.begin();
    Serial.begin(115200);

    BLEDevice::init("M5Stack");
    BLEServer *pServer = BLEDevice::createServer();
    init_beacon();

    // taskBeacon 内で start している
    // 実は start 1回だけではうまくいかないことが多く、
    // start/stop を繰り返す必要がある
    // advertising->start();
}

// Arduinoのloop関数
void loop() {
    // メインタスクのループ
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(10, 10);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(2);
    M5.Lcd.print("Hello, M5Stack with FreeRTOS! ");
    M5.Lcd.print(BEACON_UUID);
    Serial.println("Hello, M5Stack with FreeRTOS! ");
    // vTaskDelay(pdMS_TO_TICKS(3000));  // 3秒待機
    advertising->start();
    delay(3000);
    advertising->stop();
    delay(3000);
}

iBeacon で送信するサービスUUIDは、Apple の 0x004c となっていて、内部データとして iBeacon として送信したい UUID(12345678-1111-2222-3333-56789abcdef0)を設定しています。本来ならば、BEACON_UUID から 16 bytes のデータを作るところでが、面倒なので UUID 配列に詰め込んでいます。ちなみに 0x1502 は特性ID(キャラクタリスティック)のようなもので固定値です。

このコードでは、init_beacon 関数内でビーコンの発信設定をしているので、advertising->start(); を 1回だけ呼び出せばビーコンが送信されるような気もするのですが、うまくいきません。

これが BLEAdvertising クラスの仕様なのか、M5Stack の制限なのか(あるいは手元の M5Stack が不調なのか)わかりませんが、loop 関数にあるように、ある程度の間隔で start/stop を繰り返してやらないと iBeacon のデータが飛びません。

iBeacon が無事飛んでいると、次のように別のツールで動作確認ができます。

これは dotnet で作ったツールで、こんなコードで動かしています。

using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Storage.Streams;

// BLEのスキャナ
BluetoothLEAdvertisementWatcher watcher;
// MACアドレスの保持(ランダムなので意味はない)
List<ulong> maclist = new List<ulong>();

Main(args);

void Main(string[] args)
{
    Console.WriteLine("Folkbears iBeaconCheck");

    watcher = new BluetoothLEAdvertisementWatcher()
    {
        ScanningMode = BluetoothLEScanningMode.Passive
    };
    // スキャンしたときのコールバックを設定
    watcher.Received += Watcher_Received;
    // スキャン開始
    watcher.Start();
    // キーが押されるまで待つ
    Console.WriteLine("Press any key to continue");
    Console.ReadLine();
}

void Watcher_Received(
    BluetoothLEAdvertisementWatcher sender,
    BluetoothLEAdvertisementReceivedEventArgs args)
{

    var uuids = args.Advertisement.ServiceUuids;
    var mac = string.Join(":",
                BitConverter.GetBytes(args.BluetoothAddress).Reverse()
                .Select(b => b.ToString("X2"))).Substring(6);
    var name = args.Advertisement.LocalName;
    var rssi = args.RawSignalStrengthInDBm;
    var time = args.Timestamp.ToString("yyyy/MM/dd HH:mm:ss.fff");
    

    if ( args.Advertisement.ManufacturerData.Count > 0)
    {
        var data = args.Advertisement.ManufacturerData[0];
        if ( data.CompanyId == 0x004c && data.Data.Length >= 23)
        {
            byte[] ibeacon = new byte[data.Data.Length];
            DataReader.FromBuffer(data.Data).ReadBytes(ibeacon);
            if (ibeacon[0] == 0x02 && ibeacon[1] == 0x15)
            {
                byte[] uuid = ibeacon[2..18];
                byte[] major = ibeacon[18..20];
                byte[] minor = ibeacon[20..22];
                byte txpower = ibeacon[22];

                int majorvalue = major[0] * 256 + major[1];
                int minorvalue = minor[0] * 256 + minor[1];
                Console.WriteLine($"{time} [{tohex(uuid)}] {rssi} dBm {mac} "
                    + string.Format("{0:x04}", majorvalue) + " "
                    + string.Format("{0:x04}", minorvalue) + " "
                    );
            }
        }
    }
    string tohex( byte[] data )
    {
        return BitConverter.ToString(data).Replace("-", "").ToLower();
    }
}

*.csproj ファイルはこんな感じです。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

RTOS を使う場合

先のコードでは iBeacon の発信と停止を loop 内で制御しましたが、RTOS を使えばタスク内で記述ができます。

#include <M5Stack.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <M5Stack.h>

static BLEServer *pServer ;
static BLEAdvertising *advertising ;

// タスクの定義
void task1(void *pvParameters) {
    while (1) {
        Serial.println("Hello from Task 1");
        vTaskDelay(pdMS_TO_TICKS(1000));  // 1秒待機
    }
}

void task2(void *pvParameters) {
    while (1) {
        Serial.println("Hello from Task 2");
        vTaskDelay(pdMS_TO_TICKS(2000));  // 2秒待機
    }
}

void taskBeacon(void *pvParameters) {
    while (1) {
        Serial.println("task beacon start");
        advertising->start();
        vTaskDelay(pdMS_TO_TICKS(3000));  // 3秒待機
        Serial.println("task beacon stop");
        advertising->stop();
        vTaskDelay(pdMS_TO_TICKS(10000));  // 10秒待機
    }
}

#define BEACON_UUID "12345678-1111-2222-3333-56789abcdef0"
#define BEACON_MAJOR 0x0001
#define BEACON_MINOR 0x0001
#define BEACON_POWER 0xC5  // Measured power (at 1 meter distance)

void init_beacon() {
  BLEAdvertisementData ibeaconData;
  // 12345678-1111-2222-3333-56789abcdef0
  char UUID[] = { 
    0x12, 0x34, 0x56, 0x78, 
    0x11, 0x11, 
    0x22, 0x22, 
    0x33, 0x33, 
    0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 
    0x00};
    int major = BEACON_MAJOR ;
    int minor = BEACON_MINOR ;
    int txPower = BEACON_POWER;

    ibeaconData.setFlags(0x1A);
    std::string strServiceData = "";
    strServiceData += (char)0x4c;  // apple 
    strServiceData += (char)0x00;  // apple 
    strServiceData += (char)0x02;  // apple 
    strServiceData += (char)0x15;  // apple
    strServiceData += UUID ;
    strServiceData += (char)(major >> 8);    // major
    strServiceData += (char)(major & 0xFF);  // major
    strServiceData += (char)(minor >> 8);    // minor
    strServiceData += (char)(minor & 0xFF);  // minor
    strServiceData += (char)txPower;  // 
    ibeaconData.setManufacturerData(strServiceData);

    // アドバタイズの設定
    advertising = BLEDevice::getAdvertising();
    advertising->setAdvertisementData(ibeaconData);
    advertising->setScanResponse(false);
}

// Arduinoのsetup関数
void setup() {
    // M5Stack の初期化
    M5.begin();
    Serial.begin(115200);

    BLEDevice::init("M5Stack");
    BLEServer *pServer = BLEDevice::createServer();
    init_beacon();

    // taskBeacon 内で start している
    // 実は start 1回だけではうまくいかないことが多く、
    // start/stop を繰り返す必要がある
    // advertising->start();

    // タスクの作成
    xTaskCreate(&task1, "task1", 2048, NULL, 5, NULL);
    xTaskCreate(&task2, "task2", 2048, NULL, 5, NULL);
    xTaskCreate(&taskBeacon, "taskBeacon", 2048, NULL, 5, NULL);
}

// Arduinoのloop関数
void loop() {
    // メインタスクのループ
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(10, 10);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(2);
    M5.Lcd.print("Hello, M5Stack with FreeRTOS! ");
    M5.Lcd.print(BEACON_UUID);
    Serial.println("Hello, M5Stack with FreeRTOS! ");
    vTaskDelay(pdMS_TO_TICKS(3000));  // 3秒待機
}

ここでは、task1, task2, taskBeacon という3つのタスクが同時に動いています。優先度が同じなので、並列で動いているのですが、優先度を変えれば iBeacon 発信のタスクだけ優先して動かすことも可能でしょう(実験までしていませんが)。

先の iBeacon の発信/停止の切り替えは loop 関数から taskBeacon 内に移動しています。

m5stackの画面はこんな感じです。

カテゴリー: 開発 パーマリンク