空気品質センサとサーボモータで環境の悪化を通知する

 前回ひとまず空気品質センサ CCS811 で二酸化炭素濃度などを測定してみました。今回は二酸化炭素濃度が閾値を超えたら何か目に見える形で通知するようにしてみたいと思います。オフィスの二酸化炭素濃度は業務のパフォーマンスにも影響するということで、チームラボさんも測定して改善されているようなので、二酸化炭素濃度が高くなっていることがわかるようにしてみたいと思います。

ch.nicovideo.jp

サーボモータを使ってみる

 二酸化炭素濃度が閾値を超えたら Slack に通知するというのをそのうちやりたいとは思ってるんですが、それ自体には自分としてはそんなに目新しさもなかったので、もうちょっとギミック的なものが作れないかなと、サーボモータを使ってみることにしました。

www.switch-science.com

 サーボモータは今まで使ったことなかったのですが、使い方は簡単で、 Arduino ではサーボモータ用のライブラリが標準で用意されているので、それを使用します。まずは servo.attach() で制御用のピンを指定して、 servo.write() で角度を指定するだけです。

#include <Servo.h>

Servo servo;

servo.attach(3); // 制御ピンに 3番ピンを指定する例
servo.write(90); // サーボの角度を 90度に指定する例

 今回はサーボモータに LED を組み合わせて、二酸化炭素濃度が閾値を超えたら LED が点灯しつつ持ち上がるようにしてみたいと思います。

回路図

 だいぶごちゃっとしてしまいましたが、回路図は下記のようになりました。

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

 CCS811 の配線については基本は前回と同じですが、今回はサーボモータを使う都合で Seeeduino の動作電圧を 5V にしたので、 CCS811 の NOT_WAK ピンへの入力電圧が 3.3V 以下になるように、4.7kΩ の抵抗を使用して分圧しています。

 サーボモータの電源は 5V 出力から取り、制御ピンは 3番ピンを使用しています。

 LED には 330Ωの抵抗を使用しています。

ファームウェア

 ファームウェアも基本的には前回と同様で、サーボモータと LED の初期化と、二酸化炭素濃度の測定値によってサーボモータと LED を制御する処理を追加しています。

#include <SparkFunCCS811.h>
#include <Servo.h>

#define CCS811_ADDR 0x5B

#define PIN_NOT_WAKE 5
#define PIN_NOT_INT 6
#define PIN_SERVO 3
#define PIN_LED 4

#define DEGREE_CLEAN_AIR 90
#define DEGREE_DIRTY_AIR 0
#define CO2_THRESHOLD 1000

CCS811 myCCS811(CCS811_ADDR);
Servo servo;

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("...");

  CCS811Core::status returnCode;
  
  returnCode = myCCS811.begin();
  Serial.print("CCS811 begin exited with: ");
  printDriverError( returnCode );
  Serial.println();
  
  returnCode = myCCS811.setDriveMode(2);
  Serial.print("Mode request exited with: ");
  printDriverError( returnCode );
  Serial.println();
  
  pinMode(PIN_NOT_INT, INPUT_PULLUP);
  returnCode = myCCS811.enableInterrupts();
  Serial.print("Interrupt configuation exited with: ");
  printDriverError( returnCode );
  Serial.println();
  
  pinMode(PIN_NOT_WAKE, OUTPUT);
  digitalWrite(PIN_NOT_WAKE, 1);
  
  servo.attach(PIN_SERVO);
  servo.write(90);
  
  pinMode(PIN_LED, OUTPUT);
}

void loop() {
  if (digitalRead(PIN_NOT_INT) == 0)
  {
    digitalWrite(PIN_NOT_WAKE, 0);
    delay(1);
    myCCS811.readAlgorithmResults();
    
    Serial.print("CO2[");
    Serial.print(myCCS811.getCO2());
    Serial.print("] tVOC[");
    Serial.print(myCCS811.getTVOC());
    Serial.print("] millis[");
    Serial.print(millis());
    Serial.print("]");
    Serial.println();
    
    if (myCCS811.getCO2() >= CO2_THRESHOLD)
    {
      digitalWrite(PIN_LED, HIGH);
      servo.write(DEGREE_DIRTY_AIR);
    }
    else
    {
      digitalWrite(PIN_LED, LOW);
      servo.write(DEGREE_CLEAN_AIR);
    }
    
    digitalWrite(PIN_NOT_WAKE, 1);
    delay(1);
  }
}

void printDriverError( CCS811Core::status errorCode )
{
  switch ( errorCode )
  {
    case CCS811Core::SENSOR_SUCCESS:
      Serial.print("SUCCESS");
      break;
    case CCS811Core::SENSOR_ID_ERROR:
      Serial.print("ID_ERROR");
      break;
    case CCS811Core::SENSOR_I2C_ERROR:
      Serial.print("I2C_ERROR");
      break;
    case CCS811Core::SENSOR_INTERNAL_ERROR:
      Serial.print("INTERNAL_ERROR");
      break;
    case CCS811Core::SENSOR_GENERIC_ERROR:
      Serial.print("GENERIC_ERROR");
      break;
    default:
      Serial.print("Unspecified error.");
  }
}

動作時の様子

 だいぶ雑多な感じですが、とりあえず組んでみた回路は下記のようになりました。

f:id:akanuma-hiroaki:20180619075828j:plain

 二酸化炭素濃度が閾値を超えると下記画像のようにサーボモータが 90度回転して、 LED も点灯します。

f:id:akanuma-hiroaki:20180619075858j:plain

まとめ

 今回はとりあえず配線した感じでしたが、もっと配線をすっきりさせて筐体に入れて、サーボが動いた時だけ筐体から出て見えるようにしたり、何か工夫できると面白そうかなと思いました。また、 LED はワニ口クリップで配線してるのですが、下手をするとショートする可能性もあるので、継続的に使うにはこの辺の安全性も改善したいところです。

空気品質センサ CCS811 で二酸化炭素濃度などを測定してみる

 最近オフィス内の人数も増えて来て、換気が不十分でオフィス内の空気が悪いと感じるメンバーが増えて来たこともあり、試しに二酸化炭素濃度などを測ってみようと下記センサーを買ってみました。

www.switch-science.com

 今回はとりあえず下記チュートリアルに従って、正しくセンサーを動作させることができるのか、どんな値が取れるのかを確認してみたいと思います。

CCS811 Air Quality Breakout Hookup Guide - learn.sparkfun.com

ピンヘッダ実装

 モジュールにはピンヘッダが実装されていないので、まずはピンヘッダを下記写真のように半田付けします。半田の量がまちまちなのはご愛嬌ということで。

f:id:akanuma-hiroaki:20180610212741j:plain

 NTCピンにNTCサーミスタを実装すれば温度を測定して空気測定値の補正に利用できるらしいのですが、今回はまだNTCサーミスタは入手していないので、とりあえずなしで使ってみます。

シンプルな計測サンプル

 まずは最もシンプルな構成で空気品質を測定してみたいと思います。実際は Seeeduino を利用したのですが、 Seeeduino の Fritzing のデータでは I2C の SCL と SDA を接続しているピンが不足していたので、回路図では Arduino を使用しています。また、 Seeeduino では動作電圧をスイッチで 5V か 3.3V に切り替えられますが、今回は CCS811 の動作電圧が 3.3V なので、 3.3V で動作させています。

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

 では次にファームウェアの実装です。 CCS811 の Arduino 用ライブラリが SparkFun から提供されていますのでこれを使用します。

