D-BusからBLEデバイスのNotificationを受け取る

 前回の記事で、Raspberry Pi上でRuby(irb)からD-Busを使ってBLEデバイスに接続し、値を読み取るというところまでやりましたが、今回はPeripheralからのNotificationを受け取って値の変化を検知するところまでを実装してみました。前回はirbから試してみましたが、今回はスクリプトにまとめてあります。また、対象のBLEデバイスは前回同様にLightBlueで擬似Peripheralデバイスを作成して使用しています。

スクリプト全体

 まずはスクリプト全体を掲載しておきます。

require 'bundler/setup'
require 'dbus'

SERVICE_PATH = '/org/bluez'
ADAPTER      = 'hci0'

DEVICE_IF         = 'org.bluez.Device1'
SERVICE_IF        = 'org.bluez.GattService1'
CHARACTERISTIC_IF = 'org.bluez.GattCharacteristic1'
PROPERTIES_IF     = 'org.freedesktop.DBus.Properties'

HEARTRATE_SERVICE_UUID = '0000180d-0000-1000-8000-00805f9b34fb'
HEARTRATE_UUID         = '00002a37-0000-1000-8000-00805f9b34fb'

log = Logger.new('ble_notification.log')

bus = DBus::SystemBus.instance
bluez = bus.service('org.bluez')

adapter = bluez.object("#{SERVICE_PATH}/#{ADAPTER}")
adapter.introspect
puts 'Discoverying Nodes...'
adapter.StartDiscovery
sleep(10)

nodes = adapter.subnodes
connected_device = nil

begin
  nodes.each do |node|
    device = bluez.object("#{SERVICE_PATH}/#{ADAPTER}/#{node}")
    device.introspect

    properties = device.GetAll(DEVICE_IF)[0]
    name    = properties['Name']
    address = properties['Address']
    rssi    = properties['RSSI']

    next if name.nil? || rssi.nil?
    next unless name.downcase.include?('heart rate')

    puts "Connecting to the device: #{address} #{name} RSSI:#{properties['RSSI']}"
    begin
      device.Connect
      puts 'Connected. Resolving Services...'
      device.introspect

      while !properties['ServicesResolved'] do
        sleep(0.5)
        device.introspect
        properties = device.GetAll(DEVICE_IF)[0]
      end
      puts 'Resolved.'

      connected_device = device
      break
    rescue => e
      puts e
    end
  end

  raise 'No device connected.' if connected_device.nil?

  heartrate_service = nil
  nodes = connected_device.subnodes
  nodes.each do |node|
    service = bluez.object("#{connected_device.path}/#{node}")
    service.introspect

    properties = service.GetAll(SERVICE_IF)[0]
    uuid = properties['UUID']

    next unless uuid == HEARTRATE_SERVICE_UUID

    heartrate_service = service
  end

  nodes = heartrate_service.subnodes
  nodes.each do |node|
    characteristic = bluez.object("#{heartrate_service.path}/#{node}")
    characteristic.introspect

    properties = characteristic.GetAll(CHARACTERISTIC_IF)[0]
    uuid = properties['UUID']

    next unless uuid == HEARTRATE_UUID

    characteristic.StartNotify
    characteristic.default_iface = PROPERTIES_IF
    characteristic.on_signal('PropertiesChanged') do |_, v|
      log.info("#{heartrate_service.path.split('/')[4]} #{v}")
    end
  end

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

  puts 'Monitoring Heart Rate...'
  main.run
rescue Interrupt => e
  puts e
  puts "Interrupted."
rescue => e
  puts e
ensure
  connected_device.Disconnect unless connected_device.nil?
  adapter.StopDiscovery
end

 それでは各パートごとに説明していきます。

Peripheralへの接続

 まずはBluetoothアダプタのDiscoveryModeをONにし、BLEデバイスを検索します。今回はとりあえず10秒間は検索のために待機しています。

bus = DBus::SystemBus.instance
bluez = bus.service('org.bluez')

adapter = bluez.object("#{SERVICE_PATH}/#{ADAPTER}")
adapter.introspect
puts 'Discoverying Nodes...'
adapter.StartDiscovery
sleep(10)

