背景
SwitchBotのスマート電球とモーションセンサーを使って人が近づくと一定時間電球を点灯させるプログラムをRaspberry Pi5上で作っている。
以前の記事では、(1) センサーが人を検知したら電球をオンにするコマンドを送り、(2) その後タイマーを設定して、(3) タイムアウトしたら電球をオフさせるコマンドを送るように設計した。
また、連続で人を検知した場合は都度タイマーを延長するように設計した。
前回の記事ではタイマー機能は外部のライブラリを使う方針に決めたが、今回はPOCOと呼ばれるライブラリを使うことにした。
この記事ではPOCOの簡単な紹介と実装例を説明する
POCO
POCOとはC++のライブラリで、スレッドの管理やファイルシステム、ネットワークに関する基本的な機能を提供している。
(ちなみにPOCOはPOrtable COmponentsの略らしい)
POCOを使った理由は、POCOはIoTアプリケーション開発で使われている実績があるからである。
具体的にはmacchina.ioと呼ばれるIoTアプリケーション向けのプラットフォームで採用されており、今後macchina.io向けに何かを作る時に備えてPOCOに慣れていたおいてもいいだろうと思った。
POCOのセットアップ
インストール方法は公式サイトに載っている。
今開発しているプロジェクトではパッケージ管理ツールは使っていないので、開発環境に直接POCOをビルド&インストールした。
具体的にはGitHubからリポジトリをダウンロードし、以下の通りにCmakeを実行した
$ git clone -b poco-1.14.2-release https://github.com/pocoproject/poco.git
$ cd poco
$ mkdir cmake-build
$ cd cmake-build
$ cmake .. && cmake --build .
$ sudo cmake --build . --target install
残念ながらPOCOには.pcファイルが見当たらなかったのでpkg-configは使えない。
(このIssueを見る限りかつてはサポートしていそうだが、無くなったのだろうか?)
そのためMakefileにincludeディレクトリと共有ライブラリのディレクトリを直接指定する必要がある。
(公式サイトの記載通りにやれば/usr/local/include/Pocoと/usr/local/libにインストールされているはずである)
INCLUDES += -I/usr/local/include/Poco
CFLAGS += $(INCLUDES)
LDFLAGS += -lPocoFoundation -Wl,-rpath,/usr/local/lib
$(DEFAULT_TARGET): $(DEFAULT_SRCS)
$(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS)
Poco::Timerの使用
POCOではスレッドベースで動くタイマー機能が提供されている。
似たような機能を提供するクラスは何種類かあるようだが、今回はTimerクラスを使用した
Timerはシンプルなクラスで、start()を呼べばタイマーが開始し、stop()を呼ぶとタイマーを中止することができる。
コールバック関数はstart()の引数で指定することができ、タイムアウトした際に実行される。
タイムアウト時間はTimerのコンストラクタの引数で指定することができる。
タイムアウト時間はstartIntervalとperiodicIntervalの二種類ある
- startIntervalは最初の一回目のタイムアウト時間で、periodicIntervalはstartInterval後周期的に起動するタイマーのタイムアウト時間である
- periodicIntervalを指定しなければ、最初の一回だけタイマーが実行される
コールバック関数はPoco::TimerCallback形式でTimerクラスに渡される。
TimerCallbackはテンプレートにコールバックを呼ぶオブジェクトを指定し、コンストラクタの引数にオブジェクトのポインタとそのオブジェクトが持つコールバックにしたい関数を指定する。
このようにTimerクラスはシンプルで使いやすいのだが、タイマーのリセットに関しては自分で実装する必要があった
- タイマーが走っている間に新たにモーションを検出した場合はリセットをかけたい
- Timerにはreset()という名前のメソッドがあるが、これはperiodicIntervalに対してのみ機能する
- そのためTimerクラスをクラスのメンバとして持ち、タイマーが実行・完了するたびにインスタンスの生成と削除を行うことでタイマー起動中かどうかを判定できるようにした
以下が実装例である。
// This code snipet removed unrelated functions and variables
class IotDeviceHubManager
{
private:
std::unique_ptr<Poco::Timer> mLightTimer;
void onLightTimeout(Poco::Timer& timer);
};
void IotDeviceHubManager::onLightTimeout(Poco::Timer& timer)
{
std::cout << "Light timer expired, turning off the light" << std::endl;
auto command = mBulbDevice->getTurnOffCommand();
mBle->sendBleCommand(command);
mLightTimer.reset();
}
void IotDeviceHubManager::onMotionUpdate(std::vector<uint8_t> data)
{
std::cout << "Motion data received" <<std::endl;
auto command = mBulbDevice->getTurnOnCommand();
mBle->sendBleCommand(command);
if(mLightTimer)
{
std::cout << "Detected motion again, extend light on timer" <<std::endl;
mLightTimer->stop();
mLightTimer->start(Poco::TimerCallback<IotDeviceHubManager>(*this, &IotDeviceHubManager::onLightTimeout));
}
else
{
mLightTimer = std::make_unique<Poco::Timer>(DEFAULT_LIGHT_INTERVAL, 0);
mLightTimer->start(Poco::TimerCallback<IotDeviceHubManager>(*this, &IotDeviceHubManager::onLightTimeout));
}
結果
ライトが点灯したらDEFAULT_LIGHT_INTERVALで指定した時間経過後にライト消灯コマンドが送られていることが確認できた。
今後の方針
これで元々やりたかった機能はすべて実現できた。
これからはテストコードを書いたり、あるいはより色んなユースケースに対応できるように拡張したりしたい。
作ったプログラムのリポジトリ