github.com

 Arduino Web Editor を使っていれば上記ライブラリはダウンロードやインポートをしなくてもそのまま利用できます。

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

 コードはチュートリアルとほぼ同様ですが、下記のような実装になります。

#include <SparkFunCCS811.h>

#define CCS811_ADDR 0x5B

CCS811 mySensor(CCS811_ADDR);

void setup() {
  Serial.begin(115200);
  Serial.println("CCS811 Basic Example");
  
  CCS811Core::status returnCode = mySensor.begin();
  if (returnCode != CCS811Core::SENSOR_SUCCESS)
  {
    Serial.println(".begin() returned with an error.");
    while (1);
  }
}

void loop() {
  if (mySensor.dataAvailable())
  {
    mySensor.readAlgorithmResults();
    
    Serial.print("CO2[");
    Serial.print(mySensor.getCO2());
    Serial.print("] tVOC[");
    Serial.print(mySensor.getTVOC());
    Serial.print("] millis[");
    Serial.print(millis());
    Serial.print("]");
    Serial.println();
  }
  
  delay(10);
}

 setup() の中で mySensor.begin() することでセンサーを初期化し、失敗した場合はそこでスタックさせています。

 loop() の中では mySensor.dataAvailable() でまだ読み取っていない新しいデータが存在するかをチェックし、存在する場合は mySensor.readAlgorithmResults() で TVOC と eCO2 のレベルを計算させ、 mySensor.getCO2() と mySensor.getTVOC() でそれぞれの値を取得しています。

 また、このセンサーでは48時間のエージングと、20分のコンディショニングが推奨されていて、初めて実行した場合は下記のように有効な値が取得されません。

CO2[400] tVOC[0] millis[5996]
 tVOC[0] millis[6991]
CCS811 Basic Example
CO2[0] tVOC[0] millis[2017]
CO2[0] tVOC[0] millis[3011]
CO2[0] tVOC[0] millis[4006]
CO2[400] tVOC[0] millis[5002]
CO2[400] tVOC[0] millis[5996]
CO2[400] tVOC[0] millis[6991]
CO2[400] tVOC[0] millis[7987]
CO2[400] tVOC[0] millis[8981]
CO2[400] tVOC[0] millis[9976]
CO2[400] tVOC[0] millis[10972]
CO2[400] tVOC[0] millis[11966]
CO2[400] tVOC[0] millis[12961]
CO2[400] tVOC[0] millis[13957]
CO2[400] tVOC[0] millis[14952]

 48時間のエージングはまだ実行できていませんが、とりあえず20分のコンディショニング後は下記のように値が取れ始めました。

CO2[408] tVOC[1] millis[1224601]
CO2[408] tVOC[1] millis[1225595]
CO2[408] tVOC[1] millis[1226591]
CO2[408] tVOC[1] millis[1227586]
CO2[413] tVOC[1] millis[1228581]
CO2[415] tVOC[2] millis[1229577]
CO2[415] tVOC[2] millis[1230572]
CO2[415] tVOC[2] millis[1231566]
CO2[415] tVOC[2] millis[1232562]
CO2[415] tVOC[2] millis[1233557]
CO2[415] tVOC[2] millis[1234552]
CO2[418] tVOC[2] millis[1235548]
CO2[418] tVOC[2] millis[1236533]
CO2[413] tVOC[1] millis[1237528]
CO2[408] tVOC[1] millis[1238523]
CO2[408] tVOC[1] millis[1239519]
CO2[408] tVOC[1] millis[1240513]
CO2[408] tVOC[1] millis[1241508]
CO2[408] tVOC[1] millis[1242504]
CO2[405] tVOC[0] millis[1243499]

 二酸化炭素濃度なので、息を吹きかけると下記のように値が上昇するのがわかります。

CO2[817] tVOC[63] millis[1319103]
CO2[905] tVOC[76] millis[1320087]
CO2[1090] tVOC[105] millis[1321082]
CO2[1044] tVOC[98] millis[1322078]
CO2[1380] tVOC[149] millis[1323073]
CO2[967] tVOC[86] millis[1324068]
CO2[1113] tVOC[108] millis[1325064]
CO2[1316] tVOC[139] millis[1326059]
CO2[844] tVOC[67] millis[1327054]
CO2[1095] tVOC[105] millis[1328050]
CO2[825] tVOC[64] millis[1329045]
CO2[1215] tVOC[124] millis[1330040]
CO2[1349] tVOC[144] millis[1331036]
CO2[770] tVOC[56] millis[1332031]
CO2[1380] tVOC[149] millis[1333026]

 一度電源をOFFにしてまた起動すると下記のように初回起動と同様の状態になったので、コンディショニングは起動時に毎回必要なようです。

CCS811 Basic Example
CO2[0] tVOC[0] millis[2017]
CO2[0] tVOC[0] millis[3011]
CO2[0] tVOC[0] millis[4006]
CO2[400] tVOC[0] millis[5002]
CO2[400] tVOC[0] millis[5998]
CO2[400] tVOC[0] millis[6982]
CO2[400] tVOC[0] millis[7977]
CO2[400] tVOC[0] millis[8972]
CO2[400] tVOC[0] millis[9967]
CO2[400] tVOC[0] millis[10962]
CO2[400] tVOC[0] millis[11957]
CO2[400] tVOC[0] millis[12952]
CO2[400] tVOC[0] millis[13947]
CO2[400] tVOC[0] millis[14943]
CO2[400] tVOC[0] millis[15937]
CO2[400] tVOC[0] millis[16932]
CO2[400] tVOC[0] millis[17928]

Interrupt と Wake を使ったサンプル

 もう1つのサンプルとして、 Interrupt と Wake を使った例を試してみます。 CCS811 ではセンサーをスリープさせることで消費電力を抑え、新しいデータが取得された時にスリープを解除して値を取得することができます。回路図としては下記のように、先程までのものに加えて NOT WAKE ピンと NOT INT ピンへの接続を追加しています。チュートリアルでは動作電圧が 5V のマイコンを使っているため、 NOT WAKE ピンへの接続は電圧が 3.3V 以下になるように抵抗の接続などで電圧を下げていますが、今回は Seeeduino を 3.3V で動作させているので、直接接続しています。

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

 ファームウェアの実装としては下記のようになります。 setup() の中で myCCS811.begin() するのは先程と同じですが、 myCCS811.setDriveMode(2) でデータ取得間隔を 10 秒に変更しています。また、 myCCS811.enableInterrupts() でデータ取得時の Interrupt を有効にしています。 pinMode() で NOT INT ピンをインプット用、 NOT WAKE ピンをアウトプット用として設定し、 NOT WAKE ピンは High 、つまりスリープ状態にしておきます。

 新しいデータが取得された時は NOT INT ピンが Low になるので、 loop() の中では NOT INT ピンの状態をチェックし、 Low になった(新しいデータが取得された)時に NOT WAKE ピンを Low にしてスリープ状態を解除し、 myCCS811.readAlgorithmResults() でレベルを計算させてからそれぞれの値を取得しています。

 NOT WAKE ピンの状態の切り替えには 20μs〜50μs かかるので、切り替え時には 1ms の delay を挟んでいます。

#include <SparkFunCCS811.h>

#define CCS811_ADDR 0x5B

#define PIN_NOT_WAKE 5
#define PIN_NOT_INT 6

