スマートホーム構築

出先から家に戻ってくるときに、エアコンを遠隔で操作して夏なら冷えた状態に、冬なら暖かい状態にしたいと思った。 そこでRaspberry piを使って解決することにした。 今回はエアコンの操作をリモコンの信号を記憶させて真似するようにしてみた。




赤外線送受信回路の製作


参考: 格安スマートリモコンの作り方
Raspberry piは省電力なzero WHを使い、上記のサイトを参考にして回路を作る。 上記のサイトでも注意されているが、赤外線LEDに定格以上の電流を流すとLEDが破損する恐れがあるので注意。

因みに自分はLEDにTSAL6100を使って、抵抗は39 Ω, 1W抵抗を使うことで、定格以下で発振するようにしている。 具体的にはTSAL6100は順方向電圧が100mAで1.6 V, Typ.:1.35 V@100 mAなのでこれを超えないように抵抗値を選んだ。 電源は5 Vなので(5-1.35) Vで100 mA流すことを考えると抵抗値は37 Ωとなるので余裕を見て39 Ωの抵抗を使うことにした (1.1 V@LED)。 ここでLEDには電流が流れ始めるしきい値が存在するのであまり抵抗を高くしすぎると電圧降下で光らなくなってしまうので注意。
さらにLEDはどの方向でも送信できるように31個を放射状に並べている。 LEDは個々に抵抗を繋ぐ必要があるので、抵抗も31個用意してLEDと接続した。
ただし、その場合はRaspberry piでは給電が間に合わないので、LED駆動の5VはACアダプタ(5V, 4A)から直接入れている。 そのため赤外線LEDの発振回路は図1のようになった。GPIO17はRaspberry pi ZeroのGPIO17に接続

IR_ctrl_circuit
図1 送受信回路図

実際に組み立てたものが、写真1。

remort_controller
写真1 組み立て後

赤外線送受信のプログラムのインストール


最初に操作したい機器についている純正のリモコンと、前項のRaspberry piを用意する。
Raspberry piでは前提となるpigpioをインストールして以下のように設定する。

$ sudo apt install pigpio
$ pip install pigpio
$ sudo systemctl enable pigpiod.service
$ sudo systemctl start pigpiod
$ crontab -e
#エディタが開いたら以下の文を追記
@reboot until echo 'm 17 w w 17 0 m 18 r pud 18 u' > /dev/pigpio; do sleep 1s; done

ここで、w 17, r 18はRaspberry piのpinの番号で他のpinを使いたい場合は数字を変える(Raspberry piのpinの振り分けをよく確認すること!)。 w, rはRaspberry piの動作でそれぞれ出力、入力を意味している。pudはプルアップ・プルダウン抵抗のことで、ここでは18pinをプルアップ(u)に設定している。 上記の参考サイトにもあるIR Record and Playbackをダウンロードして、 適当なディレクトリ上に保存する。

Linux/macなら、

cd <保存したいディレクトリ名>
curl https://abyz.me.uk/rpi/pigpio/code/irrp_py.zip | zcat > irrp.py

これだけでは働かないので、リモコンの赤外線信号を覚えさせる必要がある。 以下のコマンドを実行するとRecordingという文字が現れるので受光部に向かって リモコンで教えさせたいボタンを押す。 成功するとOkayと出る。

$ python3 irrp.py -r -g18 -f codes <信号名(任意)> --no-confirm --post <信号長(<=130)>

信号長は任意だが、エアコン等は信号自体が長いので130 msとったほうが良さそう。 覚えさせた赤外線の信号は{ON時間(ms),OFF時間(ms), ON, OFF, ...}という配列でJSON形式で同じディレクトリ内のcodesというファイルに記録される。
次に覚えさせた信号が正しいか以下のコマンドで確認する。

$ python3 irrp.py -p -g17 -f codes <覚えさせた信号名>

ここで動作しなかった場合はうまく信号を記録できていないので再度記録、実行を繰り返してみる。

MQTTで実行


MQTT経由で前項のコマンドを実行できるようにする。 まずはコマンドを実行しやすくするために、以下のようなシェルスクリプトを書く。

#!/bin/sh
name=$1
python3 <irrp.pyの相対パス> -p -g17 -f <codesの相対パス> "$name"

相対パスはこのシェルスクリプトのあるディレクトリに対してのそれぞれの位置のことで、同じ階層なら./<ファイル名>とかで問題ない。 これをC/C++で実行するには以下のようにプログラムを書く

system("<シェルスクリプトの絶対パス> <覚えさせた信号名>")

例えば前のシェルスクリプトがremort_ctrl.shという名前で自分のホームディレクトリにあり、覚えさせた信号名がaircon:onだった場合。

system("/home/<ユーザー名>/remort_ctrl.sh aircon:on")

これでCでコマンド実行できる。

次にMQTTのクライアントを実装する(libmosquittoppが必要)。 ここでは詳しく説明をしないが、以下のようなプログラムを書いた。


参考: mosquitopp_client
参考: Mosquitto C++ sample code to publish


MQTTClient.hpp

#ifndef MQTTCLIENT
#define MQTTCLIENT
      
