TEXAS INSTRUMENTS の SimpleLink CC2650 というセンサータグを買ってみたので、Raspberry Pi から BLE でセンサーデータを取得してみたいと思います。
SimpleLink SensorTag CC2650 は気温、湿度、気圧、加速度、ジャイロ、磁気、照度などのセンサーを搭載しています。
外箱は下記のような感じです。
デバイス本体は下記のようになります。写真だと大きく見えてしまいますが、約5cm x 4cm ぐらいの大きさです。
スマートフォンアプリからの接続
まずは TEXAS INSTRUMENTS のセンサータグ用スマートフォンアプリからの接続を試してみたいと思います。自分で開発しなくても手軽にセンサーが取得しているデータを確認することができます。
アプリを起動して、SensorTagの側面にあるスイッチを押すと Advertising が始まり、リストにSensorTagが表示されます。そこからセンサーが取得しているデータや、GATT Service や Characteristic の UUID を確認できます。
Raspberry Pi からの接続
それでは Raspberry Pi から SensorTag に接続して、データを取得してみたいと思います。基本的な処理内容は以前の記事(D-BusからBLEデバイスのNotificationを受け取る)で試したものと同様ですが、今回はデバイス固有の処理とBLEデバイス共通の処理を分割してみました。
まずはBLEデバイス共通の処理のコード全体を載せておきます。BlueZによる処理をラップする形で、Device、Service、Characteristic のクラスを用意しています。エラーハンドリングはまだ考慮してません。Bluetoothのインタフェースもとりあえず hci0 を直接指定しています。
require 'bundler/setup' require 'dbus' class BLE attr_reader :bus SERVICE_NAME = 'org.bluez' SERVICE_PATH = '/org/bluez' ADAPTER = 'hci0' DEVICE_IF = 'org.bluez.Device1' SERVICE_IF = 'org.bluez.GattService1' CHARACTERISTIC_IF = 'org.bluez.GattCharacteristic1' DBUS_PROPERTIES_IF = 'org.freedesktop.DBus.Properties' SERVICE_RESOLVED_PROPERTY = 'ServicesResolved' UUID_PROPERTY = 'UUID' PROPERTIES_CHANGED_SIGNAL = 'PropertiesChanged' SERVICE_RESOLVE_CHECK_INTERVAL = 0.1 # デバイスに接続後にサービスが解決されたかをチェックするインターバル DISCOVERY_WAITING_SECOND = 10 # デバイス検出の待機時間 module UUID GENERIC_ATTRIBUTE_SERVICE = '00001801-0000-1000-8000-00805f9b34fb' DEVICE_INFORMATION_SERVICE = '0000180a-0000-1000-8000-00805f9b34fb' BATTERY_SERVICE = '0000180f-0000-1000-8000-00805f9b34fb' BATTERY_DATA = '00002a19-0000-1000-8000-00805f9b34fb' end class Device attr_reader :bluez, :name, :address def initialize(bluez, bluez_device, name, address) @bluez = bluez @bluez_device = bluez_device @name = name @address = address end # Device への接続処理。接続後に GATT Service が解決状態になるまで待機する def connect @bluez_device.introspect @bluez_device.Connect @bluez_device.introspect while !properties[SERVICE_RESOLVED_PROPERTY] do sleep(SERVICE_RESOLVE_CHECK_INTERVAL) end end def disconnect @bluez_device.Disconnect end def properties @bluez_device.introspect @bluez_device.GetAll(DEVICE_IF).first end # Device が持つ GATT Service のリストを返す def services services = [] @bluez_device.subnodes.each do |node| service = @bluez.object("#{@bluez_device.path}/#{node}") service.introspect properties = service.GetAll(SERVICE_IF).first services << Service.new(@bluez, service, properties[UUID_PROPERTY]) end services end def service_by_uuid(uuid) services.each do |service| return service if service.uuid == uuid end raise 'Service not found.' end def read_battery_level service = service_by_uuid(BLE::UUID::BATTERY_SERVICE) characteristic = service.characteristic_by_uuid(BLE::UUID::BATTERY_DATA) yield(characteristic.read.first) characteristic.start_notify do |v| yield(v.first) end end end class Service attr_reader :uuid def initialize(bluez, bluez_service, uuid) @bluez = bluez @bluez_service = bluez_service @uuid = uuid end def properties @bluez_service.introspect @bluez_service.GetAll(SERVICE_IF).first end # Service が持つ Characteristic のリストを返す def characteristics characteristics = [] @bluez_service.subnodes.each do |node| characteristic = @bluez.object("#{@bluez_service.path}/#{node}") characteristic.introspect properties = characteristic.GetAll(CHARACTERISTIC_IF).first characteristics << Characteristic.new(characteristic, properties[UUID_PROPERTY]) end characteristics end def characteristic_by_uuid(uuid) characteristics.each do |characteristic| return characteristic if characteristic.uuid == uuid end raise 'Characteristic not found.' end end class Characteristic attr_reader :uuid def initialize(bluez_characteristic, uuid) @bluez_characteristic = bluez_characteristic @uuid = uuid end def properties @bluez_characteristic.introspect @bluez_characteristic.GetAll(CHARACTERISTIC_IF).first end # Characteristic のプロパティに変更があった時にシグナルを受け取る # ブロックを渡してシグナル検知時にブロックを実行する def start_notify @bluez_characteristic.StartNotify @bluez_characteristic.default_iface = DBUS_PROPERTIES_IF @bluez_characteristic.on_signal(PROPERTIES_CHANGED_SIGNAL) do |_, v| yield(v['Value']) end end def write(value) @bluez_characteristic.WriteValue(value, {}) end def read @bluez_characteristic.ReadValue({}).first end def inspect @bluez_characteristic.inspect end end def initialize @bus = DBus::system_bus @bluez = @bus.service(SERVICE_NAME) @adapter = @bluez.object("#{SERVICE_PATH}/#{ADAPTER}") @adapter.introspect end # Bluetoothアダプタ配下のBLEデバイスを検出して Device クラスのインスタンスのリストを返す # デバイス名や RSSI が nil のデバイスは除外 def devices @adapter.StartDiscovery sleep(DISCOVERY_WAITING_SECOND) devices = [] @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'] address = properties['Address'] rssi = properties['RSSI'] next if name.nil? || rssi.nil? devices << Device.new(@bluez, device, name, address) end @adapter.StopDiscovery devices end def device_by_name(name) devices.each do |device| return device if device.name.downcase.include?(name.downcase) end raise 'No devices found.' end end
BLEクラスを new して #device_by_name メソッドにデバイス名を渡すと、デバイス名を含んだBLEデバイスを検出して Device クラスのインスタンスとして返します。
次にSensorTag固有の処理のコードです。こちらもまだエラーハンドリングは考慮してません。SensorTag クラスには基本的に各センサーを有効化する処理と、センサーデータ検出時にデータをコールバックのブロックに受け渡す処理を実装しています。各センサーから検出されるデータは生データなので、実際に利用するにはコンバートする必要があります。コンバート処理の内容はいろいろ調べたのですが間違っているかもしれないので、参考までということで。
require 'bundler/setup' require './ble.rb' # 前述のBLEデバイス共通処理を読み込む class SensorTag DEVICE_NAME = 'CC2650' SCALE_LSB = 0.03125 module UUID IR_TEMPERATURE_SERVICE = 'f000aa00-0451-4000-b000-000000000000' HUMIDITY_SERVICE = 'f000aa20-0451-4000-b000-000000000000' BAROMETER_SERVICE = 'f000aa40-0451-4000-b000-000000000000' MOVEMENT_SERVICE = 'f000aa80-0451-4000-b000-000000000000' LUXOMETER_SERVICE = 'f000aa70-0451-4000-b000-000000000000' SIMPLE_KEYS_SERVICE = '0000ffe0-0000-1000-8000-00805f9b34fb' IO_SERVICE = 'f000aa64-0451-4000-b000-000000000000' REGISTER_SERVICE = 'f000ac00-0451-4000-b000-000000000000' CONNECTION_CONTROL_SERVICE = 'f000ccc0-0451-4000-b000-000000000000' OAT_SERVICE = 'f000ffc0-0451-4000-b000-000000000000' IR_TEMPERATURE_CONFIG = 'f000aa02-0451-4000-b000-000000000000' IR_TEMPERATURE_DATA = 'f000aa01-0451-4000-b000-000000000000' HUMIDITY_CONFIG = 'f000aa22-0451-4000-b000-000000000000' HUMIDITY_DATA = 'f000aa21-0451-4000-b000-000000000000' MOVEMENT_CONFIG = 'f000aa82-0451-4000-b000-000000000000' MOVEMENT_DATA = 'f000aa81-0451-4000-b000-000000000000' BAROMETER_CONFIG = 'f000aa42-0451-4000-b000-000000000000' BAROMETER_DATA = 'f000aa41-0451-4000-b000-000000000000' LUXOMETER_CONFIG = 'f000aa72-0451-4000-b000-000000000000' LUXOMETER_DATA = 'f000aa71-0451-4000-b000-000000000000' IO_CONFIG = 'f000aa66-0451-4000-b000-000000000000' IO_DATA = 'f000aa65-0451-4000-b000-000000000000' end # SensorTag に接続 def connect @ble = BLE.new @device = @ble.device_by_name(DEVICE_NAME) @device.connect end # 各センサーの有効化のための処理 def enable(service, config_uuid, value = [0x01]) characteristic = service.characteristic_by_uuid(config_uuid) characteristic.write(value) end def enable_ir_temperature service = @device.service_by_uuid(SensorTag::UUID::IR_TEMPERATURE_SERVICE) enable(service, SensorTag::UUID::IR_TEMPERATURE_CONFIG) end def enable_humidity service = @device.service_by_uuid(SensorTag::UUID::HUMIDITY_SERVICE) enable(service, SensorTag::UUID::HUMIDITY_CONFIG) end def enable_movement service = @device.service_by_uuid(SensorTag::UUID::MOVEMENT_SERVICE) enable(service, SensorTag::UUID::MOVEMENT_CONFIG, [0b11111111, 0b00000000]) end def enable_barometer service = @device.service_by_uuid(SensorTag::UUID::BAROMETER_SERVICE) enable(service, SensorTag::UUID::BAROMETER_CONFIG) end def enable_luxometer service = @device.service_by_uuid(SensorTag::UUID::LUXOMETER_SERVICE) enable(service, SensorTag::UUID::LUXOMETER_CONFIG) end # 各センサーの生データをコンバートする処理 def convert_ir_temperature_value(upper_byte, lower_byte) (((upper_byte << 8) + lower_byte) >> 2) * SCALE_LSB end def convert_temp_value(upper_byte, lower_byte) temp_byte = (upper_byte << 8) + lower_byte (temp_byte / 65536.0) * 165 - 40 end def convert_humidity_value(upper_byte, lower_byte) hum_byte = (upper_byte << 8) + lower_byte (hum_byte / 65536.0) * 100 end def convert_movement_value(upper_byte, lower_byte) value = (upper_byte << 8) + lower_byte if upper_byte > 0x7f value = ~value + 1 end (value >> 2).to_f end def convert_gyro_value(upper_byte, lower_byte) convert_movement_value(upper_byte, lower_byte) / 128.0 end def convert_acc_value(upper_byte, lower_byte) convert_movement_value(upper_byte, lower_byte) / (32768 / 2) end def convert_mag_value(upper_byte, lower_byte) convert_movement_value(upper_byte, lower_byte) * 4912.0 / 32768.0 end def convert_barometer_value(upper_byte, middle_byte, lower_byte) ((upper_byte << 16) + (middle_byte << 8) + lower_byte) / 100.0 end def convert_luxometer_value(upper_byte, lower_byte) lux = (upper_byte << 8) + lower_byte m = lux & 0x0fff e = (lux & 0xf000) >> 12 e = (e == 0) ? 1 : 2 << (e - 1) m * (0.01 * e) end # 各センサーのデータを読み取り、コンバート後のデータをコールバックに渡す def read_ir_temperature service = @device.service_by_uuid(SensorTag::UUID::IR_TEMPERATURE_SERVICE) characteristic = service.characteristic_by_uuid(SensorTag::UUID::IR_TEMPERATURE_DATA) characteristic.start_notify do |v| amb_lower_byte = v[2] amb_upper_byte = v[3] ambient = convert_ir_temperature_value(amb_upper_byte, amb_lower_byte) obj_lower_byte = v[0] obj_upper_byte = v[1] object = convert_ir_temperature_value(obj_upper_byte, obj_lower_byte) yield(ambient, object) end end def read_humidity service = @device.service_by_uuid(SensorTag::UUID::HUMIDITY_SERVICE) characteristic = service.characteristic_by_uuid(SensorTag::UUID::HUMIDITY_DATA) characteristic.start_notify do |v| temp_lower_byte = v[0] temp_upper_byte = v[1] temp = convert_temp_value(temp_upper_byte, temp_lower_byte) hum_lower_byte = v[2] hum_upper_byte = v[3] hum = convert_humidity_value(hum_upper_byte, hum_lower_byte) yield(temp, hum) end end def read_movement service = @device.service_by_uuid(SensorTag::UUID::MOVEMENT_SERVICE) characteristic = service.characteristic_by_uuid(SensorTag::UUID::MOVEMENT_DATA) characteristic.start_notify do |v| gyro_x_lower = v[0] gyro_x_upper = v[1] gyro_y_lower = v[2] gyro_y_upper = v[3] gyro_z_lower = v[4] gyro_z_upper = v[5] gyro_x = convert_gyro_value(gyro_x_upper, gyro_x_lower) gyro_y = convert_gyro_value(gyro_y_upper, gyro_y_lower) gyro_z = convert_gyro_value(gyro_z_upper, gyro_z_lower) acc_x_lower = v[6] acc_x_upper = v[7] acc_y_lower = v[8] acc_y_upper = v[9] acc_z_lower = v[10] acc_z_upper = v[11] acc_x = convert_acc_value(acc_x_upper, acc_x_lower) acc_y = convert_acc_value(acc_y_upper, acc_y_lower) acc_z = convert_acc_value(acc_z_upper, acc_z_lower) mag_x_lower = v[12] mag_x_upper = v[13] mag_y_lower = v[14] mag_y_upper = v[15] mag_z_lower = v[16] mag_z_upper = v[17] mag_x = convert_mag_value(mag_x_upper, mag_x_lower) mag_y = convert_mag_value(mag_y_upper, mag_y_lower) mag_z = convert_mag_value(mag_z_upper, mag_z_lower) yield(gyro_x, gyro_y, gyro_z, acc_x, acc_y, acc_z, mag_x, mag_y, mag_z) end end def read_barometer service = @device.service_by_uuid(SensorTag::UUID::BAROMETER_SERVICE) characteristic = service.characteristic_by_uuid(SensorTag::UUID::BAROMETER_DATA) characteristic.start_notify do |v| temp_lower = v[0] temp_middle = v[1] temp_upper = v[2] temp = convert_barometer_value(temp_upper, temp_middle, temp_lower) press_lower = v[3] press_middle = v[4] press_upper = v[5] press = convert_barometer_value(press_upper, press_middle, press_lower) yield(temp, press) end end def read_luxometer service = @device.service_by_uuid(SensorTag::UUID::LUXOMETER_SERVICE) characteristic = service.characteristic_by_uuid(SensorTag::UUID::LUXOMETER_DATA) characteristic.start_notify do |v| lux_lower = v[0] lux_upper = v[1] lux = convert_luxometer_value(lux_upper, lux_lower) yield(lux) end end def read_battery_level @device.read_battery_level do |battery_level| yield(battery_level) end end # シグナルの待受を開始 def run main = DBus::Main.new main << @ble.bus main.run end def disconnect @device.disconnect end end if $0 == __FILE__ sensor_tag = SensorTag.new begin sensor_tag.connect ir_temperature_log = Logger.new('logs/ir_temperature.log') sensor_tag.enable_ir_temperature sensor_tag.read_ir_temperature do |ambient, object| ir_temperature_log.info("amb: #{ambient} obj: #{object}") end humidity_log = Logger.new('logs/humidity.log') sensor_tag.enable_humidity sensor_tag.read_humidity do |temp, hum| humidity_log.info("temp: #{temp} hum: #{hum}") end gyro_log = Logger.new('logs/gyro.log') acc_log = Logger.new('logs/acc.log') mag_log = Logger.new('logs/mag.log') sensor_tag.enable_movement sensor_tag.read_movement do |gyro_x, gyro_y, gyro_z, acc_x, acc_y, acc_z, mag_x, mag_y, mag_z| gyro_log.info("gyro: #{gyro_x} #{gyro_y} #{gyro_z}") acc_log.info("acc: #{acc_x} #{acc_y} #{acc_z}") mag_log.info("mag: #{mag_x} #{mag_y} #{mag_z}") end barometer_log = Logger.new('logs/barometer.log') sensor_tag.enable_barometer sensor_tag.read_barometer do |temp, press| barometer_log.info("temp: #{temp} press: #{press}") end lux_log = Logger.new('logs/lux.log') sensor_tag.enable_luxometer sensor_tag.read_luxometer do |lux| lux_log.info("lux: #{lux}") end battery_log = Logger.new('logs/battery.log') sensor_tag.read_battery_level do |battery_level| battery_log.info("battery: #{battery_level}") end sensor_tag.run rescue Interrupt => e puts e ensure sensor_tag.disconnect end end
今回はコールバックとして単純に各データをログに出力するようにしていますので、スクリプトを実行するとセンサーのデータが各ログファイルに出力されます。
データのコンバート処理はちゃんと実装する必要ありますが、SensorTag では手軽にいろいろなセンサーデータを取得できて面白いですね。今回はとりあえずデータを出力するだけでしたが、このデータを使って何か面白いことができないか考えてみたいと思います。