CCS811 myCCS811(CCS811_ADDR);

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("...");

  CCS811Core::status returnCode;
  
  returnCode = myCCS811.begin();
  Serial.print("CCS811 begin exited with: ");
  printDriverError( returnCode );
  Serial.println();
  
  returnCode = myCCS811.setDriveMode(2);
  Serial.print("Mode request exited with: ");
  printDriverError( returnCode );
  Serial.println();
  
  pinMode(PIN_NOT_INT, INPUT_PULLUP);
  returnCode = myCCS811.enableInterrupts();
  Serial.print("Interrupt configuation exited with: ");
  printDriverError( returnCode );
  Serial.println();
  
  pinMode(PIN_NOT_WAKE, OUTPUT);
  digitalWrite(PIN_NOT_WAKE, 1);
}

void loop() {
  if (digitalRead(PIN_NOT_INT) == 0)
  {
    digitalWrite(PIN_NOT_WAKE, 0);
    delay(1);
    myCCS811.readAlgorithmResults();
    
    Serial.print("CO2[");
    Serial.print(myCCS811.getCO2());
    Serial.print("] tVOC[");
    Serial.print(myCCS811.getTVOC());
    Serial.print("] millis[");
    Serial.print(millis());
    Serial.print("]");
    Serial.println();
    
    digitalWrite(PIN_NOT_WAKE, 1);
    delay(1);
  }
}

void printDriverError( CCS811Core::status errorCode )
{
  switch ( errorCode )
  {
    case CCS811Core::SENSOR_SUCCESS:
      Serial.print("SUCCESS");
      break;
    case CCS811Core::SENSOR_ID_ERROR:
      Serial.print("ID_ERROR");
      break;
    case CCS811Core::SENSOR_I2C_ERROR:
      Serial.print("I2C_ERROR");
      break;
    case CCS811Core::SENSOR_INTERNAL_ERROR:
      Serial.print("INTERNAL_ERROR");
      break;
    case CCS811Core::SENSOR_GENERIC_ERROR:
      Serial.print("GENERIC_ERROR");
      break;
    default:
      Serial.print("Unspecified error.");
  }
}

 これを実行すると下記のように値が取得されます。 CO2 が 1,000 を超えている部分は息を吹きかけてみたところです。

...
CCS811 begin exited with: SUCCESS
Mode request exited with: SUCCESS
20
28
Interrupt configuation exited with: SUCCESS
CO2[0] tVOC[0] millis[11563]
CO2[0] tVOC[0] millis[21618]
CO2[0] tVOC[0] millis[31675]
CO2[400] tVOC[0] millis[41733]
CO2[400] tVOC[0] millis[51789]
CO2[402] tVOC[0] millis[61845]
CO2[407] tVOC[1] millis[71902]
CO2[407] tVOC[1] millis[81959]
CO2[402] tVOC[0] millis[92016]
CO2[410] tVOC[1] millis[102073]
CO2[1228] tVOC[126] millis[112129]
CO2[1118] tVOC[109] millis[122182]
CO2[1245] tVOC[128] millis[132231]

まとめ

 今回はとりあえず値を取得してみるところまででしたが、エージングやコンディショニングに時間がかかるのが少々ネックではあるものの、値の取得自体は簡単にできたので、次回は値が一定値を超えた場合に何かしらの処理をしたり、ネットワーク通信などを取り入れて、オフィスの二酸化炭素濃度が一定値を超えたら通知するような物を作ってみようかと思います。

GPS モジュールで位置情報を取得してみる

 GPSモジュールで位置情報を使って何かやってみたかったので、下記モジュールを買って位置情報の取得を試してみました。今回は Seeeduino から GPS モジュールで取得した位置情報を読み出してみます。

www.switch-science.com

 チュートリアルが下記ページで公開されているので、この内容に沿って試してみました。

チュートリアルページ
https://learn.adafruit.com/adafruit-ultimate-gps

ピンヘッダの半田付け

 モジュールにはピンヘッダが同梱されていますが装着はされていないので、ブレッドボードで試せるように下記写真のように半田付けしました。

f:id:akanuma-hiroaki:20180603002826j:plain

シリアルポートから直接読み出す

 まずは GPS モジュールを Seeeduino の シリアル - USB コンバータに接続して直接位置情報を読み出してみます。配線は下記の回路図のようになります。 GPS モジュールの TX/RX ポートを Seeeduino の TX/RX ポートに接続していますので、 Seeeduino のチップは通さずに値を読み出す形になります。 電源電圧は 3.3V - 5V で、 Seeeduino の 5V 出力から GPS モジュールの VIN に入力しています。 Seeeduino は動作電圧も 3.3V か 5V を切り替えられるようになっていますが、今回は 5V で使用しています。

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

 GPS モジュールは取得した位置情報(FIX)を毎秒シリアルポートに出力します。今回はシリアルモニタで直接確認するだけなので、 Seeeduino のファームウェア側でやることは特にないため、コードは下記のようにメソッド定義のみで Seeeduino にフラッシュします。

void setup() {    
}

void loop() {
}

 正常にフラッシュされると GPS モジュールからの値の出力が開始されます。今回は Arduino Web Editor を使い、下記画像のようにシリアルモニターで出力を確認してみました。

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

 GPS の位置情報は NMEA というフォーマットで出力されます。 NMEA の中でもいくつかのデータタイプがあり、各行の先頭でその行がどのデータタイプのデータかを示しています。よく使われるのは GPRMC というデータタイプのようで、チュートリアルでも GPRMC を対象としているので、今回は GPRMC について確認します。

 また、GPS モジュールでは FIX が取得できてもできなくても値を出力します。上記画像では室内で動作させていたため FIX が取得できず、カンマばかりで出力されていますが、屋外で動作させると下記のように FIX が取得できます。(緯度・経度はサンプル値に置き換えてあります)

$GPRMC,081033.000,A,1234.5678,N,12345.6789,E,0.36,168.34,020618,,,A*65

 それぞれの値の意味は下記の通りです。

$GPRMC: データタイプ

081033.000: UTCにおけるタイムスタンプ。先頭から2桁ずつ 時、分、秒を表し、ドット以降の3桁がミリ秒を表す。(08:10:33.000 UTC)

A: ステータスコード。A は FIX が取得できていること(Active)を表し、 V は FIX が取得できていないこと(Void)を表す。

1234.5678,N: 緯度情報。先頭2〜3桁が度数を表し、ドットの前の2桁以降で分数を表す。また、 N は北緯(North)を表し、南緯(South)の場合は S で表される。(北緯 12度 34.5678分)

12345.6789,E: 経度情報。先頭2〜3桁が度数を表し、ドットの前の2桁以降で分数を表す。また、 E は東経(East)を表し、西経(West)の場合は W で表される。(東経 123度 45.6789分)

0.36: 移動速度(ノット)

168.34: 移動方向(168.34度)

020618: UTCにおける日付。フォーマットは dd/mm/yy。(2018/06/02)

A*65: チェックサム

 ちなみに Google Map では緯度、経度については N, S, W, E の代わりに +/- を使用する必要があります。N, E は +、 S, W は - で表しますので、上記の内容は +12 34.5678', +123 45.6789' とする必要があります。

Seeeduino 経由で位置情報を読み出す

 では次に Seeeduino を経由して GPS モジュールの位置情報を読み出してみたいと思います。配線図は下記の通りです。 GPS モジュールの TX/RX ピンへの接続をそれぞれ Seeduino の 3番ピン/2番ピン に変更しただけです。

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

 続いてファームウェアを用意します。今回使用している GPS モジュールについては Adafruit から Arduino 用のライブラリが提供されています。ライブラリにはサンプルコードも含まれているので今回はそれをそのまま使用してみます。

