« 2015年12月 | トップページ | 2016年2月 »

2016年1月の記事

HomebridgeとRaspberry Piを使ってHome Kitの実験をしてみた

TwitterでHomebridgeとRaspberry Piを使ったHome Kitのデモが流れていたので、私も真似をしてやってみました。これ、面白いです! Siriを使って音声でRaspberry PiにつないだLEDをOn/Offできるまでの手順を簡単に示します。

今回の実験はRaspberry Pi 2とRaspbian Wheezyを使っています。Raspbianはずいぶん前にインストールしたものですが、apt-get upgradeで最新の状態にして一連のインストールを行いました。最新のRaspbianはJessieになっていますが、Jessieでは試していません。そのため、最近RasPiを買ってRaspbianをインストールした方には以下の手順は適用できませんのでご注意下さい。

WebIOPiのインストール

今回の実験では、Raspberry PiにLED制御(Raspberry PiのGPIO制御)用のREST APIを作り、Home KitからこのREST APIを叩くことでLEDの制御を行うことにします。そのために、REST APIの提供とGPIOの制御を同時にできるWebIOPiを使用します。WebIOPiのインストールはリンクの手順 に従って行いました。リンクの手順はv0.6.0を使っていますが、執筆時点で最新のv0.7.1を使っています。

WebIOPiのv0.7.1はRaspberry Pi 2に対応していないため、以下の手順でパッチを当ててからビルドします。

wget http://sourceforge.net/projects/webiopi/files/WebIOPi-0.7.1.tar.gz
tar xvzf WebIOPi-0.7.1.tar.gz
cd WebIOPi-0.7.1
wget https://raw.githubusercontent.com/doublebind/raspi/master/webiopi-pi2bplus.patch
patch -p1 -i webiopi-pi2bplus.patch
sudo ./setup.sh

WebIOPiが動くようになったら、LEDをつなぐGPIOポート(今回はGPIO 17を使用)を出力に設定しておきます。

NodeJSとHomebridgeのインストール

このリンクの手順に従って、NodeJSをインストールします。NodeJSをインストールする前にGCC 4.9をインストールする必要があり(Raspberry PiのGCCはバージョンが古くC++14に対応していないため)、Install C++14の手順に従って行います。

リンクの手順ではNodeJSは4.0.0を使っていますが、執筆時点のLTS版であるv4.2.6をインストールしました。私はRaspberry Pi 2を使っているので、ARMv7版を使いました。

続けて、AvahiとHomebridgeをインストールします。

Homebridge Pluginのインストール

HomebridgeをインストールしただけではLEDや電球の制御は行えず、そのためにはプラグインをインストールする必要があります。プラグインはリンクにあるように90近くがあり何を使ったら良いか迷いますが、Raspberry PiのREST APIを叩くことでLEDの制御ができるように、httpベースのプラグインを探しました。今回は、homebridge-httpというプラグインを使います。プラグインのインストールは次のコマンドで行います。

sudo npm install -g homebridge-http

Pluginの設定

インストールしたプラグインをhomebridgeが認識するために、/home/pi/.homebridge ディレクトリにconfig.jsonという名前のファイルを作成します。config.jsonの内容は以下の通りです。

{
    "bridge":
    {
       "name": "Homebridge",
       "username": "B8:27:EB:4D:31:D7",
       "port": 51826,
       "pin": "031-45-154"
    },

    "accessories": [
    {
        "accessory": "Http",
        "name": "部屋のランプ",
        "switchHandling": "yes",
        "http_method": "POST",
        "on_url":      "http://localhost:8000/GPIO/17/value/1",
        "off_url":     "http://localhost:8000/GPIO/17/value/0",
        "status_url":  "http://localhost:8000/GPIO/17/value",
        "service": "Light",
        "brightnessHandling": "no",
        "brightness_url":     "http://localhost/controller/1707/%b",
        "brightnesslvl_url":  "http://localhost/status/100054",
        "sendimmediately": "",
        "username" : "webiopi",
        "password" : "raspberry"                     
     }
  ]
}

設定ファイルのポイントは以下です;

  • bridgeのusernameはMACアドレスの形式ならなんでもよいみたいですが、Raspberry PiのMACアドレスにしています
  • accessoriesのon_rul/off_urlにWebIOPiのREST APIに従ってGPIO制御のurlを記述します。私はGPIO 17番を使っています
  • WebIOPiはusername/passwordを使った認証機能があるため、accessoriesにデフォルトのusername/passwordを設定しています

Homebridgeの起動