#include <mosquittopp.h>
#include <string.h>
#include <iostream>
#include <sstream>
#include "GlobalVariables.hpp"
      
#define MAX_PAYLOAD 50
#define DEFAULT_KEEP_ALIVE 60
      
class MQTTClient : public mosqpp::mosquittopp {
  public:
    ~MQTTClient();
    void on_connect(int rc);
    MQTTClient(const char *_id, const char *_hostname, int _port, char *topic);
    void on_message(const struct mosquitto_message *message);
    void on_subscribe(int mid, int qos_count, const int *granted_qos);
    void on_publish(int mid);
    bool sendMessage(char *message, int qos, bool retain);
    bool sendMessage(int sendnum, int qos, bool retain);
    std::string getMessage();
    void clearMessage();
      
  private:
    const char *clientid;
    const char *hostname;
    char *topic;
    int port;
    int keepalive;
    std::string _messages;
};
      
inline MQTTClient::MQTTClient(const char *_id, const char *_hostname, int _port, char *_topic) : mosquittopp(_id) {
  mosqpp::lib_init();
  clientid = _id;
  hostname = _hostname;
  port = _port;
  topic = _topic;
  keepalive = DEFAULT_KEEP_ALIVE;
  connect(hostname, port, keepalive);
}
      
inline bool MQTTClient::sendMessage(char *message, int qos = 0, bool retain = false) {
  int ret = publish(NULL, topic, strlen(message), message, qos, retain);
  return (ret == MOSQ_ERR_SUCCESS);
}
      
inline bool MQTTClient::sendMessage(int sendnum, int qos = 0, bool retain = false) {
  std::stringstream ssnum;
  ssnum.str("");
  ssnum << sendnum;
  const char *message=(ssnum.str()).c_str();
  int ret=publish(NULL, topic, strlen(message), message, qos, retain);
  return (ret==MOSQ_ERR_SUCCESS);
} 

inline std::string MQTTClient::getMessage() { 
  int ret=subscribe(NULL, topic);
  return _messages;
}

inline void MQTTClient::clearMessage() { _messages="" ; }

inline MQTTClient::~MQTTClient() { mosqpp::lib_cleanup(); }

#endif // !MQTTCLIENT

MQTTClient.cpp

#include "MQTTClient.hpp"
      
void MQTTClient::on_connect(int rc) {
  if (!rc) {
      std::cout << "Connected: " << clientid << "- code " << rc << std::endl; 
  } 
}

void MQTTClient::on_subscribe(int mid, int qos_count, const int *granted_qos){ 
  // std::cout << "Subscription succeeded." << std::endl;
}

void MQTTClient::on_publish(int mid){
  std::cout << "Published: " << mid << std::endl;
}

void MQTTClient::on_message(const struct mosquitto_message *message){ 
  int payload_size=MAX_PAYLOAD + 1;
  char buf[payload_size];
  if (!strcmp(message->topic, topic)) {
    memset(buf, 0, payload_size * sizeof(char));
    memcpy(buf, message->payload, MAX_PAYLOAD * sizeof(char));
    _messages=buf;
  } else {
    std::cout << "not get message: " << buf << std::endl;
  } 
}

MQTTHandler.hpp

#ifndef MQTTHANDLER
#define MQTTHANDLER

#include "MQTTClient.hpp"

namespace MQTTHandler {
  void publish(MQTTClient *_mqtt, char *message, int qos, bool retain);
  void publish(MQTTClient *_mqtt, int sendnum, int qos, bool retain);
  std::string getString(MQTTClient *_mqtt);
  int getNumbered(MQTTClient *_mqtt);
} // namespace MQTTHandler
      
inline void MQTTHandler::publish(MQTTClient *_mqtt, char *message, int qos, bool retain) {
  int rc = _mqtt->loop();
  while (!(_mqtt->sendMessage(message, qos, retain))) {
    if (rc) _mqtt->reconnect();
    std::cout << "Did't send messages!" << std::endl;
    rc=_mqtt->loop();
  }
}
      
inline void MQTTHandler::publish(MQTTClient *_mqtt, int sendnum, int qos, bool retain) {
  int rc = _mqtt->loop();
  while (!(_mqtt->sendMessage(sendnum, qos, retain))) {
    if (rc) _mqtt->reconnect();
    std::cout << "Did't send messages!" << std::endl;
    rc=_mqtt->loop();
  }
}
      
inline std::string MQTTHandler::getString(MQTTClient *_mqtt) {
  std::string output = "";
  while (true) {
    _mqtt->loop();
    output = _mqtt->getMessage();
    if (output != "") {
      std::cout << "message: " << output << std::endl;
      break;
    }
    // std::cout << "get null: " << output << std::endl;
  }
  _mqtt->clearMessage();
  return output;
}
      
inline int MQTTHandler::getNumbered(MQTTClient *_mqtt) {
  std::string output = "";
  while (true) {
    _mqtt->loop();
    output = _mqtt->getMessage();
    if (output != "") {
      std::cout << "message: " << output << std::endl;
      break;
    }
    // std::cout << "get null: " << output << std::endl;
  }
  _mqtt->clearMessage();
  return atoi(output.c_str());
}
      