github.com

 Arduino Web Editor であれば上記サイトからダウンロードしなくても、 Library Manager から Favorite に登録して使うことができます。

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

 ライブラリの中の Examples フォルダにサンプルコードが入っていますので、今回はその中から echo というサンプルを使用します。

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

 コンパイルと Seeeduino へのフラッシュができるように自分のスペースにサンプルコードを保存します。

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

 コピーが保存されたらそのまま Seeduino にフラッシュすると、先ほどと同様に GPS モジュールから取得された位置情報がシリアルモニタに出力されるようになります。

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

 ちなみにサンプルコードからコメント行を取り除いたコードは下記のようになります。今回の GPS モジュールではデータの出力頻度を 1Hz, 5Hz, 10Hz から選択できるようになっていて、下記のコードは 1Hz に設定した例になります。

#include <Adafruit_GPS.h>

#include <Adafruit_GPS.h>
#if ARDUINO >= 100
 #include <SoftwareSerial.h>
#else
#endif

#if ARDUINO >= 100
  SoftwareSerial mySerial(3, 2);
#else
  NewSoftSerial mySerial(3, 2);
#endif
Adafruit_GPS GPS(&mySerial);


#define GPSECHO  true

boolean usingInterrupt = false;
void useInterrupt(boolean); // Func prototype keeps Arduino 0023 happy

void setup()  
{    
  Serial.begin(115200);
  Serial.println("Adafruit GPS library basic test!");

  GPS.begin(9600);
  
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);
  GPS.sendCommand(PMTK_API_SET_FIX_CTL_1HZ);

  GPS.sendCommand(PGCMD_ANTENNA);

  useInterrupt(true);
  
  delay(1000);
}

SIGNAL(TIMER0_COMPA_vect) {
  char c = GPS.read();
  if (GPSECHO)
    if (c) UDR0 = c;  
}

void useInterrupt(boolean v) {
  if (v) {
    OCR0A = 0xAF;
    TIMSK0 |= _BV(OCIE0A);
    usingInterrupt = true;
  } else {
    TIMSK0 &= ~_BV(OCIE0A);
    usingInterrupt = false;
  }
}


void loop()                     // run over and over again
{
}

位置情報のパースサンプル

 位置情報の生データはそのままではみづらいので、サンプルの中からもう1つ、位置情報をパースしている例を見てみます。先ほどの echo の場合と同じような形で、今回は parsing というサンプルコードをコピーします。

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

 コメントを除いたコードは下記のようになります。 GPS ライブラリを使用し、パースした位置情報をシリアルモニタに出力しています。

#include <Adafruit_GPS.h>
#include <SoftwareSerial.h>

SoftwareSerial mySerial(3, 2);

Adafruit_GPS GPS(&mySerial);

#define GPSECHO  true

boolean usingInterrupt = false;
void useInterrupt(boolean); // Func prototype keeps Arduino 0023 happy

void setup()  
{
  Serial.begin(115200);
  Serial.println("Adafruit GPS library basic test!");

  GPS.begin(9600);
  
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);   // 1 Hz update rate

  GPS.sendCommand(PGCMD_ANTENNA);

  useInterrupt(true);

  delay(1000);
  mySerial.println(PMTK_Q_RELEASE);
}

SIGNAL(TIMER0_COMPA_vect) {
  char c = GPS.read();
#ifdef UDR0
  if (GPSECHO)
    if (c) UDR0 = c;  
#endif
}

void useInterrupt(boolean v) {
  if (v) {
    OCR0A = 0xAF;
    TIMSK0 |= _BV(OCIE0A);
    usingInterrupt = true;
  } else {
    TIMSK0 &= ~_BV(OCIE0A);
    usingInterrupt = false;
  }
}

uint32_t timer = millis();
void loop()                     // run over and over again
{
  if (! usingInterrupt) {
    char c = GPS.read();
    if (GPSECHO)
      if (c) Serial.print(c);
  }
  
  if (GPS.newNMEAreceived()) {
    if (!GPS.parse(GPS.lastNMEA()))   // this also sets the newNMEAreceived() flag to false
      return;  // we can fail to parse a sentence in which case we should just wait for another
  }

  if (timer > millis())  timer = millis();

  if (millis() - timer > 2000) { 
    timer = millis(); // reset the timer
    
    Serial.print("\nTime: ");
    Serial.print(GPS.hour, DEC); Serial.print(':');
    Serial.print(GPS.minute, DEC); Serial.print(':');
    Serial.print(GPS.seconds, DEC); Serial.print('.');
    Serial.println(GPS.milliseconds);
    Serial.print("Date: ");
    Serial.print(GPS.day, DEC); Serial.print('/');
    Serial.print(GPS.month, DEC); Serial.print("/20");
    Serial.println(GPS.year, DEC);
    Serial.print("Fix: "); Serial.print((int)GPS.fix);
    Serial.print(" quality: "); Serial.println((int)GPS.fixquality); 
    if (GPS.fix) {
      Serial.print("Location: ");
      Serial.print(GPS.latitude, 4); Serial.print(GPS.lat);
      Serial.print(", "); 
      Serial.print(GPS.longitude, 4); Serial.println(GPS.lon);
      Serial.print("Location (in degrees, works with Google Maps): ");
      Serial.print(GPS.latitudeDegrees, 4);
      Serial.print(", "); 
      Serial.println(GPS.longitudeDegrees, 4);
      
      Serial.print("Speed (knots): "); Serial.println(GPS.speed);
      Serial.print("Angle: "); Serial.println(GPS.angle);
      Serial.print("Altitude: "); Serial.println(GPS.altitude);
      Serial.print("Satellites: "); Serial.println((int)GPS.satellites);
    }
  }
}

 このコードを Seeeduino にフラッシュすると、位置情報を下記のように見易くフォーマットしてシリアルモニタに出力します。

Time: 8:27:57.0
Date: 3/6/2018
Fix: 1 quality: 1
Location: 1234.5678N, 12345.6789E
Location (in degrees, works with Google Maps): 12.3456, 123.4567
Speed (knots): 0.28
Angle: 9.54
Altitude: 130.50
Satellites: 5

まとめ

 サンプルコードやライブラリのおかげもあり、位置情報の取得自体はとても簡単にできてしまいました。モジュールのサイズも小さいので、 Trinket などの小型のマイコンやコイン型電池等と組み合わせればとてもコンパクトなデバイスが作れそうです。ただその場合はネットワークへの接続が課題になってくるので、リアルタイムでのトラッキングではなく、このモジュールが持っているロギング機能で位置情報データを蓄積しておいて、あとで BLE や Wi-Fi 等に接続できる環境になったらデータを取り出して利用するような用途であれば比較的簡単に作成できそうに思いました。

Amazon FreeRTOS のサンプルを実機で動かしてみる

 もう結構前の話になってしまいますが、 re:Invent 2017で Amazon FreeRTOS が発表されました。

aws.amazon.com

 そもそも RTOS とはなんぞやというような説明はここでは割愛しますが、発表されてから気になっていたので今回は実機でチュートリアルに従ってサンプルを動かすまでをやってみました。

開始方法はこちらで紹介されています。
Amazon FreeRTOS – 開始方法 – AWS

対象ハードウェア

 公式にサポートされているハードウェアは開始方法のページでも紹介されていて、まだまだ限定的ですが、その中でも Espressif の ESP32-DevKitC は秋月電子で簡単に購入できそうだったので今回はこちらを使用してみます。