homebrigeを起動すると以下のようなメッセージが出れば起動成功です。

*** WARNING *** The program 'node' uses the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see 
*** WARNING *** The program 'node' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi.
*** WARNING *** Please fix your application to use the native API of Avahi!
*** WARNING *** For more information see 
Loaded plugin: homebridge-http
Registering accessory 'homebridge-httpstatus.Http'
---
Loaded plugin: homebridge-lockitron
Registering accessory 'homebridge-lockitron.Lockitron'
---
Loaded config.json with 1 accessories and 0 platforms.
---
Loading 1 accessories...
[部屋のランプ] Initializing Http accessory...
Scan this code with your HomeKit App on your iOS device to pair with Homebridge:
                       
    ┌────────────┐     
    │ 031-45-154 │     
    └────────────┘     
                       
Homebridge is running on port 51826.

iPhoneにHome Kitアプリをインストール

iPhoneにHome Kitアプリをインストールします。私はアップルのHomeKit Catalogを使用しました。本アプリはソース形式で配布されているため、Xcodeを使ってビルドする必要があります。ダウンロードにはDevelopperアカウントが必要だと思います。HomeKit Catalog以外にも、App Storeに登録されたHome Kit対応のアプリがあるようですので、HomeKit Catalogがダウンロードできない場合にはApp Storeのアプリを使用可能です。

Home Kitアプリを起動してアクセサリの登録を行い、LEDを制御するための「シーン」を設定します。細かな手順は割愛しますが、アプリを触っていると使い方はわかると思います。

動作イメージ

一連の設定でSiriを使ってRaspberry PiにつないだLEDを音声でOn/Offができるようになります。操作のデモを動画に撮ってYouTubeにアップしました。

iOSからMilkcocoaへMQTT publishする

iOSからMQTTブローカにメッセージをpublishするサンプルを作ってみました。

MQTTブローカにはMilkcocoaというサービスを使いました。フリーのMQTTブローカとしてはtest.mosquitto.orgがありますが、以前に試した際はイマイチ接続が安定していない感じでした。そこで、無償使用も可能な商用サービスであるMilkcocoaを試してみました。Milkcocoaは10万メッセージまでは無償で利用できるため、ちょっとしたお試しは無償利用範囲内だと思います。

Milkcocoaのアカウント作成などは、本記事では割愛します。MilkcocoaにはArudino SDKも提供されており、本SDKを使えばESP-WROOM-02を使って簡単にMilkcocoaサーバーにデータをpublishすることができます。今回は、iOSデバイスからMilkcocoaにデーターのpublishを行いたかったため、iOS用のMQTTライブラリを使ってサンプルを作成しました(Arduino SDKを使うともっと簡潔に記述ができるのですが)。

iOS用MQTTライブラリの準備

iOS用のMQTTライブラリはGoogleで検索するといくつかの候補がヒットします。最近Swiftをお勉強中のため、Objective-CではなくSwiftでプログラミングできるライブラリを選びました。Swiftが使えるMQTTライブラリとしては、上位にヒットするもので以下がありました:

  • CocoaMQTT: SwiftとObjective-C(ソケット関連の処理部分)で書かれたSwift/Objective-Cネイティブなライブラリ
  • Moscapsule: APIはSwiftになっていますが、実態はmosquittoのラッパー

今回は、Swift/Objective-CネイティブなCocoaMQTTを使いました。Webサイトの指示に従って、CocoaPodを使ってライブラリをインストールし、XcodeプロジェクトにBridging-Header.hを登録します。 Bridging-Header.hはCocoaMQTTがSwiftとObjective-Cの両方を使用しているため、SwiftとObjective-C間の連携を行うために必要です。

Swiftのコード

今回作成したコードを以下に示します。iPhoneの加速度センサーを読み取り、1秒毎にMilkcocoa MQTTブローカにメッセージをpublishします。UIとしてiPhoneにも加速度センサーの読み取り値を表示します。

//
//  ViewController.swift
//  mqtt_test
//
//  Created by Todotani on 2016/01/17.
//  Copyright © 2016年 Todotani. All rights reserved.
//

import UIKit
import CoreMotion
import CocoaMQTT

class ViewController: UIViewController, CocoaMQTTDelegate{
    
    @IBOutlet var connectionState: UILabel!
    @IBOutlet var label_accel_X: UILabel!
    @IBOutlet var lable_accel_Y: UILabel!
    