#endif // !MQTTHANDLER

以上のプログラムでMQTTのクライアントを実装できたので、 このクライアントを起動→メッセージを受け取ったら、前に作ったコマンド実行 をするメイン関数を用意する。


main.cpp

#include <signal.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include "MQTTClient.hpp"
#include "MQTTHandler.hpp"

struct sigaction _sigact;
MQTTClient *client_air;
      
void signalHandler(int signum) {
  using std::cout;
  using std::endl;
  if (signum == SIGTERM) {
    cout << "Stop this prosess (systemctrl) !" << endl;
  } else if (signum==SIGINT) {
    cout << "kill prosess (ctrl + c) !" << endl;
  } delete client_air;
  exit(0);
  // signalnumber=1;
}

bool signalInit() {
  sigset_t _sigset;
  int sigsetok=sigemptyset(&_sigset);
  if (sigsetok < 0) return false;
  sigsetok=sigaddset(&_sigset, SIGINT);
  if (sigsetok < 0) return false;
  sigsetok=sigaddset(&_sigset, SIGTERM);
  if (sigsetok < 0) return false;
  memset(&_sigact, 0, sizeof(_sigact));
  _sigact.sa_handler=signalHandler;
  _sigact.sa_mask=_sigset;
  _sigact.sa_flags |=SA_NODEFER;
  _sigact.sa_flags |=SA_ONSTACK;
  sigsetok=sigaction(SIGINT, &_sigact, NULL);
  if (sigsetok < 0) return false;
  sigsetok=sigaction(SIGTERM, &_sigact, NULL);
  if (sigsetok < 0) return false;
  return true;
}

int main(int argc, char *argv[]) {
  client_air = new MQTTClient("remortctrl", "<MQTTブローカーのアドレス>", 1883, "remortctrl/airctrl");
  std::string mqttrecv;
  std::string basecommand = "/home/<ユーザー名>/remort_ctrl.sh aircon:";
  int tGlobalLoop = 1e+5; // Unit: [us]
  while (true) {
    client_air->reconnect();
    mqttrecv = MQTTHandler::getString(client_air);
    if (mqttrecv.find("on") != std::string::npos) {
      system((basecommand + "on").c_str());
    } else {
      system((basecommand + "off").c_str());
    }
    usleep(tGlobalLoop);
  }
  return 0;
}

MQTTブローカーのアドレスは適宜変える(自PCでブローカーが動いている場合は"localhost")。

これで、このページのsystemctlの有効化/無効化の項を参考にバックグラウンドで動かせば、 MQTT経由でメッセージを受け取ると機器を遠隔で操作できるようになる。 因みにこの上の例ではremortctrl/airctrlというトピックでonというメッセージが来たら、aircon:onという信号名が実行される。 offの信号名も登録しておけば、それ以外のメッセージが来たらoffするようになっている。

2022/11/25 追記

この後、実際に運用してみたところCPUの使用率が100%近くになっていることが判明した。 そこでコードの該当部分を以下のように改変した。


MQTTClient.cpp

(中略)

/// on_messageのメソッド内でスクリプトを実行するように書き換えた     
void MQTTClient::on_message(const struct mosquitto_message *message) {
  int payload_size = MAX_PAYLOAD + 1;
  char buf[payload_size];
  std::string basecommand = "/home/<ユーザー名>/remort_ctrl.sh aircon:";
  std::string mqttrecv = basecommand;

  if (!strcmp(message->topic, topic)) {
    memset(buf, 0, payload_size * sizeof(char));

    /* Copy N-1 bytes to ensure always 0 terminated. */
    memcpy(buf, message->payload, MAX_PAYLOAD * sizeof(char));
    if (fGlobaldebug) {
      std::cout << "getmessage(" << message->topic << "): " << buf << std::endl;
    }
    mqttrecv +=buf;
    system(mqttrecv.c_str());
    _messages=buf;
  } else {
    std::cout << "not get message: " << buf << std::endl;
  }
}

main.cpp

(中略)
//#include "MQTTHandler.hpp" //要らなくなったのでコメントアウト

(中略)

/// whileの中をシンプルにしてCPUの使用率を抑える
int main(int argc, char *argv[]) {
  if (!signalInit()) return 1; //ctrl+cで正常に止まるように
  if (argc == 2 && strstr(argv[1], "debug") != NULL) fGlobaldebug = true; //debug用に付け足した
  client_air = new MQTTClient("remortctrl", "<MQTTブローカーのアドレス>", 1883, "remortctrl/airctrl");
  std::string mqttrecv;
  std::string basecommand = "/home/<ユーザー名>/remort_ctrl.sh aircon:";
  int tGlobalLoop = 1e+5; // Unit: [us]
  while (true) {
    client_air->loop();
    client_air->subscribe(NULL, "remortctrl/airctrl");
    usleep(tGlobalLoop);
  }
  return 0;
}

今後は遠隔操作できる様になった機器がついているか、消えているかの監視ができるようにしたい。
電源の電流/電圧を直接見るのはかなり危ないので、カレントトランスとかで電源の電流値を監視するのが手っ取り早いか?