人感センサの情報をサーバで受取り、他のメディアに配信したり、別の場所で表示したりしたい。

前回は、センサの基本的な動作確認。

いよいよ、人感センサの検知データをWifi経由で、サーバに送信する。 ということで、protocolは、MQTTを使う。実装は、mosquitto。

なんでMQTT

ネットが、掲示版からSNSに変遷していったのと似てるかも。 publisher がインフルエンサーで、subscriber がフォロワー。

ただし、 生配信のみ 。フォロワーは24時間365日、がんばって張り付く必要がある。

技術的に説明すると、こんな感じ。

掲示版的なサービス

シンプルにありそうなのは、サーバ側で適当なAPIをつくって、センサであるクライアントが、そこに検知データPOSTする、HTTP Server構成だろう。 検知データは、サーバ側でCSVなり、sqliteなり、DBに保存すればよい。

SNS的なサービス

が、センサの数は増えてきたら?数千の単位だったら? モニタ数もそうだが、用途によって見たい(収集した)情報が異なる画面が増えたら?

トラフィックが増えて、HTTPサーバ、およびDBがボトルネックになってしまう。 サーバを高性能なものにすれば、それなりに動くかもしれない。 が、もっといい方法があるのでは?

MQTT のモデル

ということで、開発されたのがMQTTのモデル。

赤い矢印が集まるbrokerが、いわゆるサーバにあたる。 が、このサーバは、自身でデータをもたない。publisherから来たデータは数分もすれば消える。

  • publisher とにかく検知したデータを、brockerに押しつける(publish)。
  • subscriber 欲しいデータ範囲(topic)をbroker指定して、リアルタイムで受け取れるようにデータ待ちする。

このpublisher と subscriber をいい感じにサポートするのがbrokerの仕事。

MQTT のsetup

ここで最近手にいれたRPi4の出番。 メモリが2Gもあるので、MQTTサーバなんて余裕で動く!

install

sh
$ sudo apt-get install mosquitto

次回からrebootで自動的に起動するように設定

sh
$ sudo systemctl enable mosquitto

起動チェック

sh
 $ systemctl status mosquitto
