ATOM Matrixでボタン押下時にMQTT Publish、ただそれだけ

M5StackシリーズのATOM Matrixを使って、ボタン押下時にMQTTブローカに対してPublishしてみます。
ただただそれだけのつもりだったのですが、なんだかんだ機能追加してしまいました。

ソース一式をGitHubに上げておきます。

poruruba/mqttbutton
　https://github.com/poruruba/mqttbutton

# 準備

今回は、Microsoft Visual Studio CodeのPlatform IOを使って開発しました。もちろんArduino IDEでもOKです。
以下のライブラリをインストールしておきます。

・M5Atom
・FastLED
・PubSubClient
・ArduinoJson
・M5StickCも必要かも

あとは、プロジェクト作成時に、Boardとして、「M5Stick-C (M5Stack)」を選択します。FramewarkとしてArduinoを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3d609158-0ee1-f42a-c239-93670b5bf7b3.png)



# ボタン押下を検出してMQTT Publish

以下に必要なコードを示しておきます。

```c:main.cpp
#include <M5Atom.h>
#include <WiFi.h>
#include <PubSubClient.h>

const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";
const char* mqtt_server = "【MQTTブローカのIPアドレスまたはホスト名】";
const uint16_t mqtt_port = 1883; // MQTTブローカのポート番号(TCP接続)
#define MQTT_CLIENT_NAME  "M5Atom" // MQTTブローカ接続時のクライアント名

#define BTN_PUSH_MARGIN     100 // ボタン操作のチャタリング対策
#define MQTT_BUFFER_SIZE  512 // MQTT送受信のバッファサイズ

bool pressed = false; // 以前ボタンを押下した状態だったか

// WiFiアクセスポイントへの接続
void wifi_connect(void){
  Serial.print("WiFi Connenting");

  WiFi.begin(wifi_ssid, wifi_password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("");
  Serial.print("Connected : ");
  Serial.println(WiFi.localIP());

  // バッファサイズの変更
  client.setBufferSize(MQTT_BUFFER_SIZE);
  // MQTTブローカの接続設定
  client.setServer(mqtt_server, mqtt_port);
}

// ボタン押下イベントをMQTTで通知
void sendMessage(void){
  // ここでMQTT Publishすればよい
}

void setup() {
  M5.begin(true, false, true);

  Serial.begin(9600);
  wifi_connect();
}

void loop() {
  client.loop();
  // MQTT未接続の場合、再接続
  while(!client.connected() ){
    Serial.println("Mqtt Reconnecting");
    if( client.connect(MQTT_CLIENT_NAME) ){
      Serial.println("Mqtt Connected");
      break;
    }
    delay(1000);
  }

  M5.update();

    if (M5.Btn.isPressed() && !pressed ){
      pressed = true;
      // ボタン押下イベントを送信
      sendMessage();

      delay(BTN_PUSH_MARGIN);
    }else if( M5.Btn.isReleased() && pressed ){
      pressed = false;

      delay(BTN_PUSH_MARGIN);
    }
}
```

以下は、それぞれの環境に合わせて変更してください。
・【WiFiアクセスポイントのSSID】
・【WiFiアクセスポイントのパスワード】
・【MQTTブローカのIPアドレスまたはホスト名】

```client.setBufferSize()``` は、デフォルトで受信サイズが256バイトと小さいので、広げておくためのものです。

あとは、```void sendMessage(void)``` のところに、MQTT Publishするコードを記載すればよいだけです。
MQTT Publishは以下のような感じです。

```c
client.publish("【トピック名】", "【送信したいメッセージ】");
```

（参考）Arduino Client for MQTT
　https://pubsubclient.knolleary.net/api

# なんか機能を追加したくなる

単純な機能を作りこむだけのつもりだったのですが、だんだん以下の機能を追加したくなりました。

