I2C 接続の有機EL + Raspberry Pi + Ruby で温度計付き時計を実装してみる

 今回は Raspberry Pi と OLED(有機EL)ディスプレイを接続して表示させてみたいと思います。使用したディスプレイは aitendo さんで販売されていた下記パーツです。

www.aitendo.com

 購入時は下記写真のように、ディスプレイとピンヘッダが同封されていますが別々になっていますので、自分で半田付けして使用します。

f:id:akanuma-hiroaki:20171014221626j:plain:w450

Raspberry Pi と接続

 それでは Raspberry Pi と OLED ディスプレイを接続します。今回下記サイトを参考にさせていただきました。

Overview | SSD1306 OLED Displays with Raspberry Pi and BeagleBone Black | Adafruit Learning System

raspberry pi でI2C接続の有機ELを使って時計を作る - Qiita

 実際の配線は下記のようにシンプルです。今回使用しているディスプレイは I2C 接続なので、Raspberry Pi では 3番(SDA)と5番(SCL)のピンを使うことになります。

f:id:akanuma-hiroaki:20171015015610p:plain:w300:left

VCC: 1番ピン(3.3V Power)
GND: 6番ピン(GND)
SCL: 5番ピン(SCL)
SDA: 3番ピン(SDA)

I2C の使用設定

 Raspberry Pi の I2C 接続を使うにはいくつか設定が必要です。設定方法については下記サイトを参考にさせていただきました。

I2Cを使う(設定編) | Make.

Configuring I2C | Adafruit's Raspberry Pi Lesson 4. GPIO Setup | Adafruit Learning System

 まずは I2C 接続をするためのモジュール設定を追加します。

pi@raspberrypi:~/display_sample $ sudo vi /etc/modules

 下記内容を追記します。

i2c-bcm2708 
i2c-dev

 そして再起動します。

pi@raspberrypi:~/display_sample $ sudo reboot

 次に必要なライブラリをインストールします。 python-smbus をインストールすると i2c-tools もインストールされます。

pi@raspberrypi:~/display_sample $ sudo apt-get install python-smbus
Reading package lists... Done
Building dependency tree        
Reading state information... Done
The following package was automatically installed and is no longer required:
  libbison-dev
Use 'apt-get autoremove' to remove it.
The following extra packages will be installed:
  i2c-tools
Suggested packages:
  libi2c-dev
The following NEW packages will be installed:
  i2c-tools python-smbus
0 upgraded, 2 newly installed, 0 to remove and 53 not upgraded.
Need to get 60.8 kB of archives.
After this operation, 286 kB of additional disk space will be used.
Do you want to continue? [Y/n] Y
Get:1 http://archive.raspberrypi.org/debian/ jessie/main i2c-tools armhf 3.1.1+svn-2 [51.3 kB]
Get:2 http://archive.raspberrypi.org/debian/ jessie/main python-smbus armhf 3.1.1+svn-2 [9,462 B]
Fetched 60.8 kB in 1s (35.8 kB/s)  
Selecting previously unselected package i2c-tools.
(Reading database ... 60922 files and directories currently installed.)
Preparing to unpack .../i2c-tools_3.1.1+svn-2_armhf.deb ...
Unpacking i2c-tools (3.1.1+svn-2) ...
Selecting previously unselected package python-smbus.
Preparing to unpack .../python-smbus_3.1.1+svn-2_armhf.deb ...
Unpacking python-smbus (3.1.1+svn-2) ...
Processing triggers for man-db (2.7.0.2-5) ...
Setting up i2c-tools (3.1.1+svn-2) ...
/run/udev or .udevdb or .udev presence implies active udev.  Aborting MAKEDEV invocation.
Setting up python-smbus (3.1.1+svn-2) ...

 次にシステムの設定ファイルを編集します。

pi@raspberrypi:~/display_sample $ sudo vi /boot/config.txt

 編集前後の差分は下記のようになります。

pi@raspberrypi:~/display_sample $ diff /boot/config.txt.20171014 /boot/config.txt
46c46,47
< #dtparam=i2c_arm=on
---
> dtparam=i2c1=on
> dtparam=i2c_arm=on

 起動時に I2C のドライバが有効になっていることを確認しておきます。

pi@raspberrypi:~/display_sample $ dmesg | grep i2c
[    2.429468] i2c /dev entries driver

 ここまでで設定変更は終了です。OLEDディスプレイを接続すると、下記のように接続デバイスのアドレスが表示されます。

pi@raspberrypi:~/display_sample $ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --        

Ruby でディスプレイに表示させてみる

 ここまでで接続と設定が完了したので、ディスプレイに何か表示させてみたいと思います。今回使用しているディスプレイのコントローラは SSD1306 で、公式では Python のライブラリが公開されています。

github.com

 Ruby のライブラリを探したところ、公式ではありませんが、下記の Gem が公開されていましたので、今回は試しにこれを使ってみます。

github.com

 Gemfile には下記のように書いておきます。

pi@raspberrypi:~/display_sample $ cat Gemfile 
# frozen_string_literal: true
source "https://rubygems.org"

gem 'SSD1306'

 この Gem を使うには ImageMagick が必要なので、インストールします。

pi@raspberrypi:~/display_sample $ sudo apt-get update
pi@raspberrypi:~/display_sample $ sudo apt-get install imagemagick

 そして bundle install します。

