背景
前回の記事でMQTTブローカーをクラウド上に設定し、スマホアプリ上のPublisher・Subscriberから出版と購読ができることを確認した。
今回はRaspberryPiからこのブローカーに接続して、何かコマンドを送れるようにする。
以前から作成しているIotDeviceHubをアップデートして、MQTTでPublishメッセージを受信したら対応するコマンドをBLE機器に送るようにする。
https://github.com/futail2a/IotDeviceHub/
libmosquittoの設定
前回の記事で作成したCA証明書とクライアント証明書、クライアント鍵をダウンロードして、RaspberryPiに送る。
mosquitto_tls_set()で証明書のパスを指定するので、場所はIotDeviceHubアプリから読める所ならどこでもいい。
また、リモートにあるブローカのアドレスとポートも設定する必要があるので、そのあたりの値をまとめてJSONファイルに設定するようにした。
// IotDeviceHub/config/config.json
{
"mqtt":
{
"brokerIpv4": <address>,
"brokerPort": <port>,
"caCert": <caCertPath>,
"clientCert": <clientCertPath>,
"clientKey": <keyPath>
}
}
JSONファイルの読み込みはPOCOを使う。
MQTTを管理しているクラスのコンストラクタでJSON内の値を読み込むようにした。
MqttManager::MqttManager()
{
try
{
Poco::Util::JSONConfiguration config = Poco::Util::JSONConfiguration(CONFIG_FILE_PATH);
mBrokerIpv4 = config.getString("mqtt.brokerIpv4");
std::cout << "MQTT broker address: " << mBrokerIpv4 << std::endl;
mBrokerPort = config.getUInt16("mqtt.brokerPort");
std::cout << "MQTT broker port: " << mBrokerPort << std::endl;
mCaCertPath = config.getString("mqtt.caCert");
std::cout << "MQTT CA cert path: " << mCaCertPath << std::endl;
mClientCertPath = config.getString("mqtt.clientCert");
std::cout << "MQTT client cert path: " << mClientCertPath << std::endl;
mClientKeyPath = config.getString("mqtt.clientKey");
std::cout << "MQTT client key path: "<< mClientKeyPath << std::endl;
}
catch (Poco::Exception& ex)
{
std::cerr << "Error: " << ex.displayText() << std::endl;
}
}
証明書やブローカのアドレスはmosquitto_tls_set()とmosquitto_connect_bind_v5()で設定できる。
//IotDeviceHub/mqtt/MqttManager.cpp
auto ret = mosquitto_tls_set(m_mosq, mCaCertPath.c_str(), NULL, mClientCertPath.c_str(), mClientKeyPath.c_str(), NULL);
:
ret = mosquitto_connect_bind_v5(m_mosq, mBrokerIpv4.c_str(), mBrokerPort, 60, nullptr, nullptr);
MQTTメッセージに対応するコールバックの設定
MQTTのPublishメッセージを受信したら登録していたコールバックから対応するBLEコマンドを送れるようにしたい。
libmosquittoではmosquitto_message_v5_callback_set()でメッセージ受信時のコールバックを設定することができる。
しかしlibmosquittoはC言語ライブラリなのでメンバ関数をバインドする、みたいなことはできない。
これだと少し不都合があるので、mosquittoインスタンスを作るときにthisポインタを渡す。
こうすることでコールバックのobjにthisが渡されるので、そこからpublicメソッドにアクセスすることができる。
//bool MqttManager::init(std::string client_id)
m_mosq = mosquitto_new(client_id.c_str(), true, this);
void on_message(struct mosquitto *mosq, void *obj, const struct mosquitto_message *msg, const mosquitto_property *props)
{
auto *manager = static_cast<MqttManager*>(obj);
if(manager)
{
manager->onMessageReceived(msg);
}
}
ちなみに、イベントのハンドリングにはMediatorパターンを使っている(詳細は以下の記事)。
MQTTトピック名をイベント、ペイロードをイベントデータとして扱っている。
void MqttManager::onMessageReceived(const struct mosquitto_message *msg)
{
if(mMediator)
{
std::string eventData="";
if(msg->payload == nullptr || msg->payloadlen == 0)
{
std::cerr << "Received empty message on topic " << msg->topic << std::endl;
}
else
{
eventData=std::string((char *)msg->payload, msg->payloadlen);
}
mMediator->onEvent(msg->topic, eventData);
}
else
{
std::cerr << "Mediator not set, cannot handle message on topic " << msg->topic << std::endl;
}
}
MQTTのクライアント側で以下のように設定すればイベントとするMQTTトピックとイベントに対応するアクションを定義できる。
この例だと"exec_bot"をサブスクライブして、そのトピックを受信するとBLE機器(mBotDevice)にコマンドを送るラムダ式が実行される。
//bool IotDeviceHubManager::init()
mMqtt->subscribe("exec_bot");
mEventManager->registerEventHandler("exec_bot", [this](const std::string& eventData)
{
std::cout << "Event: exec_bot" << std::endl;
auto command = mBotDevice->getExecActionCommand();
mBle->sendBleCommand(command);
}
);
mMqtt->setMediator(mEventManager);
実験
実験として、Switchbotボットのスイッチ実行をMQTT経由で実行できるか確かめた。
まずはSwitchbotボットをエアコンのリモコンに固定する。
中途半端に固定すると動作したときにボットが動いてボタンが押せないのでマスキングテープでガチガチに固定する。
リモコンの画面が見えなくなるという致命的な欠陥があるが、一旦無視する。
その後、冒頭の記事で紹介したスマホアプリに"exec_bot"トピックをPublishするボタンパネルを設定し、ボタンを押す。
結果
スマホアプリからexec_botトピックをパブリッシュするとボットが動作した。
さすがにこれだと通常の使い方ができないのでメカニカルな改良がもっと必要だが、インターネット経由で室内の機器を操作できるようになったのでIoTらしくなってきたと思う。