● mosquitto.service - Mosquitto MQTT Broker
     Loaded: loaded (/lib/systemd/system/mosquitto.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2022-03-08 09:03:27 JST; 4 days ago
       Docs: man:mosquitto.conf(5)
             man:mosquitto(8)
   Main PID: 8406 (mosquitto)
      Tasks: 1 (limit: 3717)
        CPU: 3min 11.856s
     CGroup: /system.slice/mosquitto.service
             └─8406 /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf

Warning: journal has been rotated since unit was started, output may be incomplete.

broker のサービスIP設定

brokerは、defaultだとlocalhostのみアクセスできる。 他のマシン(private subnet)からアクセスできるように、listener の設定をする。

  • brokerのprivateIP 10.0.0.1
  • brokerのport 1883
  • 認証なしのアクセスOK

/etc/mosquitto/conf.d/listener.conf

ini
listener 1883 10.0.0.1
allow_anonymous true

再起動

sh
$ systemctl restart mosquitto

これでbroker はOK。

mosquitto client のinstall

テストのために、command line 版の mosquitto client をinstall

sh
$ sudo apt-get install mosquitto-clients

これで、publisher と sunscriber のやり取りを確認できる。

subsriber では、 topic sensor/# で待ち受けるように実行する。

sh
$  mosquitto_sub -v -h  {brokerのIP} -t 'sensor/#'

# は正規表現の * にあたり、 sensor 、 sensor/room01 など先頭がsensor のtopic全てを待ち受ける指定。

-v option をつけることで、publisherからのデータ受信時に、 topic 情報も表示される。

この topic sensor 向けに、publisher として情報 {"hello": 1} 送信する。 brokerのIPを 10.0.0.1 とすると、

sh
$ mosquitto_pub -h 10.0.0.1 -t sensor/room1  -m '{"hello": 1}'

subscriber の表示

sh
$ mosquitto_sub -v -h 10.0.0.1 -t sensor/#
sensor/room1 {"hello": 1}

M5StickCをpublisherにする

上記のpublisherをM5StickC で実装する。

m5stick_mosquitto.cpp · Gistcpp
#include <M5StickC.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "PubSubClient.h"

//
// wifi
//
const char *ssid = "AP SSID";
const char *password = "AP PASWORD";

WiFiClient wClient;

//
// MQTT
//
const char *mqttHost = "xxxx.xxx.xxx.xxx"; // MQTTのIPかホスト名
const int mqttPort = 1883;              // MQTTのポート

const char *topic = "sensor/room1"; // 送信先のトピック名
char *payload;                           // 送信するデータ
PubSubClient mqttClient(wClient);

int Last_sec = -1;
bool Is_on = false;

#define WAIT_TIME 100
#define PIR_HAT_PIN 36

//
// setup RTC by NTP
//
void setup_rtc()
{
  struct tm timeinfo;
  uint8_t hh, mm, ss;
  RTC_TimeTypeDef TimeStruct;

  // connect NTP server
  configTime(9 * 3600, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
  if (!getLocalTime(&timeinfo))
  {
    M5.Lcd.println("Failed to obtain time");
    return;
  }

  //
  // set localtime to RTC
  //
  hh = timeinfo.tm_hour;
  mm = timeinfo.tm_min;
  ss = timeinfo.tm_sec;

  TimeStruct.Hours = hh;
  TimeStruct.Minutes = mm;
  TimeStruct.Seconds = ss;
  M5.Rtc.SetTime(&TimeStruct);
}

//
// connect wifi
//
int setup_wifi()
{

  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(DARKGREY);

  WiFi.disconnect(true);

  WiFi.begin(ssid, password); // connect to wifi

  int ntry = 100;
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    M5.Lcd.print(".");

    ntry -= 1;
    if (ntry == 0)
    {
      // Serial.println("max try failed");
      M5.Lcd.setTextSize(2);
      M5.Lcd.setTextColor(RED);
      M5.Lcd.setCursor(0, 0);
      M5.Lcd.println("fail wifi connect");
      return -1;
    }
  }
  M5.Lcd.println("");
  M5.Lcd.println("WiFi connected.");
  M5.Lcd.print("IP address: ");
  M5.Lcd.println(WiFi.localIP());
  M5.Lcd.println();

  return 0;
}

/**
 * Connect MQTT
 *
 */
void connectMqtt()
{
  mqttClient.setServer(mqttHost, mqttPort);
  while (!mqttClient.connected())
  {
    Serial.println("Connecting to MQTT...");
    if (mqttClient.connect("room1"))
    {
      Serial.println("connected");
    }
    delay(500);
  }
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(16, 9 * 6);
  M5.Lcd.setTextColor(DARKGREEN);
  M5.Lcd.printf(topic);
}

void setup()
{
  M5.begin();
  M5.Axp.ScreenBreath(10);
  M5.Lcd.setRotation(3); // 左を上にす
  M5.Lcd.setTextSize(1);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(DARKGREY);
  delay(500);

  pinMode(GPIO_NUM_10, OUTPUT);
  pinMode(PIR_HAT_PIN, INPUT_PULLUP);

  //
  // Wifi
  //
  setup_wifi();

  //
  // setup RTC time
  //
  setup_rtc();

  //
  // MQTT
  //
  connectMqtt();
}

void loop()
{

  RTC_TimeTypeDef RTC_TimeStruct;

  M5.update();

  // PIR HAT(AS312)
  int pir = digitalRead(PIR_HAT_PIN);

  if (pir)
  {
    digitalWrite(GPIO_NUM_10, 0);
    payload = (char *)"{\"status\":true}";
    Is_on = true;
  }
  else
  {
    digitalWrite(GPIO_NUM_10, 1);
    payload = (char *)"{\"status\":false}";
  }

  M5.Rtc.GetTime(&RTC_TimeStruct);
  int diff = Last_sec - RTC_TimeStruct.Seconds;
  if (abs(diff) > 3)
  {
    M5.Lcd.setTextSize(1);
    M5.Lcd.setCursor(32, 9 * 8);
    M5.Lcd.setTextColor(DARKGREEN);
    M5.Lcd.fillRect(0, 9 * 8, 256, 12, BLACK);
    M5.Lcd.printf("Time: %02d : %02d : %02d\n",
                  RTC_TimeStruct.Hours, RTC_TimeStruct.Minutes, RTC_TimeStruct.Seconds);
    Last_sec = RTC_TimeStruct.Seconds;

    if (WiFi.status() == WL_CONNECTED)
    {
      if (mqttClient.publish(topic, payload))
      {
        Serial.print("publish ok");
        Serial.println(payload);
      }
      else
      {
        Serial.println("publish ng");
      }
      Is_on = false;
    }
  }
  mqttClient.loop();
  delay(WAIT_TIME);
}

publisher の結果

command line で、M5StickCから、sensor 情報が来てるのを確認

sh
$ mosquitto_sub -v -h 10.0.0.1 -t sensor/#
sensor/room1 {"status":true}
sensor/room1 {"status":false}
sensor/room1 {"status":false}

このままcommand line だと面白くないので、次回はNodeREDで可視化する。