akizukidenshi.com

 ESP32-DevKitC は以前 ESP-WROOM-32 を使ってみようとした時に、突入電流対策が取られていなかったようなので ESPr Developer 32 の方を選択したのですが、今回は多少の不安はありつつも楽観的に考えて使ってみることにしました。

 チュートリアルは対象ハードウェア別に用意されていて、 ESP32-DevKitC のチュートリアルは下記ページになります。

docs.aws.amazon.com

事前準備

 各ハードウェア向けの作業をする前にいくつか事前準備をしておきます。まずは使用する AWS アカウントの IAM ユーザに下記ポリシーを追加し、 FreeRTOS と AWS IoT 関連の権限を付与しておきます。

  • AmazonFreeRTOSFullAccess

  • AWSIoTFullAccess

 次に AWS IoT コンソールから、デバイスで使用するためのポリシーを作成しておきます。 AWS IoT のコンソールの左メニューから 安全性 -> ポリシー を選択して、 ポリシーの作成 をクリックします。

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

 ポリシーの作成画面になりますので、自分でわかりやすい名前を入力し、ポリシーの設定方法を アドバンストモード に変更した上で、 Json ドキュメントを入力して設定します。チュートリアルで記載されている Json は下記のようなものになります。

{
    "Version": "2012-10-17",
    "Statement": [
    {
        "Effect": "Allow",
        "Action": "iot:Connect",
        "Resource":"arn:aws:iot:<aws-region>:<aws-account-id>:*"
    }, 
    {
        "Effect": "Allow",
        "Action": "iot:Publish",
        "Resource": "arn:aws:iot:<aws-region>:<aws-account-id>:*"
    },
    {
         "Effect": "Allow",
         "Action": "iot:Subscribe",
         "Resource": "arn:aws:iot:<aws-region>>:<aws-account-id>:*"
    },
    {
         "Effect": "Allow",
         "Action": "iot:Receive",
         "Resource": "arn:aws:iot:<aws-region>:<aws-account-id>:*"
    }
    ]
}

 しかしこれだと実際に動かしてみた時に権限が足りず、 AWS IoT に接続できなかったので、細かくどの権限が足りなかったのかは確認しませんでしたが、とりあえず AWS IoT に関して全ての操作が可能なように下記のように設定しました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

 入力したら 作成 ボタンをクリックしてポリシーを作成します。

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

 次に今回使用するハードウェアをモノ(Thing)として登録します。 AWS IoT コンソールの左メニューから 管理 -> モノ と選択して、 作成 ボタンをクリックします。

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

 今回は1つだけ登録するので、 単一のモノを作成する をクリックします。

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

 作成フォームが表示されたら登録するモノの名前を入力して 次へ をクリックします。

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

 モノで使用する証明書を作成します。 「1-Click 証明書作成」の 証明書の作成 ボタンをクリックします。

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

 証明書が作成されたら証明書とキーを ダウンロード をクリックしてダウンロードしておきます。また 有効化 ボタンをクリックして証明書を有効化して、 ポリシーのアタッチ ボタンをクリックします。

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

 先ほど作成したポリシーを選択して、 モノの登録 ボタンをクリックします。これでひとまずの事前準備は終了です。

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

Toolchain のセットアップ

 ここからは対象とするハードウェアを使うための設定になります。まずは Toolchain のセットアップです。 Espressif のサイトからダウンロードして展開します。

$ cd
$ mkdir esp
$ cd esp
$ wget https://dl.espressif.com/dl/xtensa-esp32-elf-osx-1.22.0-80-g6c4433a-5.2.0.tar.gz
$ tar zxf xtensa-esp32-elf-osx-1.22.0-80-g6c4433a-5.2.0.tar.gz

 あとはパスを通すために .bashrc などに下記のように export を追加すればOKです。

$ tail -2 ~/.bashrc 
### ESP32 toolchain
export PATH=$PATH:$HOME/esp/xtensa-esp32-elf/bin

Amazon FreeRTOS のダウンロード

 Amazon FreeRTOS は下記のリポジトリで公開されています。

github.com

 これを git clone でローカルに取得します。

$ git clone https://github.com/aws/amazon-freertos.git 
$ cd amazon-freetos

プロジェクト設定

 FreeRTOS をビルドするには Python 2.7.10 以上の環境が必要になります。最新の 3.6.5 で試してみたところ make に失敗したので、 pyenv で2系の最新の 2.7.15 の環境を用意しました。

$ pyenv versions
  system
* 2.7.15 (set by /Users/akanuma/workspace/amazon-freertos/.python-version)
  3.6.5

 pyserial と boto3 も必要なので、 pip でインストールします。

$ pip install pyserial
$ pip install boto3

 ダウンロードした FreeRTOS の中にはセットアップを簡単にするための設定ファイルが含まれています。下記ファイルの中の設定を実際に資料する環境に合わせて変更します。

$ cat demos/common/tools/aws_config_quick_start/configure.json 
{
    "thing_name":"ESP32-DevKitC",
    "wifi_ssid":"xxxxxxxxxx",
    "wifi_password":"xxxxxxxxxxxxx",
    "wifi_security":"eWiFiSecurityWPA2"
}

 そしてセットアップスクリプトを実行します。

$ cd demos/common/tools/aws_config_quick_start
$ python SetupAWS.py setup

 ここまで来たら ESP32-DevKitC を Mac に USB ケーブルで接続します。すると私の環境では /dev/cu.SLAB_USBtoUART のように認識されました。

 次に下記コマンドを実行して Espressif IoT Development Framework の設定メニューを表示させます。

$ cd demos/espressif/esp32_devkitc_esp_wrover_kit/make/
$ make menuconfig

 メニューが表示されたら Serial flasher config メニューを選択します。

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

 Default serial port を環境に合わせて設定します。

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

 必要に応じて Default baud rate の設定も行い、設定を保存してメニューを終了させます。

 あとはこれで最後に make コマンドを実行すれば AWS IoT に接続して MQTT のメッセージの送受信を行うデモが実行されるのですが、私の場合は途中で Python のバージョンの切り替えをしたりしていたせいか、 demos/common/tools/aws_config_quick_start/configure.json に設定した Wi-Fi の AP の情報が反映されておらず、また、証明書の情報も設定されていなかったので、下記ファイルを直接変更して設定しました。

$ ls -l demos/common/include/aws_clientcredential*
-rw-r--r--  1 akanuma  staff  2814 May 27 03:43 demos/common/include/aws_clientcredential.h
-rw-r--r--@ 1 akanuma  staff  3873 May 27 03:55 demos/common/include/aws_clientcredential_keys.h

 Wi-Fi AP の情報は aws_clientcredential.h を直接編集しました。また、 aws_clientcredential_keys.h の方は生成ツールが下記パスに用意されています。

demos/common/devmode_key_provisioning/CertificateConfigurationTool/CertificateConfigurator.html

 これをブラウザで開くとクライアント証明書とプライベートキーのファイルをアップロードできるフォームが開きますので、事前準備で AWS IoT コンソールからダウンロードしたファイルを指定し、 aws_clientcredential_keys.h を生成します。

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

サンプルの実行

 それではサンプルを実行します。下記のように make コマンドを実行すると、ファームウェアが実機に書き込まれ、実行されます。