nodes = adapter.subnodes

 そして見つかった各デバイスのデバイス名を確認して、対象のデバイスであれば接続します。そして配下のGATTサービスが検知されて ServicesResolved のプロパティがtrueになるのを待ってから次の処理に進みます。

nodes.each do |node|
  device = bluez.object("#{SERVICE_PATH}/#{ADAPTER}/#{node}")
  device.introspect

  properties = device.GetAll(DEVICE_IF)[0]
  name    = properties['Name']
  address = properties['Address']
  rssi    = properties['RSSI']

  next if name.nil? || rssi.nil?
  next unless name.downcase.include?('heart rate')

  puts "Connecting to the device: #{address} #{name} RSSI:#{properties['RSSI']}"
  begin
    device.Connect
    puts 'Connected. Resolving Services...'
    device.introspect

    while !properties['ServicesResolved'] do
      sleep(0.5)
      device.introspect
      properties = device.GetAll(DEVICE_IF)[0]
    end
    puts 'Resolved.'

    connected_device = device
    break
  rescue => e
    puts e
  end
end

GATTサービスの取得

 次にデバイス配下の各サービスのUUIDを確認し、今回対象とするUUIDを持つサービスを取得します。

nodes = connected_device.subnodes
nodes.each do |node|
  service = bluez.object("#{connected_device.path}/#{node}")
  service.introspect

  properties = service.GetAll(SERVICE_IF)[0]
  uuid = properties['UUID']

  next unless uuid == HEARTRATE_SERVICE_UUID

  heartrate_service = service
end

GATTキャラクタリスティックの取得とシグナル検知時のコールバック設定

 サービス配下の各キャラクタリスティックのUUIDを確認し、今回対象とするUUIDを持つキャラクタリスティックを特定したら、Peripheralからのシグナルを検知して処理を行うためのコールバックの設定を行います。

nodes = heartrate_service.subnodes
nodes.each do |node|
  characteristic = bluez.object("#{heartrate_service.path}/#{node}")
  characteristic.introspect

  properties = characteristic.GetAll(CHARACTERISTIC_IF)[0]
  uuid = properties['UUID']

  next unless uuid == HEARTRATE_UUID

  characteristic.StartNotify
  characteristic.default_iface = PROPERTIES_IF
  characteristic.on_signal('PropertiesChanged') do |_, v|
    log.info("#{heartrate_service.path.split('/')[4]} #{v}")
  end
end

 上記のコールバック設定部分については、まず StartNotify メソッドでPeripheralからのシグナルの送信を開始しています。

characteristic.StartNotify

 そして on_signal メソッドでシグナル検知時のコールバックを指定するのですが、検知対象のインタフェースはデフォルトインタフェースになるので、先に default_iface メソッドでデフォルトインタフェースを指定しておきます。値の変更は D-Bus の org.freedesktop.DBus.Properties インタフェースで検知します。

characteristic.default_iface = PROPERTIES_IF

 シグナルとしては PropertiesChanged シグナルになりますので、 on_signal メソッドでシグナルを指定してコールバックを設定します。今回は検知時の処理としてはデバイスのアドレスと値をログに出力しています。

characteristic.on_signal('PropertiesChanged') do |_, v|
  log.info("#{heartrate_service.path.split('/')[4]} #{v}")
end

非同期処理の開始

 そして最後にイベントループでの非同期処理を開始します。

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

 これでPeripheral側からシグナルが送信されると、下記のようにログに出力されるようになります。

I, [2017-06-14T18:40:31.151687 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 60]}
I, [2017-06-14T18:40:32.163818 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 60]}
I, [2017-06-14T18:40:33.110175 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 60]}
I, [2017-06-14T18:40:34.121697 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 80]}
I, [2017-06-14T18:40:35.134417 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 80]}
I, [2017-06-14T18:40:36.146261 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 80]}
I, [2017-06-14T18:40:37.159118 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 100]}
I, [2017-06-14T18:40:38.103964 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 100]}
I, [2017-06-14T18:40:39.184016 #3589]  INFO -- : dev_7C_12_A1_23_8C_21 {"Value"=>[0, 100]}