* LEDマトリクスに文字を表示する。
* ボタンの状態を記憶して、ボタンを押すたびに、A→B→C→A→・・・ というように、遷移させる。
* 通常の1クリックと、上記の状態を遷移させるモードや、LEDマトリクスに表示する文字を、MQTT Publishで変えられるようにする

# LEDマトリクスに文字を表示

LEDマトリクスは、5×5なので、5x5ドットのフォントを用意しました。
モノクロのビットマップフォントなので、文字色や背景色は別途指定が必要です。
1バイトで1ライン(5ビット)分を表現したので、1文字5バイトになります。
用意したのは、「 （スぺース）」から「~（チルダ)」までの文字です。
以下、宣言です。bmpfont_5x5という変数にビットマップデータが入っています。

```c
// 5x5ドットフォント
#include "bmpfont5x5.h"
```

次に、これらをLEDマトリクス表示に変換する関数を作ってみました。

```c:main.cpp
#define MATRIX_WIDTH  5 // LEDマトリクスの幅

void setText(char ch, CRGB fore, CRGB back, uint8_t rotate = 0, bool invert = false){
  if( ch >= ' ' && ch <= '~' ){
    const uint8_t *t = &bmpfont_5x5[(ch - ' ') * MATRIX_WIDTH];
    uint8_t buffer[2 + 3 * MATRIX_WIDTH * MATRIX_WIDTH] = { MATRIX_WIDTH, MATRIX_WIDTH };
    for( int y = 0 ; y < MATRIX_WIDTH ; y++ ){
      for( int x = 0 ; x < MATRIX_WIDTH ; x++ ){
        // 回転後のポジションの算出
        int pos;
        if( rotate == 0 )
          pos = 2 + (y * MATRIX_WIDTH + x) * 3; 
        else if( rotate == 1 )
          pos = 2 + (x * MATRIX_WIDTH + (MATRIX_WIDTH - y - 1)) * 3;
        else if( rotate == 2 )
          pos = 2 + ((MATRIX_WIDTH - y - 1) * MATRIX_WIDTH + (MATRIX_WIDTH - x - 1)) * 3;
        else
          pos = 2 + ((MATRIX_WIDTH - x - 1) * MATRIX_WIDTH + y) * 3;

        // 文字色の設定
        CRGB color = ( t[y] & (0x01 << (MATRIX_WIDTH - x - 1)) ) ? fore : back;
        buffer[pos + 0] = color.r;
        buffer[pos + 1] = color.g;
        buffer[pos + 2] = color.b;
        if( invert ){
          // invert==true の場合に減色
          buffer[pos + 0] /= 3;
          buffer[pos + 1] /= 3;
          buffer[pos + 2] /= 3;
        }
      }
    }
    // LEDマトリクスに表示
    M5.dis.displaybuff(buffer, 0, 0);
  }else{
    // サポートしない文字の場合は表示を消去
    M5.dis.clear();
  }
}
```

表示する文字と、文字色・背景色のほかに、文字を0°・90°・180°・270°に回転させるパラメータや、アクセントをつけるための表示の明るさを一時的に暗くするパラメータを用意しています。

# MQTT Publish/Subscribeで送受信するJSON処理

JSON処理に、ArduinoJsonを利用しました。

（参考）ArduinoJson
　https://arduinojson.org/v6/doc/

まずは、MQTT受信処理です。