$ cd demos/espressif/esp32_devkitc_esp_wrover_kit/make/
$ make flash monitor

 内容としては、ファームウェアのビルド、実機への書き込み、 Wi-Fi 接続、AWS IoT への接続が行われ、 MQTT によるメッセージの送信が12回行われて終了します。エラーなく最後に下記のログが出力されれば成功です。

352 6936 [MQTTEcho] MQTT echo demo finished.

 AWS IoT コンソールからもメッセージの送受信が行われたことが確認できます。

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

まとめ

 とりあえずチュートリアルに従うことで実機でサンプルを動作させることはできました。実際に自前のファームウェアを実装していく方法はまだよくわかってませんが、 Amazon FreeRTOS には Greengrass 等の AWS サービスと連携するためのライブラリも含まれていたり、 OTA でのファームウェアアップデートもできるようなので、そのあたりの使い方も今後調べてみたいと思います。

オシロスコープで Seeeduino のタイマー処理を確認してみる

 オシロスコープを借りる機会があったので、使い方の勉強がてら Seeeduino でスレッド処理的な感じでタイマーによる処理を実装してみました。今までオシロスコープは使ったことがなかったので、基本中の基本の確認という感じです。今回お借りしたオシロスコープは、 OWON Japan の PDS5022T という機種です。

www.owonjapan.com

f:id:akanuma-hiroaki:20180524224014j:plain

LED1つでのLチカ

 まずは一番シンプルな形で、LED1つのLチカの回路で確認してみます。回路図は下記のような形になります。LED のアノード側を Seeeduino の6番ピンに接続し、カソード側は抵抗を経由して GND に接続します。また、アノード側にオシロスコープの Positive 側を接続し、カソード側に Negative 側を接続しています。

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

 最初は delay を入れず、 Seeeduino が処理できる最速の速度で LED の HIGH/LOW を切り替えてみます。コードは下記のようになります。

void setup() {
  pinMode(6, OUTPUT);
}

void loop() {
  digitalWrite(6, HIGH);
  digitalWrite(6, LOW);
}

 これをオシロスコープで確認すると下記のようなグラフになります。赤色が今回接続しているプローブの計測値になります。縦軸は電圧で一目盛が 1V なので、HIGH の場合に 5V の電圧がかかっていることがわかります。横軸は時間で、一目盛が 5.0μs なので、 5.0μs弱の間隔で HIGH と LOW が入れ代わっているようです。ちなみにこのスピードだと LED を目で見ても、点滅している様子はわからず、点灯状態のように見えます。

f:id:akanuma-hiroaki:20180524234050j:plain

LED2つでの時間差Lチカ

 それでは LED を2つ使った回路を組んでみます。回路図は下記の通りです。単純に LED 1つの時と同じセットを追加しているだけのもので、アノード側は Aruduino の5番ピンに接続しています。また、今回使用しているオシロスコープは2チャンネルあるので、もう1つのチャンネルのプローブを追加した回路のアノード側に接続します。

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

 まずはいきなりタイマー処理を実装する前に、新しく追加した方の LED は LOW のままで先ほどと同様のLチカを実行してみます。

void setup() {
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  digitalWrite(5, LOW);
}

void loop() {
  digitalWrite(6, HIGH);
  digitalWrite(6, LOW);
}

 するとオシロスコープでの測定結果は下記のようになります。

f:id:akanuma-hiroaki:20180524235951j:plain

 それぞれのプローブの値を分けて表示していて、今回は黄色いグラフが6番ピンのLEDで、赤いグラフが5番ピンのLEDになります。今回 HIGH/LOW の切り替えを行なっているのは6番ピンのみなので、5番ピンのグラフはフラットのままのはずですが、実際には6番ピンの HIGH/LOW の切り替え時に影響を受けてノイズが入るようです。

 さて、それではタイマー処理を実装してみます。2つのLEDを同じタイミングで点滅させたい時には loop メソッドの中で2つ一緒に扱えば良いのですが、それぞれのLEDを違うタイミングで点滅させたい場合にはそれぞれ独立した処理を行う必要があります。PC などでプログラミングする場合は CPU がマルチスレッド対応しているケースがほとんどだと思いますので、 Thread を用いた実装で良いのですが、 Seeeduino ではマルチスレッドに対応していないので、1つのコアをタイムシェアリングする形で擬似的なマルチスレッドとして実装する必要があります。今回やる範囲であればスレッドまではやらなくてもタイマー処理ができれば良いので、今回は TimerOne というライブラリを使ってみました。

TimerOne & TimerThree Arduino Libraries

 実装は下記のようになります。それぞれの LED のステータスを切り替えるメソッドを用意し、片方(今回は赤い LED の処理)は TimerOne を使ったタイマー処理で実行しています。もう一方(緑のLEDの処理)は今まで通り loop メソッドの中で delay を入れて繰り返し実行しています。

#include <TimerOne.h>

const int ledPinRed = 5;
const int ledPinGreen = 6;
const int brinkIntervalRed = 1000; // MicroSeconds
const int brinkIntervalGreen = 500; // MicroSeconds
int ledStatusRed = LOW;
int ledStatusGreen = LOW;

void brinkLedRed()
{
  if (ledStatusRed == LOW) {
    ledStatusRed = HIGH;
  } else {
    ledStatusRed = LOW;
  }
  digitalWrite(ledPinRed, ledStatusRed);
}

void brinkLedGreen()
{
  if (ledStatusGreen == LOW) {
    ledStatusGreen = HIGH;
  } else {
    ledStatusGreen = LOW;
  }
  digitalWrite(ledPinGreen, ledStatusGreen);
}

void setup()
{
  pinMode(ledPinRed, OUTPUT);
  pinMode(ledPinGreen, OUTPUT);
  
  Timer1.initialize(brinkIntervalRed);
  Timer1.attachInterrupt(brinkLedRed);
}

void loop()
{
  brinkLedGreen();
  delayMicroseconds(brinkIntervalGreen);
}

 これを実行して測定した結果が下記のようになります。それぞれのプローブの電圧の切り替わりのタイミングが異なっていて、独立して処理されているのが確認できます。

f:id:akanuma-hiroaki:20180525092502j:plain

まとめ

 今回は初めてオシロスコープを使ったので基本的な使い方ぐらいしかやっていませんが、実際の電圧のかかり方が可視化できるのはとても便利ですね。テスターでもその瞬間の電圧を見ることはできますが、今回のように短い時間で電圧が変わっていき、しかも複数のチャネルを同時に見たいような時にはやはり時系列で変化の様子を確認できるオシロスコープが便利です。 今回は LED の点滅も目視で確認できる速さではなかったので、機械で正しく測定するのは大切ですね。

SORACOM Inventory Agent のカスタムオブジェクトを実装してみる

 先日の記事では SORACOM Inventory が Public Beta になったということでデバイス管理と SORACOM Harvest との連携を試してみました。

blog.akanumahiroaki.com

 Public Beta になったタイミングで SORACOM Inventory では LwM2M のカスタムオブジェクトが定義できるようになったので、今回は簡単に実装してみました。 SORACOM Inventory の機能概要や LwM2M のリソースモデル等については SORACOM の公式ドキュメントを参照ください。

dev.soracom.io

コアライブラリとサンプル実装のダウンロード

 SORACOM Inventory では下記リポジトリで Java版エージェントのコアライブラリとサンプル実装が提供されています。

github.com

 まずはこちらを clone します。

$ git clone https://github.com/soracom/soracom-inventory-agent-for-java.git
$ cd soracom-inventory-agent-for-java/