    private struct MqttConstants {
        static let AppID    = "YourAppID"     // Set AppID
        static let ClientId = MqttConstants.AppID
        static let HostName = MqttConstants.AppID + ".mlkcca.com"
        static let UserName = "sdammy"
        static let PassWord = MqttConstants.AppID
        static let Topic    = MqttConstants.AppID + "/Test/push"
    }

    var mqtt: CocoaMQTT!
    var motionManager: CMMotionManager!
    var isConnected: Bool = false {
        didSet{
            if isConnected == true {
                connectionState.text = "Connected"
            } else {
                connectionState.text = "Disconnect"
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        appDelegate.viewController = self
        
        mqtt = CocoaMQTT(clientId: MqttConstants.ClientId, host: MqttConstants.HostName)
        mqtt.username = MqttConstants.UserName
        mqtt.password = MqttConstants.PassWord
        mqtt.keepAlive = 60
        mqtt.delegate = self
        
        motionManager = CMMotionManager()
        motionManager.accelerometerUpdateInterval = 0.1
        
        mqtt.connect()
    }
    
    private var counter = 0
    
    func startUpdate() {
        motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue()!, withHandler: {(data, error) in
            let accel_X = String(format: "%.4f", data!.acceleration.x)
            let accel_Y = String(format: "%.4f", data!.acceleration.y)
            
            self.label_accel_X.text = accel_X
            self.lable_accel_Y.text = accel_Y
            
            if ((self.counter % 10) == 0) {
                let message = "{\"params\":{\"Accel_X\":\(accel_X),\"Accel_Y\":\(accel_Y)}}"
                self.mqtt.publish(MqttConstants.Topic, withString: message)
            }
            self.counter += 1
        })
    }

    // MARK: CocoaMQTTDelegate
    
    func mqtt(mqtt: CocoaMQTT, didConnect host: String, port: Int) {
        print("didConnect \(host):\(port)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
        print("didConnectAck \(ack.rawValue)")
        if ack == .ACCEPT {
            mqtt.subscribe(MqttConstants.Topic, qos: .QOS0)
            mqtt.ping()
            isConnected = true
            startUpdate()
        }
    }
    
    func mqtt(mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
        print("didPublishMessage with message: \(message.string!)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didPublishAck id: UInt16) {
        print("didPublishAck with id: \(id)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16 ) {
        print("didReceivedMessage: \(message.string!) with id \(id)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didSubscribeTopic topic: String) {
        print("didSubscribeTopic to \(topic)")
    }
    
    func mqtt(mqtt: CocoaMQTT, didUnsubscribeTopic topic: String) {
        print("didUnsubscribeTopic to \(topic)")
    }
    
    func mqttDidPing(mqtt: CocoaMQTT) {
        print("didPing")
    }
    
    func mqttDidReceivePong(mqtt: CocoaMQTT) {
        _console("didReceivePong")
    }
    
    func mqttDidDisconnect(mqtt: CocoaMQTT, withError err: NSError?) {
        _console("mqttDidDisconnect")
    }
    
    func _console(info: String) {
        print("Delegate: \(info)")
    }
}


//
//  AppDelegate.swift
//  mqtt_test
//
//  Created by Todotani on 2016/01/17.
//  Copyright © 2016年 Todotani. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    var viewController: ViewController!

    func applicationDidEnterBackground(application: UIApplication) {
        viewController.mqtt.disconnect()
        viewController.isConnected = false
        viewController.motionManager.stopAccelerometerUpdates()
    }

    func applicationWillEnterForeground(application: UIApplication) {
        viewController.mqtt.connect()
    }

}

注意点

  1. 20行目のAppIDには、Milkcocoa Webサイトで「新しいアプリ」を登録した際に付与されるAppIDを設定します。25行目のTopicは、「AppID/データストア名/push」の書式で記載します。今回の例では、データストア名としてTestを使っています。
  2. CocoaMQTTライブラリを使う際は、CocoaMQTTDelegateプロトコルに定義されているメソッドを実装する必要があります。今回の例ではViewControllerクラスにCocoaMQTTDelegateを適用(adopt)し、77行目以降にdelegateメッソドを記述しています。
  3. CocoaMQTTライブラリは非同期処理を使っており、connect()メッソドを呼ぶとすぐに処理が帰ってきますが、裏で接続処理が続いています。そのため、接続が完了するまではメッセージをpublishしてはいけません。接続が完了するとmqtt:didConnectAckメソッドが呼び出されるため、mqtt:didConnectAckにメッセージの送信開始を記述します。→ ソースの80行目
  4. PublishするメッセージはJSON形式で記述し、”params”をkeyにする必要があります。最初このkeyを書いておらず、送信したメッセージがサーバー側で正しく認識されないため、だいぶ悩みました。X/Y軸加速度をpublishするためのメッセージは以下のように記述します。
    {"params":{"Key_X":"X_Val", “Key_Y":"Y_Val"}}
    今回は70行目の処理で、JSONのひな形文字列内にvalue値を埋め込む汚い処理になっています。Dictionalyを使いKey/Valueを指定することで値を設定しJSONフォーマットにシリアライズする処理も考えたのですが、変換にゴミが入ってしまいうまくいかなかったので手抜きをしています。Arduino SDKはKey/Valueを指定してメッセージのデーターを生成できるのでこちらの方が汎用的です。

終わりに

Swiftが発表されてから長らく様子見だったのですが、サンプルコードも充実してきたのでiOS 9 & Swift 2.0からSwiftのお勉強を始めました。Objective-Cを使ったiOSのプログラミングも殆ど行ってはいないので、覚えた先から忘れているのですが、Objective-CよりSwiftの方がプログラミングもしやすくなってきました。

Milkcocoaはそんなに長時間の接続は試していませんが、フリーのmosquitto.orgより接続が安定しているように思います。今回のサンプルコードでは、publishしたメッセージをローカルにsubscribeしていますが配信の遅延もありません。

参考資料


Windows 10 IoT Core用のBME280ライブラリ

Windows 10 IoT Core用に、BME280センサーのライブラリを作ってみました。BME280は3.3V動作の温度・湿度・気圧センサーで、秋月さんSwitch Scienceさんから購入できます。

ESP-WROOM-02 ArduinoでBME280を使った際は、Embedded Adventuresさんのライブラリを使っていました。Windows 10 IoT用のライブラリに関しては、類似品であるBMP280用のライブラリならNuGetで取得することができるのですが、BME280用のライブラリは見つかりませんでした。そのため、Adafruitのforumで紹介されていたBMP280用のライブラリをベースに自作(改造)してみました。

BMP280とBME280はレジスターの構成や設定はほんとんど共通のため、湿度読み取り機能を追加し、読み取り値の補正関数をBME280用に修正しています。

またオリジナルのAdafruit版は、I2Cのからのセンサーデーター読み取りなどで非同期処理(async/await)を多用していたのですが、データ読み取りの時間は瞬時で非同期処理を入れるオーバーヘッド(非同期用の別タスクを起こす処理)の方が大きそうな気がしたため、デバイスの初期化部分以外では非同期処理を使わないように変更しました。

作成したライブラリのコードを以下に示します。GitHubでもプロジェエクトファイルを公開しました:

using System;
using System.Threading.Tasks;
using System.Diagnostics;
using Windows.Devices.Enumeration;
using Windows.Devices.I2c;

namespace Sensor.BME280
{
    public class BME280_CalibrationData
    {
        //BME280 Registers
        public ushort dig_T1 { get; set; }
        public short  dig_T2 { get; set; }
        public short  dig_T3 { get; set; }

        public ushort dig_P1 { get; set; }
        public short  dig_P2 { get; set; }
        public short  dig_P3 { get; set; }
        public short  dig_P4 { get; set; }
        public short  dig_P5 { get; set; }
        public short  dig_P6 { get; set; }
        public short  dig_P7 { get; set; }
        public short  dig_P8 { get; set; }
        public short  dig_P9 { get; set; }

        public byte   dig_H1 { get; set; }
        public short  dig_H2 { get; set; }
        public byte   dig_H3 { get; set; }
        public short  dig_H4 { get; set; }
        public short  dig_H5 { get; set; }
        public sbyte  dig_H6 { get; set; }
    }


    public class BME280
    {
        const byte BME280_Address   = 0x76;
        const byte BME280_Signature = 0x60;

        enum eRegisters : byte
        {
            BME280_REGISTER_DIG_T1 = 0x88,
            BME280_REGISTER_DIG_T2 = 0x8A,
            BME280_REGISTER_DIG_T3 = 0x8C,

            BME280_REGISTER_DIG_P1 = 0x8E,
            BME280_REGISTER_DIG_P2 = 0x90,
            BME280_REGISTER_DIG_P3 = 0x92,
            BME280_REGISTER_DIG_P4 = 0x94,
            BME280_REGISTER_DIG_P5 = 0x96,
            BME280_REGISTER_DIG_P6 = 0x98,
            BME280_REGISTER_DIG_P7 = 0x9A,
            BME280_REGISTER_DIG_P8 = 0x9C,
            BME280_REGISTER_DIG_P9 = 0x9E,

            BME280_REGISTER_DIG_H1 = 0xA1,
            BME280_REGISTER_DIG_H2 = 0xE1,
            BME280_REGISTER_DIG_H3 = 0xE3,
            BME280_REGISTER_DIG_H4_L = 0xE4,
            BME280_REGISTER_DIG_H4_H = 0xE5,
            BME280_REGISTER_DIG_H5_L = 0xE5,
            BME280_REGISTER_DIG_H5_H = 0xE6,
            BME280_REGISTER_DIG_H6 = 0xE7,

            BME280_REGISTER_CHIPID  = 0xD0,
            BME280_REGISTER_SOFTRESET = 0xE0,

            BME280_REGISTER_CONTROLHUMID = 0xF2,
            BME280_REGISTER_STATUS  = 0xF3,
            BME280_REGISTER_CONTROL = 0xF4,
            BME280_REGISTER_CONFIG  = 0xF5,

            BME280_REGISTER_PRESSUREDATA_MSB  = 0xF7,
            BME280_REGISTER_PRESSUREDATA_LSB  = 0xF8,
            BME280_REGISTER_PRESSUREDATA_XLSB = 0xF9, // bits <7:4>

            BME280_REGISTER_TEMPDATA_MSB  = 0xFA,
            BME280_REGISTER_TEMPDATA_LSB  = 0xFB,
            BME280_REGISTER_TEMPDATA_XLSB = 0xFC, // bits <7:4>

            BME280_REGISTER_HUMIDDATA_MSB = 0xFD,
            BME280_REGISTER_HUMIDDATA_LSB = 0xFE,
        };

        // Enables 2-wire I2C interface when set to ‘0’
        public enum interface_mode_e : byte
        {
            i2c = 0,
            spi = 1
        };

        // t_sb standby options - effectively the gap between automatic measurements 
        // when in "normal" mode
        public enum standbySettings_e : byte
        {
            tsb_0p5ms   = 0,
            tsb_62p5ms  = 1,
            tsb_125ms   = 2,
            tsb_250ms   = 3,
            tsb_500ms   = 4,
            tsb_1000ms  = 5,
            tsb_10ms    = 6,
            tsb_20ms    = 7
        };


        // sensor modes, it starts off in sleep mode on power on
        // forced is to take a single measurement now
        // normal takes measurements reqularly automatically
        public enum mode_e :byte
        {
            smSleep     = 0,
            smForced    = 1,
            smNormal    = 3
        };


        // Filter coefficients
        // higher numbers slow down changes, such as slamming doors
        public enum filterCoefficient_e : byte
        {
            fc_off  = 0,
            fc_2    = 1,
            fc_4    = 2,
            fc_8    = 3,
            fc_16   = 4
        };


        // Oversampling options for humidity
        // Oversampling reduces the noise from the sensor
        public enum oversampling_e : byte
        {
            osSkipped   = 0,
            os1x        = 1,
            os2x        = 2,
            os4x        = 3,
            os8x        = 4,
            os16x       = 5
        };


        //String for the friendly name of the I2C bus 
        private const string I2CControllerName = "I2C1";
        //Create an I2C device
        private I2cDevice bme280 = null;
        //Create new calibration data for the sensor
        private BME280_CalibrationData CalibrationData;

        // Value hold sensor operation parameters
        private byte int_mode = (byte)interface_mode_e.i2c;
        private byte t_sb;
        private byte mode;
        private byte filter;
        private byte osrs_p;
        private byte osrs_t;
        private byte osrs_h;

        public BME280(standbySettings_e t_sb = standbySettings_e.tsb_0p5ms, 
                      mode_e mode = mode_e.smNormal, 
                      filterCoefficient_e filter = filterCoefficient_e.fc_16,
                      oversampling_e osrs_p = oversampling_e.os16x, 
                      oversampling_e osrs_t = oversampling_e.os2x,
                      oversampling_e osrs_h = oversampling_e.os1x)
        {
            this.t_sb = (byte)t_sb;
            this.mode = (byte)mode;
            this.filter = (byte)filter;
            this.osrs_p = (byte)osrs_p;
            this.osrs_t = (byte)osrs_t;
            this.osrs_h = (byte)osrs_h;
        }


        //Method to initialize the BME280 sensor
        public async Task Initialize()
        {
            Debug.WriteLine("BME280::Initialize");

            try
            {
                //Instantiate the I2CConnectionSettings using the device address of the BME280
                I2cConnectionSettings settings = new I2cConnectionSettings(BME280_Address);
                //Set the I2C bus speed of connection to fast mode
                settings.BusSpeed = I2cBusSpeed.FastMode;
                //Use the I2CBus device selector to create an advanced query syntax string
                string aqs = I2cDevice.GetDeviceSelector(I2CControllerName);
                //Use the Windows.Devices.Enumeration.DeviceInformation class to create a collection using the advanced query syntax string
                DeviceInformationCollection dis = await DeviceInformation.FindAllAsync(aqs);
                //Instantiate the the BME280 I2C device using the device id of the I2CBus and the I2CConnectionSettings
                bme280 = await I2cDevice.FromIdAsync(dis[0].Id, settings);
                //Check if device was found
                if (bme280 == null)
                {
                    Debug.WriteLine("Device not found");
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine("Exception: " + e.Message + "\n" + e.StackTrace);
                throw;
            }

            byte[] readChipID = new byte[] { (byte)eRegisters.BME280_REGISTER_CHIPID };
            byte[] ReadBuffer = new byte[] { 0xFF };

            //Read the device signature
            bme280.WriteRead(readChipID, ReadBuffer);
            Debug.WriteLine("BME280 Signature: " + ReadBuffer[0].ToString());

            //Verify the device signature
            if (ReadBuffer[0] != BME280_Signature)
            {
                Debug.WriteLine("BME280::Begin Signature Mismatch.");
                return;
            }

            //Read the coefficients table
            CalibrationData = ReadCoefficeints();

            //Set configuration registers
            WriteConfigRegister();
            WriteControlMeasurementRegister();
            WriteControlRegisterHumidity();

            //Set configuration registers again to ensure configuration of humidity
            WriteConfigRegister();
            WriteControlMeasurementRegister();
            WriteControlRegisterHumidity();

            //Dummy read temp to setup t_fine
            ReadTemperature();
        }


        //Method to write the config register (default 16)
        //000  100  00 
        // ↑  ↑   ↑I2C mode
        // ↑  ↑Filter coefficient = 16
        // ↑t_sb = 0.5ms
        private void WriteConfigRegister()
        {
            byte value = (byte)(int_mode + (filter << 2) + (t_sb << 5));
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONFIG, value };
            bme280.Write(WriteBuffer);
            return;
        }

        //Method to write the control measurment register (default 87)
        //010  101  11 
        // ↑  ↑   ↑ mode
        // ↑  ↑ Pressure oversampling
        // ↑ Temperature oversampling
        private void WriteControlMeasurementRegister()
        {
            byte value = (byte)(mode + (osrs_p << 2) + (osrs_t << 5));
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONTROL, value };
            bme280.Write(WriteBuffer);
            return;
        }

        //Method to write the humidity control register (default 01)
        private void WriteControlRegisterHumidity()
        {
            byte value = osrs_h;
            byte[] WriteBuffer = new byte[] { (byte)eRegisters.BME280_REGISTER_CONTROLHUMID, value };
            bme280.Write(WriteBuffer);
            return;
        }


        //Method to read a 16-bit value from a register and return it in little endian format
        private ushort ReadUInt16_LittleEndian(byte register)
        {
            ushort value = 0;
            byte[] writeBuffer = new byte[] { 0x00 };
            byte[] readBuffer = new byte[] { 0x00, 0x00 };

            writeBuffer[0] = register;

            bme280.WriteRead(writeBuffer, readBuffer);
            int h = readBuffer[1] << 8;
            int l = readBuffer[0];
            value = (ushort)(h + l);
            return value;
        }

        //Method to read an 8-bit value from a register
        private byte ReadByte(byte register)
        {
            byte value = 0;
            byte[] writeBuffer = new byte[] { 0x00 };
            byte[] readBuffer = new byte[] { 0x00 };

            writeBuffer[0] = register;

            bme280.WriteRead(writeBuffer, readBuffer);
            value = readBuffer[0];
            return value;
        }

        //Method to read the caliberation data from the registers
        private BME280_CalibrationData ReadCoefficeints()
        {
            // 16 bit calibration data is stored as Little Endian, the helper method will do the byte swap.
            CalibrationData = new BME280_CalibrationData();

            // Read temperature calibration data
            CalibrationData.dig_T1 = ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T1);
            CalibrationData.dig_T2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T2);
            CalibrationData.dig_T3 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_T3);

            // Read presure calibration data
            CalibrationData.dig_P1 = ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P1);
            CalibrationData.dig_P2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P2);
            CalibrationData.dig_P3 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P3);
            CalibrationData.dig_P4 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P4);
            CalibrationData.dig_P5 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P5);
            CalibrationData.dig_P6 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P6);
            CalibrationData.dig_P7 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P7);
            CalibrationData.dig_P8 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P8);
            CalibrationData.dig_P9 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_P9);

            // Read humidity calibration data
            CalibrationData.dig_H1 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H1);
            CalibrationData.dig_H2 = (short)ReadUInt16_LittleEndian((byte)eRegisters.BME280_REGISTER_DIG_H2);
            CalibrationData.dig_H3 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H3);
            short e4 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H4_L);    // Read 0xE4
            short e5 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H4_H);    // Read 0xE5
            CalibrationData.dig_H4 = (short)((e4 << 4) + (e5 & 0x0F));
            short e6 = ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H5_H);    // Read 0xE6
            CalibrationData.dig_H5 = (short)((e5 >> 4) + (e6 << 4));
            CalibrationData.dig_H6 = (sbyte)ReadByte((byte)eRegisters.BME280_REGISTER_DIG_H6);

            return CalibrationData;
        }


        //t_fine carries fine temperature as global value
        int t_fine;

        //Method to return the temperature in DegC. Resolution is 0.01 DegC. Output value of “51.23” equals 51.23 DegC.
        private double BME280_compensate_T_double(int adc_T)
        {
            double var1, var2, T;

            //The temperature is calculated using the compensation formula in the BME280 datasheet
            var1 = (adc_T / 16384.0 - CalibrationData.dig_T1 / 1024.0) * CalibrationData.dig_T2;
            var2 = ((adc_T / 131072.0 - CalibrationData.dig_T1 / 8192.0) * 
                (adc_T / 131072.0 - CalibrationData.dig_T1 / 8192.0)) * CalibrationData.dig_T3;

            t_fine = (int)(var1 + var2);

            T = (var1 + var2) / 5120.0;
            return T;
        }


        //Method to returns the pressure in Pa, in Q24.8 format (24 integer bits and 8 fractional bits).
        //Output value of “24674867” represents 24674867/256 = 96386.2 Pa = 963.862 hPa
        private long BME280_compensate_P_Int64(int adc_P)
        {
            long var1, var2, p;

            //The pressure is calculated using the compensation formula in the BME280 datasheet
            var1 = (long)t_fine - 128000;
            var2 = var1 * var1 * CalibrationData.dig_P6;
            var2 = var2 + ((var1 * CalibrationData.dig_P5) << 17);
            var2 = var2 + ((long)CalibrationData.dig_P4 << 35);
            var1 = ((var1 * var1 * CalibrationData.dig_P3) >> 8) + ((var1 * CalibrationData.dig_P2) << 12);
            var1 = (((long)1 << 47) + var1) * CalibrationData.dig_P1 >> 33;
            if (var1 == 0)
            {
                Debug.WriteLine("BME280_compensate_P_Int64 Jump out to avoid / 0");
                return 0; //Avoid exception caused by division by zero
            }
            //Perform calibration operations as per datasheet: 
            p = 1048576 - adc_P;
            p = (((p << 31) - var2) * 3125) / var1;
            var1 = ((long)CalibrationData.dig_P9 * (p >> 13) * (p >> 13)) >> 25;
            var2 = ((long)CalibrationData.dig_P8 * p) >> 19;
            p = ((p + var1 + var2) >> 8) + ((long)CalibrationData.dig_P7 << 4);
            return p;
        }


        // Returns humidity in %rH as as double. Output value of “46.332” represents 46.332 %rH
        private double BME280_compensate_H_double(int adc_H)
        {
            double var_H;

            var_H = t_fine - 76800.0;
            var_H = (adc_H - (CalibrationData.dig_H4 * 64.0 + CalibrationData.dig_H5 / 16384.0 * var_H)) *
                CalibrationData.dig_H2 / 65536.0 * (1.0 + CalibrationData.dig_H6 / 67108864.0 * var_H *
                (1.0 + CalibrationData.dig_H3 / 67108864.0 * var_H));
            var_H = var_H * (1.0 - CalibrationData.dig_H1 * var_H / 524288.0);

            if (var_H > 100.0)
            {
                Debug.WriteLine("BME280_compensate_H_double Jump out to 100%");
                var_H = 100.0;
            } else if (var_H < 0.0)
            {
                Debug.WriteLine("BME280_compensate_H_double Jump under 0%");
                var_H = 0.0;
            }

            return var_H;
        }


        public float ReadTemperature()
        {
            //Read the MSB, LSB and bits 7:4 (XLSB) of the temperature from the BME280 registers
            byte tmsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_MSB);
            byte tlsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_LSB);
            byte txlsb = ReadByte((byte)eRegisters.BME280_REGISTER_TEMPDATA_XLSB); // bits 7:4

            //Combine the values into a 32-bit integer
            int t = (tmsb << 12) + (tlsb << 4) + (txlsb >> 4);

            //Convert the raw value to the temperature in degC
            double temp = BME280_compensate_T_double(t);

            //Return the temperature as a float value
            return (float)temp;
        }

        public float ReadPreasure()
        {
            //Read the MSB, LSB and bits 7:4 (XLSB) of the pressure from the BME280 registers
            byte pmsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_MSB);
            byte plsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_LSB);
            byte pxlsb = ReadByte((byte)eRegisters.BME280_REGISTER_PRESSUREDATA_XLSB); // bits 7:4

            //Combine the values into a 32-bit integer
            int p = (pmsb << 12) + (plsb << 4) + (pxlsb >> 4);

            //Convert the raw value to the pressure in Pa
            long pres = BME280_compensate_P_Int64(p);

            //Return the pressure as a float value
            return ((float)pres) / 256;
        }

        public float ReadHumidity()
        {
            //Read the MSB and LSB of the humidity from the BME280 registers
            byte hmsb = ReadByte((byte)eRegisters.BME280_REGISTER_HUMIDDATA_MSB);
            byte hlsb = ReadByte((byte)eRegisters.BME280_REGISTER_HUMIDDATA_LSB);

            //Combine the values into a 32-bit integer
            int h = (hmsb << 8) + hlsb;

            //Convert the raw value to the humidity in %
            double humidity = BME280_compensate_H_double(h);

            //Return the humidity as a float value
            return (float)humidity;
        }

        //Method to take the sea level pressure in Hectopascals(hPa) as a parameter and calculate the altitude using current pressure.
        public float ReadAltitude(float seaLevel)
        {
            //Read the pressure first
            float pressure = ReadPreasure();
            //Convert the pressure to Hectopascals(hPa)
            pressure /= 100;

            //Calculate and return the altitude using the international barometric formula
            return 44330.0f * (1.0f - (float)Math.Pow((pressure / seaLevel), 0.1903f));
        }
    }
}