```c:main.cpp
#include <ArduinoJson.h>

const char* topic = "btn/m5atom"; // ボタン押下時の通知用のトピック名
const char* topic_setting = "btn/m5atom_setting"; // ボタン設定受信用のトピック名

// MQTT Subscribe用
const int request_capacity = JSON_OBJECT_SIZE(4) + JSON_ARRAY_SIZE(NUM_OF_BTNS) + NUM_OF_BTNS * JSON_OBJECT_SIZE(4);
StaticJsonDocument<request_capacity> json_request;
// MQTT Publish用
const int message_capacity = JSON_OBJECT_SIZE(3);
StaticJsonDocument<message_capacity> json_message;
char message_buffer[MQTT_BUFFER_SIZE];

bool mode_reset = false; // MQTT Subscribeで新しい設定を受信しているかどうか

// MQTT Subscribeで受信した際のコールバック関数
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  Serial.println("received");
  if( mode_reset ){
    // まだ以前に受信したボタン設定が未設定の場合は無視
    Serial.println("Mode change Busy");
    return;
  }

  DeserializationError err = deserializeJson(json_request, payload, length);
  if( err ){
    Serial.println("Deserialize error");
    Serial.println(err.c_str());
    return;
  }

  // 新しいボタン状態を受信したことをセット
  mode_reset = true;
}

// MQTTで受信したボタン設定を解析し設定
void parseRequest(void){
  serializeJson(json_request, Serial);
  Serial.println("");

  current_mode = json_request["mode"];
  current_id = json_request["id"] | 0;
  uint8_t brightness = json_request["brightness"] | DEFAULT_BRIGHTNESS;
  M5.dis.setBrightness(brightness);
  JsonArray array = json_request["btns"].as<JsonArray>();
  num_of_btns = array.size();
  if( num_of_btns > NUM_OF_BTNS )
    num_of_btns = NUM_OF_BTNS;

  for( int i = 0 ; i < num_of_btns ; i++ ){
    uint32_t color;
    if( array[i]["fore"] ){
      color = array[i]["fore"];
      btns[i].fore = color;
    }else{
      btns[i].fore = default_fore;
    }
    if( array[i]["back"] ){
      color = array[i]["back"];
      btns[i].back = color;
    }else{
      btns[i].back = default_back;
    }
    const char* str = array[i]["char"];
    btns[i].ch = str[0];
    btns[i].rotate = array[i]["rotate"] | DEFAULT_ROTATE;
  }
}
```

ボタン動作モード設定時に受信するJSONフォーマットは以下の感じです。

```
{
  “mode”: ボタン動作モード, (0: 停止、1: クリック時送信、2: クリック時＋状態保持送信)
  “id”: 11223344, // 任意(未指定時は0)
  “brightness”: 20, // 任意
  “btns”: [
    {
      “char”: “【表示したい文字】”,
      “fore”: 文字色(Number型), // 任意(未指定時は白)
      “back”: 背景色(Number型), // 任意(未指定時は黒)
      “rotate”: 回転(0～3) // 任意(未指定時は0)
    },
    // クリック時送信時は1個まで、クリック時＋状態保持送信時は5個まで
  ]
}
```

ボタン動作モード設定のMQTTを受け付けるため、```void wifi_connect(void)``` の中で以下を呼び出しておきます。

```c
  // MQTTコールバック関数の設定
  client.setCallback(mqtt_callback); 
```

そうすることで、MQTTを受信すると、``` void mqtt_callback(char* topic, byte* payload, unsigned int length)``` が呼び出されるようになります。
その中で、受信したMQTTをいったんStaticJsonDocument型の変数に格納した後、```void parseRequest(void)``` で内容を抜き出しています。

また、```loop()``` におけるMQTTブローカ接続成功時に、ボタン動作モード設定のMQTTを受信するために、Subscribe設定しておきます。

```c:main.cpp
      // MQTT Subscribe
      client.subscribe(topic_setting);
```

次は、MQTT送信の部分です。
ボタン押下時に通知されるJSONフォーマットは以下の感じです。

```
{
  “mode”: ボタン動作モード,
  “id”: ボタン設定のID(ボタン動作モード設定時に指定したもの)
  “index”: 表示ボタンのインデックス //クリック時＋状態保持送信時のみ
}
```

MQTT送信処理はこんな感じの実装になります。