環境構築

 プロジェクトのビルドには Gradle が使用されています。私は IDE に Eclipse を使用しているので、下記コマンドで Eclipse 用の設定ファイルを生成します。

$ ./gradlew eclipse

 また、後でコアライブラリの jar ファイルを使用するので、下記コマンドでビルドしておきます。

$ ./gradlew build

 そして Eclipse のプロジェクトとしてインポートします。File メニュー の Import から General > Existing Projects into Workspace と辿って、root directory に soracom-inventory-agent-for-java ディレクトリを選択します。

 次に今回作成するエージェントのプロジェクト用のディレクトリを作成します。

$ mkdir restroom-monitor-agent
$ cd restroom-monitor-agent/

 下記コマンドでプロジェクトを初期化します。

$ gradle init --type java-application

 すると下記のようなファイルが生成されます。

$ ls -l
total 40
-rw-r--r--  1 akanuma  staff   992 May 12 23:52 build.gradle
drwxr-xr-x  3 akanuma  staff    96 May 12 23:52 gradle
-rwxr-xr-x  1 akanuma  staff  5296 May 12 23:52 gradlew
-rw-r--r--  1 akanuma  staff  2260 May 12 23:52 gradlew.bat
-rw-r--r--  1 akanuma  staff   369 May 12 23:52 settings.gradle
drwxr-xr-x  4 akanuma  staff   128 May 12 23:52 src

 build.gradle の内容は下記のように変更します。

$ cat build.gradle 
plugins {
    id 'java'
    id 'eclipse'
    id 'idea'
    id 'application'
}

repositories {
    jcenter()
    maven { url 'https://soracom.github.io/maven-repository/' }
}

def INVENTORY_AGENT_VERSION="0.0.5"

dependencies {
    compile "io.soracom:soracom-inventory-agent-for-java-core:$INVENTORY_AGENT_VERSION"
    testCompile 'junit:junit:4.12'
}

mainClassName = 'com.akanumahiroaki.restroommonitor.agent.RestroomMonitorAgent'

 Eclipse 用の設定ファイルを生成します。

$ ./gradlew eclipse

 そして Eclipse にインポートします。初期化時に生成されたメインクラスは下記のように App.java になっています。

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

 これを build.gradle の mainClassName で指定したパッケージとクラス名に合うように変更します。

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

 パッケージとクラス名変更後の初期の実装は下記のようになっています。

package com.akanumahiroaki.restroommonitor.agent;
/*
 * This Java source file was generated by the Gradle 'init' task.
 */
public class RestroomMonitorAgent {
    public String getGreeting() {
        return "Hello world.";
    }

    public static void main(String[] args) {
        System.out.println(new RestroomMonitorAgent().getGreeting());
    }
}

 次に必要なライブラリにパスを通します。 LeshanClient を使用するためのライブラリは別途取得する必要があるので、私は下記サイトから依存する jar と共に取得してきました。

jar-download.com

 また、先ほどビルドしたコアライブラリの jar が下記パスに生成されています。

soracom-inventory-agent-for-java/build/libs/soracom-inventory-agent-for-java-0.0.5.jar

 これらの jar ファイルを外部ライブラリとしてビルドパスに追加しておきます。

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

カスタムオブジェクト定義

 カスタムオブジェクトを定義するには XML ファイルを作成する必要があります。今回はシンプルに読み取り用のリソースを一つだけ定義してみます。トイレセンサーを作っている想定で、その空室状況を表すステータスです。カスタムオブジェクトの使い方としてこういうステータスを扱うのが正しいのか微妙な気がしましたが、今回は実装を試すのが主目的ということでやってみます。下記のような XML ファイルを作成し、サンプルプロジェクトと同様に src/main/resources 配下に 30000.xml というファイル名で配置します。

<?xml version="1.0" encoding="UTF-8"?>
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">
    <Object ObjectType="MODefinition">
        <Name>Restroom</Name>
        <Description1>Restroom Monitor</Description1>
        <ObjectID>30000</ObjectID>
        <ObjectURN>urn:oma:lwm2m:oma:30000</ObjectURN>
        <MultipleInstances>Single</MultipleInstances>
        <Mandatory>Optional</Mandatory>
        <Resources>
            <Item ID="0">
                <Name>Status</Name>
                <Operations>R</Operations>
                <MultipleInstances>Single</MultipleInstances>
                <Mandatory>Mandatory</Mandatory>
                <Type>String</Type>
                <RangeEnumeration />
                <Units/>
                <Description>Status of a restroom.</Description>
            </Item>
        </Resources>
        <Description2 />
    </Object>
</LWM2M>

実装クラス

 上記で定義したカスタムオブジェクトの実装クラスを下記のように作成します。今回はテキストファイルに現在のステータスが記録されているという前提で、簡単にその内容を読み込んで返すというだけにしてあります。

 AnnotatedLwM2mInstanceEnabler を継承したクラスに @LWM2MObject アノテーションでオブジェクトIDとオブジェクト名を指定します。また、値を返すメソッドには @Resource アノテーションでリソースIDとオペレーション種別を指定します。今回は読み取り専用のリソースなので、オペレーション種別は Operation.Read を指定しています。

package com.akanumahiroaki.restroommonitor.agent.object;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

import io.soracom.inventory.agent.core.lwm2m.*;

@LWM2MObject(objectId = 30000, name = "Restroom")
public class RestroomObject extends AnnotatedLwM2mInstanceEnabler {
    @Resource(resourceId = 0, operation = Operation.Read)
    public String readStatus() {
        try {
            List<String> lines = Files.readAllLines(Paths.get("/home/pi/work/RestroomMonitor/status.txt"));
            return String.join("", lines);
        } catch (IOException e) {
            e.printStackTrace();
            return "unknown";
        }
    }
}

エージェントクラスの実装

 ここまでで用意したカスタムオブジェクト定義と実装を利用するエージェントクラスを下記のように実装します。

 lwM2mModelBuilder.addPresetObjectModels() でデフォルトのオブジェクト定義を読み込んだ後で、 lwM2mModelBuilder.addObjectModelFromClassPath("/30000.xml") で今回定義したオブジェクト定義を読み込んでいます。また、実装したオブジェクトクラスを initializer.addInstancesForObject(new RestroomObject()) で設定しています。

package com.akanumahiroaki.restroommonitor.agent;

import org.eclipse.leshan.client.californium.LeshanClient;

import com.akanumahiroaki.restroommonitor.agent.object.RestroomObject;

import io.soracom.inventory.agent.core.initialize.InventoryAgentInitializer;
import io.soracom.inventory.agent.core.initialize.LwM2mModelBuilder;
import io.soracom.inventory.agent.core.lwm2m.typed_object.impl.MockDeviceObjectImpl;

public class RestroomMonitorAgent {
    public static void main(String[] args) {
        InventoryAgentInitializer initializer = new InventoryAgentInitializer();
        
        LwM2mModelBuilder lwM2mModelBuilder = new LwM2mModelBuilder();
        lwM2mModelBuilder.addPresetObjectModels();
        lwM2mModelBuilder.addObjectModelFromClassPath("/30000.xml");
        initializer.setLwM2mModel(lwM2mModelBuilder.build());
        
        initializer.addInstancesForObject(new MockDeviceObjectImpl());
        initializer.addInstancesForObject(new RestroomObject());
        LeshanClient client = initializer.buildClient();
        client.start();
    }
}