ライブラリを使用したセンサーデーター読み取りアプリのサンプルは以下です:

using System;
using System.Threading;
using System.Diagnostics;
using Windows.UI.Xaml.Controls;
using Sensor.BME280;


namespace BME280_Test
{
    public sealed partial class MainPage : Page
    {
        private BME280 bme280;
        private Timer periodicTimer;

        public MainPage()
        {
            this.InitializeComponent();

            bme280 = new BME280();
            initBme280();
        }

        private async void initBme280()
        {
            await bme280.Initialize();
            periodicTimer = new Timer(this.TimerCallback, null, 0, 1000);
        }

        private void TimerCallback(object state)
        {
            var temp = bme280.ReadTemperature();
            var press = bme280.ReadPreasure() / 100;
            var humidity = bme280.ReadHumidity();
            var alt  = bme280.ReadAltitude(1013);   // 1013hPa = pressure at 0m
            Debug.WriteLine("Temp:{0:F2}℃ Humidity:{1:F2}% Press:{2:F2}hPa Alt:{3:F0}m", temp, humidity, press, alt);

            var task = this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                tempValue.Text = temp.ToString("F2") + "℃";
                humValue.Text = humidity.ToString("F2") + "%";
                pressValue.Text = press.ToString("F2") + "hPa";
            });

        }
    }
}

電源オンの初回起動時に湿度情報が読み取れない問題があり(一旦アプリを終了して再度起動すると正常になる)、回避策としてレジスタの初期化を2回行っています(ライブラリソースの226行目)。Arudino用のライブラリコードを見ると設定を2回やるような処理は行っておらず、この点は謎です。

BME280のセンサーとしての精度ですが、手持ちのAM2302に比べて温度が高め、湿度が低めに出ます。湿度は5%以上の差分があり、センサー種別によって測定値に大きなばらつきがあります。温度はその他手持ちの温度計と比較するとAM2302の方が正確なため、湿度もAM2302の方が正確ではないかと思います。

« 2015年12月 | トップページ | 2016年2月 »

2017年2月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28        
無料ブログはココログ