```c
// ボタン押下イベントをMQTTで通知
void sendMessage(void){
  json_message.clear();
  json_message["mode"] = current_mode;
  json_message["id"] = current_id;
  if( current_mode == TOGGLE )
    json_message["index"] = current_btn_index;

  serializeJson(json_message, message_buffer, sizeof(message_buffer));
  client.publish(topic, message_buffer);
}
```

あと、ボタン押下の状態遷移を覚えておくようにしていたりしています。
詳細は、GitHubのソースをご参照ください。

#テスト用クライアント

テストのためにクライアントを仕立てます。
今回はブラウザにします。

MQTTクライアントには、Pahoを使いました。
あとは、BootstrapやらVueやらを使っています。

こんな感じの画面です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/904ea8d7-2eb6-0914-a9a5-9223be3302e0.png)

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>MqttButton テスト</title>

  <link rel="stylesheet" href="css/start.css">
  <script src="js/methods_bootstrap.js"></script>
  <script src="js/components_bootstrap.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>  
</head>
<body>
    <div id="top" class="container">
        <h1>MqttButton テスト</h1>

        <label>connected</label> {{connected}}<br>
        <label>mqtt_url</label> <input type="text" class="form-control" v-model="mqtt_url">
        <button class="btn btn-primary" v-on:click="connect_mqtt()">connect</button><br>
        <br>
        <div class="panel panel-default">
          <div class="panel-body">
            <h4>Received Message</h4>
            <div v-if="received_date!=null">
              <label>date</label> {{received_date}}<br>
              <label>mode</label> {{received_mode}}<br>
              <label>id</label> {{received_id}}<br>
            </div>
            <div v-if="received_index!=null">
              <label>index</label> {{received_index}}
            </div>
          </div>
        </div>
        <button class="btn btn-primary" v-on:click="publish_setting()">setting</button>
        <br><br>
        <div class="form-inline">
          <label>mode</label>
          <select class="form-control" v-model.number="mode">
            <option value="0">IDLE</option>
            <option value="1">CLICK</option>
            <option value="2">TOGGLE</option>
          </select>
          <label>id</label> <input type="number" class="form-control" v-model.number="id">
          <label>brightness</label> <input type="number" class="form-control" v-model.number="brightness">
        </div>
        <br>
        <table class="table table-striped">
          <tbody>
            <tr v-for="(item, index) in btns">
              <th>
                <div class="checbox">
                  <input type="checkbox" v-model="btns[index].enable"> 
                </div>
              </th><th>
                <div class="form-inline">
                  <label>char</label> <input type="text" class="form-control" v-model="btns[index].char">
                </div>
                <div class="form-inline">
                  <label>fore</label> <input type="color" v-model.number="btns[index].fore">
                  <label>RGB:</label> <input type="text" size="7" class="form-control" v-model="btns[index].fore">
                  <label>back</label> <input type="color" v-model.number="btns[index].back">
                  <label>RGB:</label> <input type="text" size="7" class="form-control" v-model="btns[index].back">
                </div>
                <div class="form-inline">
                  <label>rotate</label>
                  <select class="form-control" v-model.number="btns[index].rotate">
                    <option value="0">0°</option>
                    <option value="1">90°</option>
                    <option value="2">180°</option>
                    <option value="3">270°</option>
                  </select>
                </div>
              </th>
            </tr>
          </tbody>
        </table>

        <!-- for progress-dialog -->
        <progress-dialog v-bind:title="progress_title"></progress-dialog>
    </div>

    <script src="js/start.js"></script>
</body>
```

Javascriptです。

（参考）Eclipse Paho JavaScript Client
 https://www.eclipse.org/paho/files/jsdoc/Paho.MQTT.Client.html

```js:start.js
'use strict';

//var vConsole = new VConsole();