ユーザコンソールからオブジェクト定義を登録

 ユーザコンソールにもカスタムオブジェクトの定義が反映されるように、オブジェクト定義のXMLをユーザコンソールから登録します。ユーザコンソールの左メニューの SORACOM Inventory の オブジェクトモデル を選択します。

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

 オブジェクトモデルの追加 ボタンをクリックします。

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

 表示されたフォームにオブジェクトモデル定義XMLの内容を貼り付けて、 追加 ボタンをクリックします。

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

 すると下記のようにオブジェクトモデルが追加されます。

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

エージェントの実行

 ここまでで一通り実装は完了なので、最後にデバイスへの配布用のアーカイブを下記のコマンドで生成します。

$ ./gradlew distZip

 下記のようにアーカイブが生成されますので、これをエージェントを実行するデバイスへ配布します。

$ ls -ltr build/distributions/
total 2272
-rw-r--r--  1 akanuma  staff  1159199 May 13 01:44 restroom-monitor-agent.zip

 配布先デバイスで下記のようにアーカイブを展開してエージェントを実行します。

$ unzip restroom-monitor-agent.zip 
$ cd restroom-monitor-agent/bin
$ ./restroom-monitor-agent

 すると下記のようにコンソールからカスタムオブジェクトが確認できるようになります。 Observe することも可能です。

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

まとめ

 今回試してみたのはシンプルな内容だったので、簡単にカスタムオブジェクトを実装することができました。実際のサービスで使用する場合にはカスタムオブジェクトの前に標準で提供されているリソースモデルに対して正しい値を返すようにエージェントを実装する必要がありますので、取得したい情報全てが取得できるようにするには結構なボリュームの実装が必要そうではあります。ただ、エージェントを実装すればプラットフォームには手をかける必要がないので、うまく使えればとても便利なのではないかと思います。

ミニカメラと Seeeduino でタイムラプス撮影

 下記のミニカメラが面白そうだったので購入してみました。

www.switch-science.com

 基板部分のサイズが 28.5 mm x 17 mm x 4.2 mm、カメラ部分のサイズが 6.2 mm x 6.2 mm x 4.4 mm ととても小さいので、使い方によっては面白いものが作れそうです。今回はとりあえずタイムラプス動画を作る想定で、インターバル撮影を試してみました。

配線の準備

 今回はマイコンに Arduino Uno 互換の Seeeduino を使用します。ミニカメラのワイヤーは下記の写真のようにコネクタが接続されていますが、これだと Seeeduino やブレッドボードに配線することができません。

f:id:akanuma-hiroaki:20180507234443j:plain

 そこで思い切ってコネクタを切断してしまいます。

f:id:akanuma-hiroaki:20180507234517j:plain

 そして先端の被覆を剥がします。

f:id:akanuma-hiroaki:20180507234615j:plain

 次に錫メッキ線を半田付けします。

f:id:akanuma-hiroaki:20180507234812j:plain

 最後に熱収縮チューブでカバーすればひとまず準備完了です。これで Seeeduino やブレッドボードにも配線することができるようになりました。

f:id:akanuma-hiroaki:20180507235014j:plain

 ちなみにですが実は今回使っているミニカメラは2台目でして、1台目の時はコネクタをカットするのではなく基板に半田付けされている部分を外して他のケーブルを繋ごうと思い、下記の写真のような保護剤に覆われていた部分をカッターで削って半田ごてで加熱して外したのですが、細かく回路が密集しているところに結構力をかけてしまったりしたせいか接触の問題か、うまく動作しなくなってしまいました。保護剤を無理に削ったりはするものではないなと思った次第です。

f:id:akanuma-hiroaki:20180508075107j:plain

配線する

 それでは MiniCamera と Seeeduino を配線します。今回は単純にカメラでの撮影だけ試せれば良く、特に他のセンサーも使わないので、ブレッドボードも使わず、直接 Seeeduino に配線してしまいます。赤のケーブルは5V出力に、黒のケーブルは GND に、白のケーブルはトリガー用に13番ピンに接続します。

f:id:akanuma-hiroaki:20180507235619j:plain

 また、 Seeeduino は動作電圧を5Vにするか3.3Vにするかをスイッチで選べるようになっています。今回使用するミニカメラの動作電圧は 3.7V 〜 5V なので、 Seeeduino の動作電圧は 5V にしておきます。

f:id:akanuma-hiroaki:20180508000711j:plain

ファームウェア

 このミニカメラの撮影の仕様としては、トリガーを HIGH の状態から 0.5秒以内で LOW にして HIGH に戻すと写真が撮影され、その際に赤いLEDが短時間点滅します。撮影された写真は micro SD カードに保存され、ファイル名には連番が振られます。連番は999までで、そこに到達してしまうとそれ以降は写真が撮影されなくなります。

 また、動画の撮影も可能で、トリガーを HIGH の状態から 0.5秒以上 LOW にして HIGH に戻すと動画の撮影が開始され、撮影中は赤いLEDが点灯します。再度 0.5 秒以上 LOW にして HIGH に戻すと撮影が停止されます。

 インターバル撮影のコードは下記のようにシンプルなものです。 setup() でトリガーピンのピンモードを OUTPUT に設定し、 HIGH の状態にしておきます。 loop() では LOW にして 50ms 後に HIGH に戻して5秒間待つというのを繰り返します。つまり5秒間隔で写真を撮り続けるということになります。

int trig = 13;
int intervalMs = 5000;

void setup() {                
  Serial.begin(115200);

  pinMode(trig, OUTPUT);         
  digitalWrite(trig, HIGH); 

  Serial.println("MiniCamera is ready.");
}
 
void loop() {
  digitalWrite(trig, LOW);   

  delay(50);               
 
  digitalWrite(trig, HIGH);   
  
  Serial.println("Shoot.");
 
  delay(intervalMs);               
}

 写真の解像度は 1280 × 720 で、例えば下記のような写真が撮れます。

f:id:akanuma-hiroaki:20180508080631j:plain

 ちなみに動画の撮影は例えば下記のようなコードにすると、10秒撮影して10秒待つ、というのを繰り返すことになります。

int trig = 13;
int intervalMs = 10000;

void setup() {                
  Serial.begin(115200);

  pinMode(trig, OUTPUT);         
  digitalWrite(trig, HIGH); 

  Serial.println("MiniCamera is ready for video.");
}
 
void loop() {
  digitalWrite(trig, LOW);   

  delay(600);               
 
  digitalWrite(trig, HIGH);   
  
  Serial.println("Shoot.");
 
  delay(intervalMs);               
}

タイムラプス動画作成

 ミニカメラ + Seeeduino で今回行っているのは写真のインターバル撮影までなので、撮影された写真からタイムラプス動画を作成するには他のアプリなどを使う必要があります。詳細は割愛しますが、例えば Panolapse というアプリを使うと JPEG 画像からタイムラプス動画を作成できます。出力解像度1280×720 HDまでなら無料で使うことができます。

www.panolapse360.com

 例えば10秒間隔で55枚撮影した画像から作成したタイムラプス動画は下記のような感じになりました。

まとめ

 今回はとりあえず筐体もバッテリーもなしでインターバル撮影してみただけでしたが、バッテリーと一緒に小さい筐体に収めればかなりコンパクトになるので、例えばロボットのプラモデルとかに収めて目のところにカメラを配置したら面白そうかなと思いました。撮影枚数上限が1,000枚なのと、保存先がミニカメラの micro SD カードなので、ファームウェアから画像を操作することは難しそうなため、遠隔地に配置して色々やるみたいな用途には向かなそうですが、手元に置いておいて遊ぶデバイスに使う分には楽しそうです。