pi@raspberrypi:~/display_sample $ bundle install
Fetching gem metadata from https://rubygems.org/...
Fetching version metadata from https://rubygems.org/..
Resolving dependencies...
Using i2c 0.4.0
Installing rmagick 2.16.0 with native extensions
Using bundler 1.14.6
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /home/pi/display_sample/vendor/bundle/gems/rmagick-2.16.0/ext/RMagick
/home/pi/.rbenv/versions/2.4.1/bin/ruby -r ./siteconf20171014-3193-1tivtgl.rb extconf.rb
checking for gcc... yes
checking for Magick-config... no
checking for pkg-config... yes
Package MagickCore was not found in the pkg-config search path.
Perhaps you should add the directory containing `MagickCore.pc'
to the PKG_CONFIG_PATH environment variable
No package 'MagickCore' found
checking for outdated ImageMagick version (<= 6.4.9)... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/home/pi/.rbenv/versions/2.4.1/bin/$(RUBY_BASE_NAME)

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /home/pi/display_sample/vendor/bundle/extensions/armv7l-linux/2.4.0-static/rmagick-2.16.0/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /home/pi/display_sample/vendor/bundle/gems/rmagick-2.16.0 for inspection.
Results logged to /home/pi/display_sample/vendor/bundle/extensions/armv7l-linux/2.4.0-static/rmagick-2.16.0/gem_make.out

An error occurred while installing rmagick (2.16.0), and Bundler cannot continue.
Make sure that `gem install rmagick -v '2.16.0'` succeeds before bundling.

 No package 'MagickCore' found ということでエラーになってしまったので、追加で必要なライブラリをインストールします。

pi@raspberrypi:~/display_sample $ sudo apt-get install libmagickcore-dev libmagickwand-dev

 そして再度 bundle install 実行。

pi@raspberrypi:~/display_sample $ bundle install
Fetching gem metadata from https://rubygems.org/...
Fetching version metadata from https://rubygems.org/..
Resolving dependencies...
Using i2c 0.4.0
Installing rmagick 2.16.0 with native extensions
Using bundler 1.14.6
Installing SSD1306 0.6.1
Bundle complete! 1 Gemfile dependency, 4 gems now installed.
Bundled gems are installed into ./vendor/bundle.

 今度は成功しました。

 ではまずは Hello World ということで、文字を表示してみます。サンプルコードは下記のようにしました。

require 'bundler/setup'
require 'SSD1306'

class Display
  def initialize
    @disp = SSD1306::Display.new
  end

  def execute
    @disp.println('Hello World!!')
    @disp.display!
  end
end

if $0 == __FILE__
  display = Display.new
  display.execute
end

 そして実行します。

pi@raspberrypi:~/display_sample $ bundle exec ruby display.rb

 すると下記のようにディスプレイに文字が表示されます。

f:id:akanuma-hiroaki:20171014221514j:plain:w450

 フォントを大きくしてみます。上記コードの execute メソッドに下記のようにフォントサイズの設定を追加して実行します。

  def execute
    @disp.font_size = 2 # フォントサイズの設定
    @disp.println('Hello World!!')
    @disp.display!
  end

 すると先ほどと比べて縦横が二倍になった大きさで文字が表示されます。

f:id:akanuma-hiroaki:20171014222203j:plain:w450

 次に画像を表示してみます。今回のディスプレイの解像度は 128 x 64 なので、下記のような 128 x 64 のモノクロ画像を用意し、 Raspberry Pi 上に cat.png として配置しておきます。

f:id:akanuma-hiroaki:20171014224726p:plain:w200

 そしてコードは下記のようにしました。 ImageMagick の Image クラスで画像を読み込んで、それを画像表示用メソッドに渡す形になります。

require 'bundler/setup'
require 'SSD1306'

include Magick

class Display

  def initialize
    @disp = SSD1306::Display.new
  end

  def execute
    img = Image.read('/home/pi/display_sample/cat.png').first
    @disp.image(img)
    @disp.display!
  end
end

if $0 == __FILE__
  display = Display.new
  display.execute
end

 これを実行すると下記のような感じで画像が表示されます。比較用に元の画像を並べておきます。

f:id:akanuma-hiroaki:20171014224850j:plain:w450f:id:akanuma-hiroaki:20171014224726p:plain

温度計付き時計の実装

 それではここまでの内容を踏まえて時計を表示させてみたいと思います。ついでに温度センサーを組み合わせて温度も一緒に表示します。温度センサーは以前使ったことのある DS18B20 を使います。モジュールの設定方法などについてはこちらの記事をご参照ください。配線は下記のようにしています。

f:id:akanuma-hiroaki:20171019083901p:plain:w300

 まずは温度計のクラスを下記のように実装します。 DS18B20 からの温度データの読み出し方の詳細については先ほどのリンク先の記事をご覧いただくとして、ここでは割愛します。

require 'bundler/setup'

SENSOR_FILE_PATH = '/sys/bus/w1/devices/28-*/w1_slave'.freeze

# For using thermo sensor DS18B20.
class ThermoMeter
  def initialize
    @device_file_name = Dir.glob(SENSOR_FILE_PATH).first
  end

  def read
    sensor_data = File.read(@device_file_name)
    sensor_data.match(/t=(.*$)/)[1].to_f / 1000
  end
end

 次に SSD1306 を使うためのクラスを下記のように実装します。 SSD1306::Display のメソッドをラップした形です。

require 'bundler/setup'
require 'SSD1306'

include Magick

class Display
  def initialize
    @disp = SSD1306::Display.new
  end

  def font_size(size)
    @disp.font_size = size
  end

  def show
    @disp.display!
  end

  def clear
    @disp.clear!
  end

  def println(str)
    @disp.println(str)
  end

  def print(str)
    @disp.print(str)
  end
end

 そして時計を表示するためのクラスを下記のように実装します。やっていることは単純で、ループを回して一秒ごとに前回ループ時の時刻と今回の時刻が分単位で同じかをチェックし、変わっていれば温度データも取得して画面の表示を更新しています。

require 'bundler/setup'
require './display.rb'
require './thermometer.rb'

class Clock
  TIMEZONE     = 'Asia/Tokyo'
  DATE_FORMAT  = '%Y/%m/%d'
  TIME_FORMAT  = '%H:%M'
  DATE_FONT_PX = 2
  TIME_FONT_PX = 4
  TEMP_FONT_PX = 2

  def initialize
    ENV['TZ'] = 'Asia/Tokyo'
    @display = Display.new
    @thermo_meter = ThermoMeter.new
    @time_str_buffer = ''
  end

  def redisplay_time(date_str, time_str)
    @display.clear
    @display.font_size(DATE_FONT_PX)
    @display.println(date_str)
    @display.font_size(TIME_FONT_PX)
    @display.println(time_str)
    @display.font_size(TEMP_FONT_PX)
    @display.println("#{@thermo_meter.read.round(1)} deg.C")
    @display.show
  end

  def start
    begin
      loop do
        sleep(1)

        now = Time.now.localtime

        date_str = now.strftime(DATE_FORMAT)
        time_str = now.strftime(TIME_FORMAT)

        next if @time_str_buffer == time_str

        @time_str_buffer = time_str
        redisplay_time(date_str, time_str)
      end
    rescue => e
      puts e.backtrace.join("\n")
    ensure
      @display.clear
    end
  end
end

if $PROGRAM_NAME == __FILE__
  clock = Clock.new
  clock.start
end

 これを下記のように実行すると、ディスプレイに現在時刻と気温が表示されます。

pi@raspberrypi:~/display_sample $ bundle exec ruby clock.rb

f:id:akanuma-hiroaki:20171019090020j:plain:w450

まとめ

 液晶ディスプレイが使えるようになるとできることの幅も広がりそうですし、デバッグも便利になりそうです。今回は公開されている Gem をそのまま使っただけなので細かい制御まではできていませんが、表示している内容の一部分だけを書き換えたりできるようになると、レイアウトも柔軟にできそうな気がします。

 今回のコードは下記にも公開しました。

github.com

 おまけで画像の表示サンプルに使った画像の元画像を載せておきます。

f:id:akanuma-hiroaki:20171015002714j:plain:w300

BLE Nano + 焦電センサーで人感センサーを作ってみる

 今回は BLE Nano と焦電センサーを使って人感センサーを作ってみました。焦電センサーは人間等が放出する熱エネルギーに変化が生じた時に発生する赤外線を検知するため、人感センサーとして使うことができます。ただし熱エネルギーの「変化」を検知するので、人がいても動かずじっとしている時には検知されませんので、ずっとそこにいることを検知するというよりは、入室や通過等を検知するのに向いているようです。

DAPLink と接続して配線する

 今回使用している焦電センサーは、焦電型赤外線センサー PaPIRs 5m EKMC1601111 です。

akizukidenshi.com

 また、今回下記サイトを参考に BLE Nano と EKMC1601111 の Fritzing パーツを自作してみました。

Fritzingパーツ作成方法 | Home Made Garbage

Fritzing カスタムパーツの作り方 – jumbleat

そして配線図はこちらです。

f:id:akanuma-hiroaki:20171014115114p:plain

 プログラムの書き込みとシリアル接続でのデバック用に DAPLink と BLE Nano を接続しているので、電源も DAPLink から VIN ピンで取り、VDD ピンから LED と焦電センサーに供給しています。 LED は動作確認用で、焦電センサーが人の動きを感知しているときは点灯させます。 EKMC1601111 には 3本のピンがあり、上記配線図では左から VDD, OUT, GND となっていますので、 OUT は抵抗を噛ませて BLE Nano の P0_5 ピンと接続しています。

ソースコード

 今回実装したコードの全文は下記の通りです。以前の照度センサーの時と大体同じで、焦電センサーの値を扱うカスタムサービスを PyroelectricService.h として実装しています。

#ifndef __BLE_PYROELECTRIC_SERVICE_H__
#define __BLE_PYROELECTRIC_SERVICE_H__

class PyroelectricService {
public:
  const static uint16_t PYROELECTRIC_SERVICE_UUID = 0xA000;
  const static uint16_t PYROELECTRIC_VALUE_CHARACTERISTIC_UUID = 0xA001;

  PyroelectricService(BLE &_ble, bool initialValueForPyroelectricCharacteristic) :
    ble(_ble), pyroelectricState(PYROELECTRIC_VALUE_CHARACTERISTIC_UUID, &initialValueForPyroelectricCharacteristic, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
  {
    GattCharacteristic *charTable[] = {&pyroelectricState};
    GattService pyroelectricService(PYROELECTRIC_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.gattServer().addService(pyroelectricService);
  }

  void updatePyroelectricState(bool newState) {
    ble.gattServer().write(pyroelectricState.getValueHandle(), (uint8_t *)&newState, sizeof(bool));
  }

private:
  BLE &ble;
  ReadOnlyGattCharacteristic<bool> pyroelectricState;
};

#endif

 そして main.cpp から PyroelectricSerivce.h を使用しています。

#include "mbed.h"
#include "ble/BLE.h"
#include "PyroelectricService.h"

Serial pc(USBTX, USBRX);
DigitalOut led(P0_4, 0);
AnalogIn pyro(P0_5);

const static char DEVICE_NAME[] = "PyroSample";
static const uint16_t uuid16_list[] = {PyroelectricService::PYROELECTRIC_SERVICE_UUID};

static volatile bool triggerSensorPolling = false;

unsigned short pyroValue;
const int threshold = 500;

bool pyroelectricState;

PyroelectricService *pyroelectricServicePtr;

Ticker ticker;

BLE ble;

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
  BLE::Instance().gap().startAdvertising();
}

void periodicCallback(void)
{
  pyroValue = pyro.read_u16();
  pyroelectricState = pyroValue >= threshold;
  if (pyroelectricState) {
    led = 1;
  } else {
    led = 0;
  }

  triggerSensorPolling = true;
}

void onBleInitError(BLE &ble, ble_error_t error)
{
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
  BLE& ble = params->ble;
  ble_error_t error = params->error;

  if (error != BLE_ERROR_NONE) {
    onBleInitError(ble, error);
    return;
  }

  if (ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
    return;
  }

  ble.gap().setDeviceName((const uint8_t *) DEVICE_NAME);
  ble.gap().onDisconnection(disconnectionCallback);

  bool initialValueForPyroelectricCharacteristic = false;
  pyroelectricServicePtr = new PyroelectricService(ble, initialValueForPyroelectricCharacteristic);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
  ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
  ble.gap().setAdvertisingInterval(1000);
  ble.gap().startAdvertising();
}

int main(void)
{
  pc.printf("Starting BLE_PYROELECTRIC...\r\n");

  ticker.attach(periodicCallback, 1);

  pc.printf("Initializing BLE Controller...\r\n");
  ble.init(bleInitComplete);
  pc.printf("Initialized.\r\n");

  while (ble.hasInitialized() == false) { /* spin loop */ }

  pc.printf("Starting loop...\r\n");
  while(true) {
    if (triggerSensorPolling) {
      triggerSensorPolling = false;
      pc.printf("%d\r\n", pyroelectricState);
      pyroelectricServicePtr->updatePyroelectricState(pyroelectricState);
    } else {
      ble.waitForEvent();
    }
  }
}

 焦電センサーからの出力はアナログ入力として受け取り、入力値が閾値を上回ったかどうかでステータスを切り替えています。

AnalogIn pyro(P0_5);
void periodicCallback(void)
{
  pyroValue = pyro.read_u16();
  pyroelectricState = pyroValue >= threshold;
  if (pyroelectricState) {
    led = 1;
  } else {
    led = 0;
  }

  triggerSensorPolling = true;
}

 最初はデジタル入力として試していたのですが、一度検知して true を返すようになると、その後正しく false に戻らなかったため、アナログ入力の値を使うようにしてみました。 BLE のカスタムサービスで扱う値が bool になったという以外は、基本的には照度センサーを使用した時と同様の構成です。

動作確認

 ではプログラムをコンパイルして書き込んで、動作確認してみます。

[vagrant@localhost BLE_PYROELECTRIC]$ mbed compile
Building project BLE_PYROELECTRIC (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: mbed
Scan: env
Compile [100.0%]: main.cpp
Link: BLE_PYROELECTRIC
Elf2Bin: BLE_PYROELECTRIC
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |   127 |     3 |   47 |
| Misc      | 41520 |   141 | 1649 |
| Subtotals | 41647 |   144 | 1696 |
+-----------+-------+-------+------+
Allocated Heap: 2256 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 1840 bytes
Total RAM memory (data + bss + heap + stack): 6144 bytes
Total Flash memory (text + data + misc): 41791 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_PYROELECTRIC.hex
[vagrant@localhost BLE_PYROELECTRIC]$ 
[vagrant@localhost BLE_PYROELECTRIC]$ cp ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_PYROELECTRIC.hex /run/media/vagrant/DAPLINK/
[vagrant@localhost BLE_PYROELECTRIC]$ 

 するとシリアル接続していると下記のように、人の動きを検知しているときは 1 を、検知していないときは 0 を出力します。

Starting BLE_PYROELECTRIC...
Initializing BLE Controller...
Initialized.
Starting loop...
0
0
0
0
0
0
0
0
1
1
1
1
1
0
0

 また、 LightBlue でも下記のように Peripheral のリストに PyroSample が表示され、接続して Notification を Listen すると値が更新されていくのが確認できます。

f:id:akanuma-hiroaki:20171009235322p:plain:w300 f:id:akanuma-hiroaki:20171009235347p:plain:w300

ボタン電池で動かす

 動作確認ができたので、実際にセンサーとして使う場合を想定して、DAPLink から切り離してボタン電池で動かしてみます。配線は下記のようにしました。

f:id:akanuma-hiroaki:20171014133706p:plain

 CR2032 のボタン電池ホルダーを配置し、+側から BLE Nano の VIN ピンに給電しています。ボタン電池をはめればすぐに起動して動き始めます。ちなみに「人感」センサーと言ってますが猫でもちゃんと検知します。

[f:id:akanuma-hiroaki:20171014135212j:plain:450]

まとめ

 冒頭でも書きましたが、焦電センサーは動きを検知することになるため、そこに人間や動物がいても、ほとんど動いてなければ検知されません。猫が昼寝スポットにいるかどうかを検知するものでも作ってみようかと思ってたのですが、焦電センサーだとその用途にはマッチしないようです。

f:id:akanuma-hiroaki:20171014140347j:plain:w300:left

 また、動作確認をしていると、実際はセンサー前で動き続けていても時々検知結果が false になるので、在室検知のようなことをするとしたら、状態の管理としてはある程度 false の時間が続いたら false と判定するのが良さそうです。

Raspberry Pi とモーターを繋げて Ruby で動かす

 今までは Raspberry Pi と他のセンサー類を組み合わせてセンサー値を読み取り、データとして使える形にはしていましたが、ハードウェアのアウトプットとしては LED を光らせるぐらいでした。今回はモーターを動かすことに挑戦してみたいと思います。

電池ボックスとモーターの配線準備

 今回は下記のモーターと電池ボックスを使用します。

akizukidenshi.com

akizukidenshi.com

 いずれもリード線はついていますが、そのままではブレッドボードにさすには不便なので、下記サイトを参考にリード線を加工しました。

ブレッドボードで電子回路の実験をするために乾電池ホルダーのリード線をジャンパワイヤのように端子化するmatsumotoyoshio.wordpress.com

 加工に使った材料は下記の二つです。

f:id:akanuma-hiroaki:20171001150412j:plain:w450

 最終的に下記のようにリード線の先端にピンがついた形になりました。

f:id:akanuma-hiroaki:20170930084942j:plain:w300

ブレッドボードで配線する

 今回モーターを回すためにさらに必要なものとして、下記のモータードライバ(TA7291P)を使用しました。

f:id:akanuma-hiroaki:20170930085035j:plain:w300

 これらを動作確認用の LED と組み合わせて、下記の図のように配線します。ちなみに TA7291P の Fritzing 用の画像は下記サイトのものを使用させていただきました。

無料ダウンロード:Fritzing 部品一覧

f:id:akanuma-hiroaki:20171001203824p:plain

 モータードライバの各PINは下記のように接続しています。

  1. GND
  2. モーターの片方の端子
  3. 未接続
  4. Vref. Raspberry Pi の 18番PIN
  5. IN1. Raspberry pi の 27番PIN
  6. IN2. Raspberry Pi の 4番PIN
  7. Raspberry Pi の 5V 電源出力
  8. 電池ボックスの+側
  9. 未接続
  10. モーターのもう一方の端子

 TA7291P の4番PINはモーターへの出力調整用なので、電圧を調整できるように Raspberry Pi の PWM対応ピンに接続しています。

モーターを回すサンプルコード

 Ruby で GPIO を操作するために pi_piper を使っていますが、 PWM を使う場合は pi_piper のPwmクラスを使用します。

http://www.rubydoc.info/github/jwhitehorn/pi_piper/PiPiper/Pwm

 まずはサンプルコードの全体を掲載します。

require 'bundler/setup'
require 'pi_piper'

PIN_VREF = 18
PIN_IN_1 = 27
PIN_IN_2 =  4
PIN_LED  = 17 

class MotorSample
  def initialize
    puts 'Initializing MotorSample...'
    @pin_led  = PiPiper::Pin.new(pin: PIN_LED,  direction: :out)
    @pin_in_1 = PiPiper::Pin.new(pin: PIN_IN_1, direction: :out)
    @pin_in_2 = PiPiper::Pin.new(pin: PIN_IN_2, direction: :out)
    @pin_vref = PiPiper::Pwm.new(pin: PIN_VREF)
    @pin_vref.value = 0
  end

  def stop_motor
    puts 'Stopping.'
    @pin_led.off
    @pin_in_1.off
    @pin_in_2.off
    sleep 1
  end

  def toggle_led(i)
    i % 2 == 0 ? @pin_led.on : @pin_led.off
  end

  def speeding_up
    puts 'Speeding Up...'
    0.step(1.0, 0.1) do |v|
      toggle_led((v * 10).to_i)
      @pin_vref.value = v
      sleep 2
    end
  end

  def slowing_down
    puts 'Slowing Down...'
    1.0.step(0, -0.1) do |v|
      toggle_led((v * 10).to_i)
      @pin_vref.value = v
      sleep 2
    end
  end

  def execute
    puts 'Rolling Forward.'
    @pin_in_1.on
    @pin_in_2.off
    speeding_up
    slowing_down

    stop_motor

    puts 'Rolling Backward.'
    @pin_in_1.off
    @pin_in_2.on
    speeding_up
    slowing_down
  end
end

if $0 == __FILE__
  motor_sample = MotorSample.new
  motor_sample.execute
end

 上記のサンプルコードを動かすと、モーターが順方向へ回転し始めて徐々に速度を上げ、最高速度に到達すると徐々に速度を下げていきます。そして今度は逆方向へ同様のことを行って終了します。

 ON/OFF の切り替えで良いピンは PiPiper::Pin を new していますが、 PWM を使いたいピンについては PiPiper::Pwm を new しています。

  def initialize
    puts 'Initializing MotorSample...'
    @pin_led  = PiPiper::Pin.new(pin: PIN_LED,  direction: :out)
    @pin_in_1 = PiPiper::Pin.new(pin: PIN_IN_1, direction: :out)
    @pin_in_2 = PiPiper::Pin.new(pin: PIN_IN_2, direction: :out)
    @pin_vref = PiPiper::Pwm.new(pin: PIN_VREF)
    @pin_vref.value = 0
  end

 TA7291P ではモーターの ON/OFF/回転方向 の切り替えは IN1 と IN2 の ON/OFF の組み合わせて決定します。

  • IN1: OFF IN2: OFF => 停止
  • IN1: ON IN2: OFF => 順方向へ回転
  • IN1: OFF IN2: ON => 逆方向へ回転

 例えば順方向へ回転させるには下記のように指定しています。

    @pin_in_1.on
    @pin_in_2.off

 PWM を使用しているピンについてはその値を 0 〜 1.0 で指定することで出力を調整できます。今回は例えば下記のように徐々に値を増加させ、モーターの回転速度を上げていったりしています。

  def speeding_up
    puts 'Speeding Up...'
    0.step(1.0, 0.1) do |v|
      toggle_led((v * 10).to_i)
      @pin_vref.value = v
      sleep 2
    end
  end

Beacon の電波強度に応じてモーターの回転速度を調節する

 指定した速度で動かすだけでは面白くないので、今度は BLE と組み合わせ、 Beacon の電波強度(RSSI)に応じてモーターの回転速度を変化させてみたいと思います。

 まずはモーターの動作をまとめた Motor クラスを作成します。内容としては単純に各ピンの初期化と、 ON/OFF、速度調節の値を設定する機能をまとめたものです。

require 'bundler/setup'
require 'pi_piper'

class Motor
  def initialize(pin_vref, pin_in_1, pin_in_2)
    @pin_in_1 = PiPiper::Pin.new(pin: pin_in_1, direction: :out)
    @pin_in_2 = PiPiper::Pin.new(pin: pin_in_2, direction: :out)
    @pin_vref = PiPiper::Pwm.new(pin: pin_vref)
    @pin_vref.value = 0
  end

  def stop
    @pin_in_1.off
    @pin_in_2.off
  end

  def forward
    @pin_in_1.on
    @pin_in_2.off
  end

  def backward
    @pin_in_1.off
    @pin_in_2.on
  end

  def adjust(value)
    @pin_vref.value = value
  end
end

 BLE で Beacon の Advertisement を受け取って Motor を操作するコードは下記のようにしました。今回は Beacon として以前にも使ったことのある、 Texas Instruments の SensorTag を使用しています。

require 'bundler/setup'
require 'dbus'
require './motor.rb'

class MotorTest
  SERVICE_NAME = 'org.bluez'
  SERVICE_PATH = '/org/bluez'
  ADAPTER = 'hci0'
  DEVICE_IF = 'org.bluez.Device1'
  DBUS_PROPERTIES_IF = 'org.freedesktop.DBus.Properties'
  PROPERTIES_CHANGED_SIGNAL = 'PropertiesChanged'
  DEVICE_NAME = 'CC2650'

  PIN_VREF = 18
  PIN_IN_1 = 27
  PIN_IN_2 =  4
  PIN_LED  = 17

  def initialize
    @bus = DBus::system_bus
    @bluez = @bus.service(SERVICE_NAME)

    @adapter = @bluez.object("#{SERVICE_PATH}/#{ADAPTER}")
    @adapter.introspect

    @motor = Motor.new(PIN_VREF, PIN_IN_1, PIN_IN_2)
    @motor.forward

    @led = PiPiper::Pin.new(pin: PIN_LED, direction: :out)
  end

  def execute
    @adapter.SetDiscoveryFilter({'Transport' => 'le'})
    @adapter.StartDiscovery

    @led.on

    target_device = nil

    while(target_device.nil?)
      @adapter.introspect
      @adapter.subnodes.each do |node|
        device = @bluez.object("#{SERVICE_PATH}/#{ADAPTER}/#{node}")
        device.introspect

        next unless device.respond_to?(:GetAll)

        properties = device.GetAll(DEVICE_IF).first
        name = properties['Name']
        rssi = properties['RSSI']

        next if name.nil?
        next unless name.downcase.include?(DEVICE_NAME.downcase)

        target_device = device
        break
      end

      sleep(0.1)
    end

    target_device.default_iface = DBUS_PROPERTIES_IF
    target_device.on_signal(PROPERTIES_CHANGED_SIGNAL) do |_, v|
      rssi = v['RSSI']
      vref_value = [[(100 + rssi).to_f / 100, 1.0].min, 0].max
      puts "RSSI: #{rssi} VREF: #{vref_value}"
      @motor.forward
      @motor.adjust(vref_value)
    end

    main = DBus::Main.new
    main << @bus

    main.run
  end
end

if $0 == __FILE__
  motor_test = MotorTest.new
  motor_test.execute
end

 BLE の操作については DBus と BlueZ を使い、 initialize で Bluetooth アダプタの初期化と Motor クラスの初期化を行っています。

  def initialize
    @bus = DBus::system_bus
    @bluez = @bus.service(SERVICE_NAME)

    @adapter = @bluez.object("#{SERVICE_PATH}/#{ADAPTER}")
    @adapter.introspect

    @motor = Motor.new(PIN_VREF, PIN_IN_1, PIN_IN_2)
    @motor.forward

    @led = PiPiper::Pin.new(pin: PIN_LED, direction: :out)
  end

 execute メソッドの中でBLEデバイスのスキャンを行っていますが、検索対象を絞るために SetDiscoveryFilter で BLE デバイスに限定しています。

    @adapter.SetDiscoveryFilter({'Transport' => 'le'})
    @adapter.StartDiscovery

 RSSI が変わると PropertiesChanged シグナルが送られてくるので、シグナルを受け取った場合にモーターの速度を変更するようにしています。 PWM ピンに設定できる値は 0 〜 1.0 なので、その範囲を出ないようにしています。今回は RSSI を厳密に取り扱うことが目的ではないので、上限と下限を超える場合は超える分をカットしている形です。

    target_device.default_iface = DBUS_PROPERTIES_IF
    target_device.on_signal(PROPERTIES_CHANGED_SIGNAL) do |_, v|
      rssi = v['RSSI']
      vref_value = [[(100 + rssi).to_f / 100, 1.0].min, 0].max
      puts "RSSI: #{rssi} VREF: #{vref_value}"
      @motor.forward
      @motor.adjust(vref_value)
    end

 このコードを動かすと、 SensorTag を Raspberry Pi に近づけるとモーターの回転速度が上がり、遠ざけると回転速度が下がります。SensorTag を動かしてからシグナルが送られるまでにタイムラグがあるのですが、とりあえずイメージした感じにはなっているので、今後調べられそうであればシグナル発行のタイミングも調べてみたいと思います。

まとめ

 今回初めて外部電源(電池ボックス)を使ってモーターを動かすことに挑戦してみました。実は途中で一度、横に置いておいた電池ボックスのリード線がいつの間にかショートして、電池ボックスを一つ焦がしてダメにしてしまい、やっぱり電気は怖いなぁと改めて思ったのですが、モーターなどを動かすことができると色々とやれる幅も広がって楽しいですね。モータードライバなどは物によって使い方が違うので、データシートなどから読み解くのは少し大変ですが、色々触って試してみたいと思います。

BLE Nano を光センサー CdS と組み合わせる

 前回までは BLE Nano をそのまま USB のライター DAPLink にさして使っていましたが、このままだと他のセンサー類と組み合わせることができないので、 BLE Nano をブレッドボードにさして使ってみます。まずは BLE Nano をブレッドボードにさして動作させられることを確認し、その後実際のセンサーと組み合わせて値を取得してみます。

ブレッドボードで配線

 写真だとちょっと分かりづらいですが、今回は下記のように配線しました。基本的には BLE Nano をブレッドボードにさして、DAPLink の対応するピンとオス-オスのジャンパーワイヤーで接続しています。

f:id:akanuma-hiroaki:20170918144703j:plain:w450 f:id:akanuma-hiroaki:20170918144714j:plain:w450

 今回は下記のピンを接続しました。

  • SWCLK
  • SWDIO
  • VIN
  • GND
  • TXD
  • RXD

 シリアル接続でのデバッグ等を行わなければ TXD, RXD は不要かもしれません。

 また、今回は LED と抵抗を接続して、前回のプログラムで BLE Nano のオンボードLEDを点滅させていたところを、ブレッドボード上の LED を点滅させてみます。 BLE Nano の P0_4 ピンから 1kΩ の抵抗を経由して LED のアノード側に接続し、カソード側と BLE Nano の GND ピンに接続しています。

ソースコードを変更してコンパイル&実行

 前回実装したコードの LED のピンの指定を下記のように変更します。

DigitalOut led(LED1, 0);
 ↓
DigitalOut led(P0_4, 0);

 そして mbed compile して hex ファイルを BLE Nano に転送すると、ブレッドボード上の LED が点滅を開始し、 LightBlue などでは前回同様にデバイスが検知され、 Characteristic の値が確認できます。

ボタン電池で動かす

 開発・デバッグ時は DAPLink に接続しているので電源もそこから取っていますが、実際に何か BLE デバイスを作成した時には単体で動くようにする必要があるので、ボタン電池で動かしてみます。配線は下記の写真のようにしました。

f:id:akanuma-hiroaki:20170923015011j:plain:w450

 DAPLink の VIN と GND にさしていたピンをそれぞれボタン電池ホルダーの + 側と - 側に繋いで、それ以外のコードを取り外しています。今回はスイッチなどはつけてないので、ボタン電池をつけるとすぐにプログラムが動き始めます。 LightBlue などで接続してみると今まで同様に動作していることが確認できます。

実際のセンサーと組み合わせる

 先ほどまではダミーの値をセンサー値として使用していましたが、今度は実際のセンサーと組み合わせてみます。今回は下記の光センサー CdSセルを使用してみました。

www.aitendo.com

 これを 4.7kΩの抵抗と組み合わせて下記の写真のように配線しました。

f:id:akanuma-hiroaki:20170923015435j:plain:w450 f:id:akanuma-hiroaki:20170923015458j:plain:w450

 コードも実際のセンサー値を使用するように変更します。 main.cpp は下記のようにしました。

#include "mbed.h"
#include "ble/BLE.h"
#include "LuxService.h"

Serial pc(USBTX, USBRX);
DigitalOut led(P0_4, 0);
AnalogIn cds(P0_5);

const static char DEVICE_NAME[] = "LUXSample";
static const uint16_t uuid16_list[] = {LuxService::LUX_SERVICE_UUID};

static volatile bool triggerSensorPolling = false;

unsigned short luxValue;
const int threshold = 500;

LuxService *luxServicePtr;

Ticker ticker;

BLE ble;

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
  BLE::Instance().gap().startAdvertising();
}

void periodicCallback(void)
{
  luxValue = cds.read_u16();
  if (luxValue >= threshold) {
    led = 0;
  } else {
    led = 1;
  }

  triggerSensorPolling = true;
}

void onBleInitError(BLE &ble, ble_error_t error)
{
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
  BLE& ble = params->ble;
  ble_error_t error = params->error;

  if (error != BLE_ERROR_NONE) {
    onBleInitError(ble, error);
    return;
  }

  if (ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
    return;
  }

  ble.gap().setDeviceName((const uint8_t *) DEVICE_NAME);
  ble.gap().onDisconnection(disconnectionCallback);

  uint16_t initialValueForLUXCharacteristic = 100;
  luxServicePtr = new LuxService(ble, initialValueForLUXCharacteristic);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
  ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
  ble.gap().setAdvertisingInterval(1000);
  ble.gap().startAdvertising();
}

int main(void)
{
  pc.baud(115200);
  pc.printf("Starting BLE_LUX Sample...\r\n");

  ticker.attach(periodicCallback, 1);

  pc.printf("Initializing BLE Controller...\r\n");
  ble.init(bleInitComplete);
  pc.printf("Initialized.\r\n");

  while (ble.hasInitialized() == false) { /* spin loop */ }

  pc.printf("Starting loop...\r\n");
  while (true) {
    if (triggerSensorPolling && ble.getGapState().connected) {
      triggerSensorPolling = false;
      pc.printf("%u\r\n", luxValue);
      luxServicePtr->updateLuxValue(luxValue);
    } else {
      ble.waitForEvent();
    }
  }
}

 今までダミーの値を生成していた部分を削除し、実際のセンサーデータを読み出す処理を追加しています。 CdSセルからのデータはアナログ入力として受け取ります。また、今までは一秒間隔で点滅させていたLEDを、センサーデータが閾値より小さかったら(暗かったら)点灯し、大きかったら(明るかったら)消すという処理にしています。

void periodicCallback(void)
{
  luxValue = (cds.read_u16() & 0xffff);
  if (luxValue >= threshold) {
    led = 0;
  } else {
    led = 1;
  }

  triggerSensorPolling = true;
}

 LuxService.h の方も全体を掲載しておきますが、前回との変更は受け取るセンサー値の型を unsigned short に変更しただけです。

#ifndef __BLE_LUX_SERVICE_H__
#define __BLE_LUX_SERVICE_H__

class LuxService {
public:
  const static uint16_t LUX_SERVICE_UUID = 0xA000;
  const static uint16_t LUX_VALUE_CHARACTERISTIC_UUID = 0xA001;

  LuxService(BLE &_ble, uint16_t initialValueForLUXCharacteristic) :
    ble(_ble), luxValue(LUX_VALUE_CHARACTERISTIC_UUID, &initialValueForLUXCharacteristic, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
  {
    GattCharacteristic *charTable[] = {&luxValue};
    GattService luxService(LUX_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.gattServer().addService(luxService);
  }

  void updateLuxValue(unsigned short newValue) {
    ble.gattServer().write(luxValue.getValueHandle(), (uint8_t *)&newValue, sizeof(bool));
  }

private:
  BLE &ble;
  ReadOnlyGattCharacteristic<uint16_t> luxValue;
};

#endif

 これで実際のセンサーの計測値をBLEで受け取ることができるようになります。 LightBlue で見てみると下記のように値が取得できます。

f:id:akanuma-hiroaki:20170923134018p:plain:w300

まとめ

 BLE Nano とボタン電池を使うことで簡単にワイヤレスで動作するデバイスが試作できました。どんなセンサーと組み合わせて何をするかはアイディア次第だと思いますので、日常的に役に立ちそうなものを考えてプロトタイプを作ってみたいと思います。

 ちなみに BLE Nano は日本でも V2 が発売されましたね。スペックも上がってるようですし、 DAPLink のバージョンも 1.0 から 1.5 に上がっているようです。DAPLink 1.0 でも BLE Nano V2 が使えるのかなどは気になるところです。

mag.switch-science.com

BLE Nano で Advertisement & カスタムサービス実装

 前回はとりあえずサンプルコードで BLE Nano を iBeacon として動かしましたが、今回は Peripheral として Advertisement を送信する例と、カスタムサービスを定義して Central との通信を行う例を実装してみたいと思います。

Advertisement 送信

 まずは Advertisement に独自の情報を載せて送信してみます。下記サンプルコードをベースにしています。

BLE_GAP_Example - a mercurial repository | Mbed

 ただ上記サンプルを mbed import でインポートしてビルドしたところエラーになってしまったので、新規にプロジェクトを作って実装してみることにしました。

 まずは mbed new でプロジェクトを作成します。デフォルトだと mbed os 5 向けとして作成されますが、 BLE Nano では mbed os 5 に対応していないようなので、 mbed os 2 向けとして作成するために、 --mbedlib オプションをつけて作成します。

[vagrant@localhost vagrant]$ mbed new BLE_GAP_EXAMPLE --mbedlib                                                       
[mbed] Creating new program "BLE_GAP_EXAMPLE" (git)                                                                   
[mbed] Adding library "mbed" from "https://mbed.org/users/mbed_official/code/mbed/builds" at latest revision in the current branch                                                                                                          
[mbed] Updating reference "mbed" -> "https://mbed.org/users/mbed_official/code/mbed/builds/tip"                       
[mbed] Couldn't find build tools in your program. Downloading the mbed 2.0 SDK tools...                               
[vagrant@localhost vagrant]$ 
[vagrant@localhost vagrant]$ cd BLE_GAP_EXAMPLE/
[vagrant@localhost BLE_GAP_EXAMPLE]$            
[vagrant@localhost BLE_GAP_EXAMPLE]$ ls         
mbed  mbed.bld  mbed_settings.py                

 とりあえず toolchain と target を指定しておきます。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed detect                                     
                                                                                     
[mbed] Detected RBLAB_BLENANO, port /dev/ttyACM0, mounted /run/media/vagrant/DAPLINK 
[mbed] Supported toolchains for RBLAB_BLENANO                                        
+--------+-----------+-----------+-----+---------+-----+                             
| Target | mbed OS 2 | mbed OS 5 | ARM | GCC_ARM | IAR |                             
+--------+-----------+-----------+-----+---------+-----+                             
+--------+-----------+-----------+-----+---------+-----+                             
Supported targets: 0                                                                 
                                                                                     
[vagrant@localhost BLE_GAP_EXAMPLE]$                                                 
[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed toolchain GCC_ARM                          
[mbed] GCC_ARM now set as default toolchain in program "BLE_GAP_EXAMPLE"             
[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed target RBLAB_BLENANO                       
[mbed] RBLAB_BLENANO now set as default target in program "BLE_GAP_EXAMPLE"          

 ライブラリを更新します。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed deploy
[mbed] Updating library "mbed" to branch tip
[mbed] Downloading library build "675da3299148" (might take a minute)
[mbed] Unpacking library build "675da3299148" in "/vagrant/BLE_GAP_EXAMPLE/mbed"
[mbed] Updating the mbed 2.0 SDK tools...

 BLE の Firmware 開発をするには mbed の BLE API が必要なので、 mbed add でライブラリを追加します。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed add http://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API/
[mbed] Adding library "BLE_API" from "https://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API" at latest revision in the current branch
[mbed] Updating reference "BLE_API" -> "https://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API/#65474dc93927"

 nRF51822 のライブラリも必要なので mbed add しておきます。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed add https://mbed.org/teams/Nordic-Semiconductor/code/nRF51822/
[mbed] Adding library "nRF51822" from "https://mbed.org/teams/Nordic-Semiconductor/code/nRF51822" at latest revision in the current branch
[mbed] Updating reference "nRF51822" -> "https://mbed.org/teams/Nordic-Semiconductor/code/nRF51822/#c90ae1400bf2"

 そして main.cpp はサンプルコードを参考に下記のように実装します。

#include "mbed.h"
#include "ble/BLE.h"

BLE ble;

const static char DEVICE_NAME[] = "SampleDevice";
const static uint8_t AdvData[] = {"SampleAdvertisement"};

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
  BLE::Instance().gap().startAdvertising();
}

void onBleInitError(BLE &ble, ble_error_t error)
{
  (void) ble;
  (void) error;
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
  BLE &ble          = params->ble;
  ble_error_t error = params->error;

  if (error != BLE_ERROR_NONE) {
    onBleInitError(ble, error);
    return;
  }

  if (ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
    return;
  }

  ble.gap().setDeviceName((const uint8_t *) DEVICE_NAME);
  ble.gap().onDisconnection(disconnectionCallback);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::MANUFACTURER_SPECIFIC_DATA, AdvData, sizeof(AdvData));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *) DEVICE_NAME, sizeof(DEVICE_NAME));

  ble.gap().setAdvertisingInterval(100);
  ble.gap().startAdvertising();
}

int main(void)
{
  ble.init(bleInitComplete);

  while (true) {
    ble.waitForEvent();
  }
}

 今回独自のデータとして使用するのは下記のデバイス名とアドバタイズデータです。

const static char DEVICE_NAME[] = "SampleDevice";
const static uint8_t AdvData[] = {"SampleAdvertisement"};

 bleInitComplete() の中で上記データを Advertisement に設定しています。

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::MANUFACTURER_SPECIFIC_DATA, AdvData, sizeof(AdvData));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *) DEVICE_NAME, sizeof(DEVICE_NAME));

 これをコンパイルして BLE Nano にコピーします。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed compile
Building project BLE_GAP_EXAMPLE (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: env
Scan: mbed
Compile [100.0%]: main.cpp
Link: BLE_GAP_EXAMPLE
Elf2Bin: BLE_GAP_EXAMPLE
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |   112 |     3 |   31 |
| Misc      | 33115 |   141 | 1189 |
| Subtotals | 33227 |   144 | 1220 |
+-----------+-------+-------+------+
Allocated Heap: 2728 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 1364 bytes
Total RAM memory (data + bss + heap + stack): 6140 bytes
Total Flash memory (text + data + misc): 33371 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_GAP_EXAMPLE.hex
[vagrant@localhost BLE_GAP_EXAMPLE]$ 
[vagrant@localhost BLE_GAP_EXAMPLE]$ cp ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_GAP_EXAMPLE.hex /run/media/vagrant/DAPLINK/
[vagrant@localhost BLE_GAP_EXAMPLE]$ 

 すると Advertisement の送信が始まりますので、 iOS アプリの LightBlue などで確認すると、下記のように表示されます。

f:id:akanuma-hiroaki:20170917120410p:plain:w300 f:id:akanuma-hiroaki:20170917120439p:plain:w300

 Manufacturer Data には “SampleAdvertisement” という文字列が16進数変換されたものが表示されています。

独自サービス実装

 mbed の BLE API では下記ページに記載されている BLE Service はサポートされていますので、すぐに利用することができます。

BLE services supported on mbed - | Mbed

 例えば Heart Rate Service を使うサンプルに下記のようなものがあります。

BLE_HeartRate - a mercurial repository | Mbed

 また、カスタムのサービスを実装している例として下記のようなサンプルがあります。

BLE_Button - a mercurial repository | Mbed

 これらを参考にしてカスタムサービスを使う例を実装してみました。照度センサーからの値を扱うケースを想定して、 LuxService を実装して利用する例です。まず LuxService.h を下記のような内容で実装します。

#ifndef __BLE_LUX_SERVICE_H__
#define __BLE_LUX_SERVICE_H__

class LuxService {
public:
  const static uint16_t LUX_SERVICE_UUID = 0xA000;
  const static uint16_t LUX_VALUE_CHARACTERISTIC_UUID = 0xA001;

  LuxService(BLE &_ble, uint16_t initialValueForLUXCharacteristic) :
    ble(_ble), luxValue(LUX_VALUE_CHARACTERISTIC_UUID, &initialValueForLUXCharacteristic, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
  {
    GattCharacteristic *charTable[] = {&luxValue};
    GattService luxService(LUX_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.gattServer().addService(luxService);
  }

  void updateLuxValue(uint16_t newValue) {
    ble.gattServer().write(luxValue.getValueHandle(), (uint8_t *)&newValue, sizeof(bool));
  }

private:
  BLE &ble;
  ReadOnlyGattCharacteristic<uint16_t> luxValue;
};

#endif

 照度の値を扱うため、 private の変数として読み取り専用の Characteristic を luxValue として定義します。

private:
  BLE &ble;
  ReadOnlyGattCharacteristic<uint16_t> luxValue;

 コンストラクタでその Characteristic を初期化します。 Characteristic の値が更新された時に Notification を送れるように、初期化時に GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY を渡しています。そして Service 初期化時に Characteristic を登録します。 最後に Service を GATT Server に登録します。

  LuxService(BLE &_ble, uint16_t initialValueForLUXCharacteristic) :
    ble(_ble), luxValue(LUX_VALUE_CHARACTERISTIC_UUID, &initialValueForLUXCharacteristic, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
  {
    GattCharacteristic *charTable[] = {&luxValue};
    GattService luxService(LUX_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.gattServer().addService(luxService);
  }

 さらに照度の値の更新用のメソッドとして void updateLuxValue(uint16_t newValue) を実装しておきます。

  void updateLuxValue(uint16_t newValue) {
    ble.gattServer().write(luxValue.getValueHandle(), (uint8_t *)&newValue, sizeof(bool));
  }

 では LuxService を利用する main.cpp の実装です。コード全体は下記のようになります。

#include "mbed.h"
#include "ble/BLE.h"
#include "LuxService.h"

DigitalOut led(LED1, 0);

const static char DEVICE_NAME[] = "LUXSample";
static const uint16_t uuid16_list[] = {LuxService::LUX_SERVICE_UUID};

static volatile bool triggerSensorPolling = false;

uint16_t luxValue = 100;
static volatile bool riseValue = true;

LuxService *luxServicePtr;

Ticker ticker;

BLE ble;

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
  BLE::Instance().gap().startAdvertising();
}

void periodicCallback(void)
{
  led = !led;
  triggerSensorPolling = true;
}

void onBleInitError(BLE &ble, ble_error_t error)
{
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
  BLE& ble = params->ble;
  ble_error_t error = params->error;

  if (error != BLE_ERROR_NONE) {
    onBleInitError(ble, error);
    return;
  }

  if (ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
    return;
  }

  ble.gap().setDeviceName((const uint8_t *) DEVICE_NAME);
  ble.gap().onDisconnection(disconnectionCallback);

  uint16_t initialValueForLUXCharacteristic = 100;
  luxServicePtr = new LuxService(ble, initialValueForLUXCharacteristic);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
  ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
  ble.gap().setAdvertisingInterval(1000);
  ble.gap().startAdvertising();
}

int main(void)
{
  ticker.attach(periodicCallback, 1);

  ble.init(bleInitComplete);

  while (ble.hasInitialized() == false) { /* spin loop */ }

  while (true) {
    if (triggerSensorPolling && ble.getGapState().connected) {
      triggerSensorPolling = false;

      if (riseValue) {
        luxValue++;
      } else {
        luxValue--;
      }

      if (luxValue == 200) {
        riseValue = false;
      } else if (luxValue == 100) {
        riseValue = true;
      }

      luxServicePtr->updateLuxValue(luxValue);
    } else {
      ble.waitForEvent();
    }
  }
}

 カスタムサービスに関連するところをピックアップして簡単に説明します。まずは先ほど実装した LuxService を include します。

#include "LuxService.h"

 UUID のリストは LuxService で定義されている UUID を使用して作成します。

static const uint16_t uuid16_list[] = {LuxService::LUX_SERVICE_UUID};

 bleInitComplete の中で LuxService を初期化します。

  uint16_t initialValueForLUXCharacteristic = 100;
  luxServicePtr = new LuxService(ble, initialValueForLUXCharacteristic);

 あとは main() の中で、センサーから取得した値(今回は擬似的に変化させている値)で LuxService の値を更新します。

      luxServicePtr->updateLuxValue(luxValue);

 そしてコンパイルして BLE Nano に hex ファイルをコピーし、動作を確認してみます。 LightBlue で確認すると下記のようにデバイスがリストに表示されます。

f:id:akanuma-hiroaki:20170917235501p:plain:w300

 デバイスを選択して接続すると、プロパティに Read と Notify が表示されています。

f:id:akanuma-hiroaki:20170917235622p:plain:w300

 Characteristic を選択して Notification の Listen を開始すると、毎秒照度の値が更新される度に値が表示されていきます。

f:id:akanuma-hiroaki:20170917235743p:plain:w300

まとめ

 ひとまずサンプルコードを頼りに見よう見まねで Advertisement の送信と、カスタムサービスを利用する例を実装してみました。細かいところはまだ理解できていないですが、とりあえず動かせるということはわかったので、詳細は順次確認しつつ、実際にセンサーと組み合わせて処理ができるようにしてみたいと思います。

BLE Nano を mbed で iBeacon 化する

 前回 BLE Nano で Lチカまでやったので、今回は BLE Nano を iBeacon 化して、iOS アプリから検知してみたいと思います。

 また、開発環境としては前回の記事で、 Vagrant で構築した VM からだと mbed import の途中で止まってしまったと書きましたが、 Mac 上の環境と同様で時間がかかるものの、待てば正常に進んだので、今回は Vagrant で構築した CentOS 環境から mbed cli で実行します。

サンプルコードを動かす

 ひとまず動かしてみるだけであれば公開されているサンプルコードをインポートしてそのままコンパイルすれば動かせてしまいます。まずはインポート。

[vagrant@localhost vagrant]$ mbed import https://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_iBeacon/
[mbed] Importing program "BLE_iBeacon" from "https://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_iBeacon" at latest revision in the current branch
[mbed] Adding library "BLE_API" from "https://developer.mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API" at rev #65474dc93927
[mbed] Adding library "mbed" from "https://mbed.org/users/mbed_official/code/mbed/builds" at rev #abea610beb85
[mbed] Downloading library build "abea610beb85" (might take a minute)
[mbed] Unpacking library build "abea610beb85" in "/vagrant/BLE_iBeacon/mbed"
[mbed] Adding library "nRF51822" from "https://mbed.org/teams/Nordic-Semiconductor/code/nRF51822" at rev #c90ae1400bf2
[mbed] Adding library "shields/TARGET_ST_BLUENRG" from "https://developer.mbed.org/teams/ST/code/X_NUCLEO_IDB0XA1" at rev #fa98703ece8e
[mbed] Couldn't find build tools in your program. Downloading the mbed 2.0 SDK tools...

 続けて toolchain と target を指定してからコンパイルしてみます。

[vagrant@localhost vagrant]$ cd BLE_iBeacon/                                        
[vagrant@localhost BLE_iBeacon]$                                                    
[vagrant@localhost BLE_iBeacon]$ mbed detect                                        
                                                                                    
[mbed] Detected RBLAB_BLENANO, port /dev/ttyACM0, mounted /run/media/vagrant/DAPLINK
[mbed] Supported toolchains for RBLAB_BLENANO                                       
+--------+-----------+-----------+-----+---------+-----+                            
| Target | mbed OS 2 | mbed OS 5 | ARM | GCC_ARM | IAR |                            
+--------+-----------+-----------+-----+---------+-----+                            
+--------+-----------+-----------+-----+---------+-----+                            
Supported targets: 0                                                                
                                                                                    
[vagrant@localhost BLE_iBeacon]$                                                    
[vagrant@localhost BLE_iBeacon]$ mbed toolchain GCC_ARM                             
[mbed] GCC_ARM now set as default toolchain in program "BLE_iBeacon"                
[vagrant@localhost BLE_iBeacon]$ mbed target RBLAB_BLENANO                          
[mbed] RBLAB_BLENANO now set as default target in program "BLE_iBeacon"             
[vagrant@localhost BLE_iBeacon]$                                                    
[vagrant@localhost BLE_iBeacon]$ mbed compile                                       
Building project BLE_iBeacon (RBLAB_BLENANO, GCC_ARM)                               
Scan: .                                                                             
Scan: mbed                                                                          
Scan: env                                                                           
Compile [  1.8%]: BLE.cpp                                                           
〜〜〜中略〜〜〜
Compile [100.0%]: nRF5xn.cpp
Link: BLE_iBeacon
/usr/local/lib/gcc-arm-none-eabi-4_9-2015q3/bin/../lib/gcc/arm-none-eabi/4.9.3/../../../../arm-none-eabi/lib/armv6-m/crt0.o: In function `_start':
(.text+0x52): undefined reference to `__wrap_exit'
./mbed/abea610beb85/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/retarget.o: In function `__cxa_pure_virtual':
retarget.cpp:(.text.__cxa_pure_virtual+0x4): undefined reference to `__wrap_exit'
./mbed/abea610beb85/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/libmbed.a(mbed_error.o): In function `error':
mbed_error.c:(.text.error+0x10): undefined reference to `__wrap_exit'
collect2: error: ld returned 1 exit status
[ERROR] /usr/local/lib/gcc-arm-none-eabi-4_9-2015q3/bin/../lib/gcc/arm-none-eabi/4.9.3/../../../../arm-none-eabi/lib/armv6-m/crt0.o: In function `_start':
(.text+0x52): undefined reference to `__wrap_exit'
./mbed/abea610beb85/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/retarget.o: In function `__cxa_pure_virtual':
retarget.cpp:(.text.__cxa_pure_virtual+0x4): undefined reference to `__wrap_exit'
./mbed/abea610beb85/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/libmbed.a(mbed_error.o): In function `error':
mbed_error.c:(.text.error+0x10): undefined reference to `__wrap_exit'
collect2: error: ld returned 1 exit status

[mbed] ERROR: "/usr/local/pyenv/versions/anaconda2-4.4.0/bin/python" returned error code 1.
[mbed] ERROR: Command "/usr/local/pyenv/versions/anaconda2-4.4.0/bin/python -u /vagrant/BLE_iBeacon/.temp/tools/make.py -t GCC_ARM -m RBLAB_BLENANO --source . --build ./BUILD/RBLAB_BLENANO/GCC_ARM" in "/vagrant/BLE_iBeacon"
---

 コンパイル自体は成功しましたが、ライブラリをリンクするところでエラーになってしまいました。調べたところ下記のページの情報をみつけました。

https://developer.mbed.org/forum/bugs-suggestions/topic/27426/?page=1#comment-52739

 mbed.bld で指定されているビルドが古いということなので、上記ページにあるように変更してみます。

[vagrant@localhost BLE_iBeacon]$ cat mbed.bld 
https://mbed.org/users/mbed_official/code/mbed/builds/abea610beb85
 ↓
[vagrant@localhost BLE_iBeacon]$ cat mbed.bld
https://mbed.org/users/mbed_official/code/mbed/builds/4eea097334d6

 そして mbed deploy で更新します。

[vagrant@localhost BLE_iBeacon]$ mbed deploy
[mbed] Updating library "BLE_API" to rev #65474dc93927
[mbed] Updating library "mbed" to rev #4eea097334d6
[mbed] Downloading library build "4eea097334d6" (might take a minute)
[mbed] Unpacking library build "4eea097334d6" in "/vagrant/BLE_iBeacon/mbed"
[mbed] Updating library "nRF51822" to rev #c90ae1400bf2
[mbed] Updating library "shields/TARGET_ST_BLUENRG" to rev #fa98703ece8e
[mbed] Updating the mbed 2.0 SDK tools...

 そしてコンパイル。

[vagrant@localhost BLE_iBeacon]$ mbed compile
Building project BLE_iBeacon (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: mbed
Scan: env
Compile [  1.8%]: BLE.cpp
〜〜〜中略〜〜〜
Compile [100.0%]: nRF5xn.cpp
[Warning] nRF5xGap.h@223,93: 'void mbed::Ticker::attach_us(T*, M, timestamp_t) [with T = nRF5xGap; M = void (nRF5xGap::*)(); timestamp_t = long unsigned int]' is deprecated (declared at ./mbed/4eea097334d6/drivers/Ticker.h:121): The attach_us function does not support cv-qualifiers. Replaced by attach_us(callback(obj, method), t). [since mbed-os-5.1] [-Wdeprecated-declarations]
Link: BLE_iBeacon
Elf2Bin: BLE_iBeacon
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |   111 |     3 |   28 |
| Misc      | 32560 |   141 | 1124 |
| Subtotals | 32671 |   144 | 1152 |
+-----------+-------+-------+------+
Allocated Heap: 2800 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 1296 bytes
Total RAM memory (data + bss + heap + stack): 6144 bytes
Total Flash memory (text + data + misc): 32815 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_iBeacon.hex

 成功して hex ファイルが作成されました。ちなみにこの時点での最新のビルドは a330f0fddbec でした。

https://developer.mbed.org/users/mbed_official/code/mbed//builds/a330f0fddbec

 ですがこのビルドを指定すると下記のようなエラーになってしまったので、 4eea097334d6 で実行しています。

[vagrant@localhost BLE_iBeacon]$ mbed compile
Building project BLE_iBeacon (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: mbed
Scan: env
Compile [  1.8%]: BLE.cpp
[Warning] toolchain.h@23,2: #warning toolchain.h has been replaced by mbed_toolchain.h, please update to mbed_toolchain.h [since mbed-os-5.3] [-Wcpp]
Compile [  3.6%]: BLEInstanceBase.cpp
Compile [  5.5%]: DiscoveredCharacteristic.cpp
Compile [  7.3%]: GapScanningParams.cpp
Compile [  9.1%]: DFUService.cpp
Compile [ 10.9%]: UARTService.cpp
Compile [ 12.7%]: URIBeaconConfigService.cpp
Compile [ 14.5%]: main.cpp
[Fatal Error] iBeacon.h@19,26: core_cmInstr.h: No such file or directory
[ERROR] In file included from ./main.cpp:18:0:
./BLE_API/ble/services/iBeacon.h:19:26: fatal error: core_cmInstr.h: No such file or directory
 #include "core_cmInstr.h"
                          ^
compilation terminated.

[mbed] ERROR: "/usr/local/pyenv/versions/anaconda2-4.4.0/bin/python" returned error code 1.
[mbed] ERROR: Command "/usr/local/pyenv/versions/anaconda2-4.4.0/bin/python -u /vagrant/BLE_iBeacon/.temp/tools/make.py -t GCC_ARM -m RBLAB_BLENANO --source . --build ./BUILD/RBLAB_BLENANO/GCC_ARM" in "/vagrant/BLE_iBeacon"
---

 それではBLE Nano に hex ファイルをコピーして動作確認してみます。

[vagrant@localhost BLE_iBeacon]$ mbed detect

[mbed] Detected RBLAB_BLENANO, port /dev/ttyACM0, mounted /run/media/vagrant/DAPLINK
[mbed] Supported toolchains for RBLAB_BLENANO
+--------+-----------+-----------+-----+---------+-----+
| Target | mbed OS 2 | mbed OS 5 | ARM | GCC_ARM | IAR |
+--------+-----------+-----------+-----+---------+-----+
+--------+-----------+-----------+-----+---------+-----+
Supported targets: 0

[vagrant@localhost BLE_iBeacon]$ 
[vagrant@localhost BLE_iBeacon]$ cp ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_iBeacon.hex /run/media/vagrant/DAPLINK/
[vagrant@localhost BLE_iBeacon]$ 

 BLE Nano 側での作業は一旦終了で、 iPhone 側で iBeacon を検知するために下記アプリをインストールします。

Locate Beacon

Locate Beacon

  • Radius Networks
  • ユーティリティ
  • 無料

 アプリを起動して下記のような内容で検知対象UUIDを登録します。

f:id:akanuma-hiroaki:20170915092148p:plain:w300

 すると下記のように iBeacon が検知されます。

f:id:akanuma-hiroaki:20170915092349p:plain:w300f:id:akanuma-hiroaki:20170915092356p:plain:w300

サンプルコードの内容

 サンプルコードの内容について自分が理解した範囲で説明してみます。ちなみに mbed の BLE API のリファレンスは下記にあります。

BLE_API | Mbed

 まずは main() の中で ble.init() で BLE Controller を初期化します。 BLE API を使う場合にはまずこれをやります。引数には初期化完了時に実行されるコールバックメソッドを指定します。

ble.init(bleInitComplete);

 コールバックメソッドには BLE::InitializationCompleteCallbackContex のインスタンスが渡されます。

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)

 コールバックメソッドの中で iBeacon クラスを new することで BLE Advertisement を iBeacon として送るためのセットアップが行われます。uuid, majorNumber, minorNumber, txPower はとりあえずサンプルコードのデフォルト値をそのまま使います。

const uint8_t uuid[] = {0xE2, 0x0A, 0x39, 0xF4, 0x73, 0xF5, 0x4B, 0xC4,
                        0xA1, 0x2F, 0x17, 0xD1, 0xAD, 0x07, 0xA9, 0x61};
uint16_t majorNumber = 1122;
uint16_t minorNumber = 3344;
uint16_t txPower     = 0xC8;
iBeacon *ibeacon = new iBeacon(ble, uuid, majorNumber, minorNumber, txPower);

 Advertisement の送信間隔を設定し、送信を開始します。

ble.gap().setAdvertisingInterval(1000); /* 1000ms. */
ble.gap().startAdvertising();

 main() の中では BLE Controller の初期化が終わるまで空のループを回して待ちます。

while (!ble.hasInitialized()) { /* spin loop */ }

 初期化が終わったら無限ループを回し、イベントが発生するまで待機を繰り返します。

while (true) {
    ble.waitForEvent(); // allows or low power operation
}

まとめ

 とりあえず iBeacon の Advertisement を送信するだけならサンプルコードをそのまま使ってとても簡単に実行することができてしまいました。次は色々なセンサー類と組み合わせるためにも、Peripheralとして動作できるように挑戦してみたいと思います。

 また、環境面では mbed cli だと Web IDE と比べてやはりライブラリとの依存関係などで悩まされることは多くなりそうです。すぐ解決できる場合は良いのですが、困った時はあまり悩まず Web IDE に切り替えて使うのが良さそうです。

BLE Nano と mbed CLI で Lチカしてみる

※USB接続の書き込み用ボードとして MK20 がセットになったものと DAPLink がセットになったものがあります。当初 MK20 と書いていましたが、私が購入したものは DAPLink でしたので、修正しました。(2017/09/18)

 BLE デバイスの Firmware 開発に興味があって入門用のものを探したところ、 BLE Nano というのがみつかったので購入しました。

BLE Nano — RedBear

f:id:akanuma-hiroaki:20170910001301j:plain:w450

 BLE Nano 単体のものと、USB接続の書き込み用ボード MK20 もしくは DAPLink がセットになったキットがありますが、初めて買う場合は書き込み用ボードも必要なのでキットを買うことになります。

www.switch-science.com

 スイッチサイエンスでは在庫切れだったのですが、運よくツクモのロボット王国で購入できました。

 業界最小と謳っているだけのことはあり、かなり小さいです。実際にデバイスに搭載されるモジュール部分だけであれば100円硬貨と同じぐらいです。

f:id:akanuma-hiroaki:20170910001752j:plain:w450

 今回はチュートリアルに沿ってオンラインの開発環境で試した後、 mbed CLI でターミナル上から開発できるようにしてみたいと思います。

設定&チュートリアル

 BLE Nano のチュートリアルは下記ページに公開されています。

Getting Started - nRF51822 — RedBear

 ハードウェアセットアップの説明では MK20 と BLE Nano にピンヘッダをはんだ付けすることになっていますが、私が買ったキットは DAPLink とのセットで、すでにはんだ付けされていました。また、MK20 側のホールが余るような説明になっていますが、DAPLink 側のホールは余っていませんでした。

f:id:akanuma-hiroaki:20170910003341j:plain:w450

f:id:akanuma-hiroaki:20170910003411j:plain:w450

 BLE Nano の開発環境としては、 GCC、 mbed、 Arduino IDE に対応しています。手軽に使えそうなのは mbed なので、今回は mbed で試してみたいと思います。チュートリアルページで紹介されている mbed のリンク先ではアカウント登録はできないようで、下記ページから登録しました。

developer.mbed.org

 アカウント登録が終わったら、BLE Nano + DAPLink をUSBポートに挿すと、USBドライブとして認識されます。私の環境では /Volumes/DAPLINK として認識されていました。ドライブの中に含まれている MBED.HTM をブラウザで開きます。

f:id:akanuma-hiroaki:20170910011134p:plain

 RedBearLab BLE Nano の画面が開きますので、画面右上の Compiler メニューをクリックするとオンラインの開発環境にアクセスでき、下記のような画面になります。

f:id:akanuma-hiroaki:20170910004607p:plain

 画面右上の RedBearLab BLE Nano と表示されているところをクリックすると対象のプラットフォームを選択できますが、すでに BLE Nano が選択されているのでそのままで問題ないと思います。

 また、画面左上の インポート メニューから、公開されているリポジトリのプログラムをインポートすることができます。

f:id:akanuma-hiroaki:20170910005602p:plain

 チュートリアルでは mbed_blinky を検索して選択していますが、検索ではチュートリアルと同じものがみつからなかったので、直接下記URLを指定する形でインポートしました。

https://developer.mbed.org/teams/mbed/code/mbed_blinky/

 無事にインポートされると下記のようにファイルリストが表示されます。

f:id:akanuma-hiroaki:20170910005938p:plain

 main.cpp をダブルクリックして内容を表示し、チュートリアルにあるように、 myled の値を P0_19 に変更します。

f:id:akanuma-hiroaki:20170910010036p:plain

 そして画面上部の コンパイル メニューをクリックするとコンパイルが実行され、 mbed_blinky_RBLAB_BLENANO.hex というファイルがビルドされますので、 /Volumes/DAPLINK にコピーすると BLE Nano にプログラムが書き込まれ、実行されます。

f:id:akanuma-hiroaki:20170910011952j:plain:w450

mbed CLI

 チュートリアルで紹介されているのはここまでですが、オフライン環境でも開発できて CLI から使える mbed CLI というのがあったので、こちらで同様のことを行ってみたいと思います。私としては vim から編集できて、コマンドラインからコンパイル等も行う方が好きなので、良さそうであればこちらのやり方でやっていきたいと思います。

 開発環境の設定については下記ページで紹介されていますので、紹介されている通りに手順を実行します。 Python, Git, Mercurial, GCC をインストールした後で mbed CLI をインストールする形になります。私の場合は Mac 環境なので、後者のページの手順を実行しました。

mbed オフラインの開発環境 | Mbed

mbed CLI (コマンドライン・インタフェース)を Mac OS X で使ってみる | Mbed

 必要なもののインストールが終わったら作業ディレクトリに移動して、オンラインの開発環境でやったのと同じように、公開されているプログラムをインポートします。

mbed_cli  $ mbed import https://developer.mbed.org/teams/mbed/code/mbed_blinky/
[mbed] Importing program "mbed_blinky" from "https://developer.mbed.org/teams/mbed/code/mbed_blinky" at latest revision in the current branch
[mbed] Adding library "mbed" from "https://mbed.org/users/mbed_official/code/mbed/builds" at rev #a97add6d7e64
[mbed] Downloading library build "a97add6d7e64" (might take a minute)
[mbed] Unpacking library build "a97add6d7e64" in "/Users/akanuma/workspace/mbed_cli/mbed_blinky/mbed"
[mbed] Couldn't find build tools in your program. Downloading the mbed 2.0 SDK tools...
[mbed] Auto-installing missing Python modules...

 上記の例だと mbed_blinky というディレクトリが作成されているのでその中に入ります。

mbed_cli  $ cd mbed_blinky/
mbed_blinky  $ 
mbed_blinky  $ ls -ltr
total 24
-rw-r--r--  1 akanuma  staff    66 Sep  9 23:14 mbed.bld
-rw-r--r--  1 akanuma  staff   168 Sep  9 23:14 main.cpp
drwxr-xr-x  4 akanuma  staff   136 Sep  9 23:17 mbed
-rw-r--r--  1 akanuma  staff  1329 Sep  9 23:18 mbed_settings.py

 mbed detect コマンドを実行すると、接続されているデバイスを検出してくれるので実行してみます。

mbed_blinky  $ mbed detect
Traceback (most recent call last):
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/detect_targets.py", line 25, in <module>
    from tools.options import get_default_options_parser
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/options.py", line 21, in <module>
    from tools.toolchains import TOOLCHAINS
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/toolchains/__init__.py", line 29, in <module>
    from tools.config import Config
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/config/__init__.py", line 29, in <module>
    from tools.arm_pack_manager import Cache
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/arm_pack_manager/__init__.py", line 2, in <module>
    from bs4 import BeautifulSoup
ImportError: No module named bs4
[mbed] ERROR: "/usr/local/opt/python/bin/python2.7" returned error code 1.
[mbed] ERROR: Command "/usr/local/opt/python/bin/python2.7 -u /Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/detect_targets.py" in "/Users/akanuma/workspace/mbed_cli/mbed_blinky"
---

 bs4 というモジュールがないということなので、 pip でインストールします。

mbed_blinky  $ pip install beautifulsoup4
Collecting beautifulsoup4
  Downloading beautifulsoup4-4.6.0-py2-none-any.whl (86kB)
    100% |████████████████████████████████| 92kB 2.5MB/s 
Installing collected packages: beautifulsoup4
Successfully installed beautifulsoup4-4.6.0
mbed_blinky  $ 
mbed_blinky  $ mbed detect                                                                                                                                                                                                                    
Traceback (most recent call last):
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/detect_targets.py", line 25, in <module>
    from tools.options import get_default_options_parser
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/options.py", line 21, in <module>
    from tools.toolchains import TOOLCHAINS
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/toolchains/__init__.py", line 29, in <module>
    from tools.config import Config
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/config/__init__.py", line 29, in <module>
    from tools.arm_pack_manager import Cache
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/arm_pack_manager/__init__.py", line 20, in <module>
    from fuzzywuzzy import process
ImportError: No module named fuzzywuzzy
[mbed] ERROR: "/usr/local/opt/python/bin/python2.7" returned error code 1.
[mbed] ERROR: Command "/usr/local/opt/python/bin/python2.7 -u /Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/detect_targets.py" in "/Users/akanuma/workspace/mbed_cli/mbed_blinky"
---

 今度は fuzzywuzzy がないということなのでこちらもインストールします。

mbed_blinky  $ pip install fuzzywuzzy
Collecting fuzzywuzzy
  Downloading fuzzywuzzy-0.15.1-py2.py3-none-any.whl
Installing collected packages: fuzzywuzzy
Successfully installed fuzzywuzzy-0.15.1
mbed_blinky  $ 
mbed_blinky  $ mbed detect                                                                                                                                                                                                                    

[mbed] Detected RBLAB_BLENANO, port /dev/tty.usbmodem1412, mounted /Volumes/DAPLINK
[mbed] Supported toolchains for RBLAB_BLENANO
+--------+-----------+-----------+-----+---------+-----+
| Target | mbed OS 2 | mbed OS 5 | ARM | GCC_ARM | IAR |
+--------+-----------+-----------+-----+---------+-----+
+--------+-----------+-----------+-----+---------+-----+
Supported targets: 0

mbed_blinky  $ 

 Detected RBLAB_BLENANO ということで、 BLE Nano が検出されました。表の中と、 Supported targets に何も表示されていないのが気になるところですがひとまず進みます。続いてツールチェーン(コンパイラ)とターゲットを指定します。

mbed_blinky  $ mbed toolchain GCC_ARM
[mbed] GCC_ARM now set as default toolchain in program "mbed_blinky"
mbed_blinky  $ 
mbed_blinky  $ mbed target RBLAB_BLENANO
[mbed] RBLAB_BLENANO now set as default target in program "mbed_blinky"

 それではコンパイルしてみます。 mbed compile を実行するだけです。

mbed_blinky  $ mbed compile
Building project mbed_blinky (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: env
Scan: mbed
Compile [100.0%]: main.cpp
Link: mbed_blinky
Elf2Bin: mbed_blinky
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |    20 |     0 |   15 |
| Misc      |  8389 |   124 |  301 |
| Subtotals |  8409 |   124 |  316 |
+-----------+-------+-------+------+
Allocated Heap: 3648 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 440 bytes
Total RAM memory (data + bss + heap + stack): 6136 bytes
Total Flash memory (text + data + misc): 8533 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/mbed_blinky.hex

 コンパイルが実行され、 ./BUILD/RBLAB_BLENANO/GCC_ARM/mbed_blinky.hex というファイルが作成されました。これを BLE Nano にコピーします。

mbed_blinky  $ cp ./BUILD/RBLAB_BLENANO/GCC_ARM/mbed_blinky.hex /Volumes/DAPLINK/

 コピーが完了すると BLE Nano でプログラムが実行されます。

 最初は Vagrant で Linux の VM 上に mbed CLI の環境を作ってみたのですが、インポートしようとすると [mbed] Downloading library build "a97add6d7e64" (might take a minute) で止まってしまって進まなかったので、Macに直接環境を作りました。

デバッグ環境の設定

 前述の mbed CLI の参考サイトにデバッグ方法も紹介されていたので環境設定をしてみました。紹介されている方法の中でMacで使えるのは Eclipse と pyOCD でのデバッグなので、その方法を試してみました。

 まずは Eclipse IDE for C/C++ Developers をダウンロードして解凍し、 Eclipse.app を Applications ディレクトリに移動して起動します。

 次は GNU ARM Eclipse plugin をインストールということなんですが、前述のページで紹介されているURLは Deprecated ということで、色々調べたところ、 GNU ARM plugin は GNU MCU plugin に変わっているようで、下記のサイトでインストール方法が紹介されていました。

gnu-mcu-eclipse.github.io

 Marketplace で検索してインストールするやり方ではエラーになってしまったので Install New Software から下記のURLを指定してインストールしました。

http://gnu-mcu-eclipse.netlify.com/v4-neon-updates/

 PyOCD はインストール済みでした。

mbed_cli  $ pip install pyocd
Requirement already satisfied: pyocd in /usr/local/lib/python2.7/site-packages
Requirement already satisfied: websocket-client in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: enum34 in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: intelhex in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: six in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: future in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: hidapi in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: setuptools>=19.0 in /usr/local/lib/python2.7/site-packages (from hidapi->pyocd)

 GDB がインストールされていなかったので brew でインストールします。

mbed_blinky  $ brew install gdb

 環境の準備ができたら --profile debug オプションをつけてプログラムをコンパイルします。

mbed_blinky  $ mbed compile --profile debug
Building project mbed_blinky (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: env
Scan: mbed
Compile [100.0%]: main.cpp
Link: mbed_blinky
Elf2Bin: mbed_blinky
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |    21 |     0 |   15 |
| Misc      |  8604 |   124 |  301 |
| Subtotals |  8625 |   124 |  316 |
+-----------+-------+-------+------+
Allocated Heap: 3648 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 440 bytes
Total RAM memory (data + bss + heap + stack): 6136 bytes
Total Flash memory (text + data + misc): 8749 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/mbed_blinky.hex

 そして Eclipse からデバッグ設定をします。Eclipse での設定も前述の mbed オフラインの開発環境 | Mbed のページに書かれています。

 「Run」メニューの 「Debug Configurations…」からデバッグ設定をします。GDB PyOCD Debugging の下に新しい設定を作成し、 C/C++ Application に先ほどコンパイルして作成されたファイルを指定します。

f:id:akanuma-hiroaki:20170910145301p:plain

 「Debugger」タブで pyOCD の Executable に pyocd-gdbserver のパスを設定し、GDB の Executable に gdb のパスを設定します。

f:id:akanuma-hiroaki:20170910145520p:plain

 ここまで設定したら設定を保存して Debug ボタンでデバッグを開始します。すると Eclipse の Debug Perspective が表示され、実行状況が見られるようになります。

f:id:akanuma-hiroaki:20170910145637p:plain

 試した限りではまだ実行中のコードを見ることができていないので、今後その辺りは調べてみようと思います。

まとめ

 もともと Web 系のエンジニアだった自分としては BLE の Firmware 開発は敷居が高かったのですが、 BLE Nano と mbed で、ひとまず動かしてみるぐらいまでは手軽にできました。また、 mbed CLI でオフラインでターミナルから開発することができるので、 vim でコードを書いたり、Git でバージョン管理もやりやすそうなのですが、私の環境では mbed CLI だとプログラムのインポートや新規作成に10分以上かかってしまっていたので、初期に色々試してみる場合はオンラインの開発環境の方がやりやすいかもしれません。まだ単体で動かしているだけですが、今後色々なセンサーと組み合わせて動かせるようにしていきたいと思います。