const mqtt_url = "【MQTTブローカのURL(WebSocket接続)】";
var button_setting = [
    {
        enable: true,
        char: "a",
        fore: "#ffffff",
        back: "#000000",
        rotate: 0
    },
    {
        enable: true,
        char: "b",
        fore: "#ff0000",
        back: "#000000",
        rotate: 0
    },
    {
        enable: true,
        char: "c",
        fore: "#00ff00",
        back: "#000000",
        rotate: 0
    },
    {
        enable: true,
        char: "d",
        fore: "#0000ff",
        back: "#000000",
        rotate: 0
    },
    {
        enable: true,
        char: "e",
        fore: "#ffff00",
        back: "#000000",
        rotate: 0
    },
];

var mqtt_client = null;

var vue_options = {
    el: "#top",
    data: {
        progress_title: '', // for progress-dialog

        mqtt_url: mqtt_url,
        connected: false,
        received_mode: null,
        received_id: null,
        received_index: null,
        id: 0,
        mode: 1,
        brightness: 20,
        received_date: null,
        btns: button_setting
    },
    computed: {
    },
    methods: {
        mqtt_onMessagearrived: async function(message){
            var topic = message.destinationName;
            console.log(topic);
            switch(topic){
                case "btn/m5atom": {
                    var body = JSON.parse(message.payloadString);
                    console.log(body);
                    this.received_date = new Date().toLocaleString();
                    this.received_mode = body.mode;
                    this.received_id = body.id;
                    this.received_index = body.index;
                    break;
                }
                default:
                    console.log('Unknown topic');
                    break;
            }
        },
        mqtt_onConnectionLost: function(errorCode, errorMessage){
            console.log("MQTT.onConnectionLost", errorCode, errorMessage);
            this.connected = false;
        },
        mqtt_onConnect: async function(){
            console.log("MQTT.onConnect");
            this.connected = true;
            mqtt_client.subscribe("btn/m5atom");
        },
        connect_mqtt: async function(){
            mqtt_client = new Paho.MQTT.Client(this.mqtt_url, "browser");
            mqtt_client.onMessageArrived = this.mqtt_onMessagearrived;
            mqtt_client.onConnectionLost = this.mqtt_onConnectionLost;
            
            mqtt_client.connect({
                onSuccess: this.mqtt_onConnect
            });
        },
        publish_setting: function(){
            if( !this.connected ){
                alert("MQTTブローカに接続していません。");
                return;
            }

            var data = {
                mode: this.mode,
                id: this.id,
                brightness: this.brightness,
                btns: []
            };
            for( var i = 0 ; i < this.btns.length ; i++ ){
                if( !this.btns[i].enable )
                    continue;
                var btn = {
                    char: this.btns[i].char,
                    fore: this.rgb2num(this.btns[i].fore),
                    back: this.rgb2num(this.btns[i].back),
                    rotate: this.btns[i].rotate
                };
                data.btns.push(btn);
            }
            var message = new Paho.MQTT.Message(JSON.stringify(data));
            message.destinationName = "btn/m5atom_setting";
            mqtt_client.send(message);
        },
        rgb2num: function(rgb){
            return parseInt(rgb.slice(1, 7), 16);
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_methods(vue_options, methods_bootstrap);
vue_add_components(vue_options, components_bootstrap);
var vue = new Vue( vue_options );
```

【MQTTブローカのURL(WebSocket接続)】には環境に合わせて指定してください。ブラウザからの接続の場合は、WebSocket接続が必要です。
例：ホスト名test.sample.com、ポート番号1884の場合、"ws://test.sample.com:1884/" となります。

その他細かなファイルは、GitHubを参照してください。

#終わりに

ArduinoJsonは、使うのに少々コツが必要です。以下も参考にしてください。
　[M5Stick-CでJsonをPOSTする](https://qiita.com/poruruba/items/4bf6a52520e431a8f4a5)
MQTTのブローカは、以下を参考にしてください。
　[AWS IoTにMosquittoをブリッジにしてつなぐ](https://qiita.com/poruruba/items/084317e01b9fababc02e)

以上