「ヤルキスイッチ」を作った

MQTT にも一通り使い慣れた?ので、
送信側に m5stack、受信側にブラウザを使用した「ヤルキスイッチ」を作成しました。
役には立ちません。いわゆる「ネタアプリ?」でしょうか。

今までコマンドラインだけ、スイッチだけだったものが、連動して動くようになると本当に楽しいです。

とりあえず動くものを見てほしいよ

完成品を動かすと、以下の動画みたいになります。

それじゃ解説です。

目次

全体の構成

以下の構成でデータが流れます。

M5Stack(mqtt 送信側) ==> 公開サーバー(mqtt ブローカー) ==> ブラウザ(mqtt 受信側)

ブローカーは前回のMQTT を使いたい。3で作成したものをベースにしただけなので、割愛。

  • M5Stack(mqtt 送信側)
  • ブラウザ(mqtt 受信側)

以上二つを主に解説します。

M5Stack(mqtt 送信側)

先に準備として m5stack には 320*200 の大きさの jpg 画像をそれぞれ

  • yaruki1logo.jpg
  • yaruki2logo.jpg
  • yaruki3logo.jpg
    という名前で保存しておきます。
    M5Stack の Lcd に、押したボタンに対応した画像を表示するためのものです。

以下を参考にして作成しました。
Arduino Client for MQTT
ESP32 を MQTT で Publish する
書き込んだソースファイルは以下の通り(簡単のため WIFI,MQTT の再接続は考慮しません)

yaruki-pub.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

#include <WiFi.h>
#include <M5Stack.h>
#include <PubSubClient.h>

//接続先SSID
const char* SSID = "derutaASUS";
//WIFI接続パスワード
const char* PW = "sankaku3";

//MQTTブローカーサーバーIP
const char *mqttHost = "xxx.xxx.xxx.xxx";
//MQTTブローカーサービス提供ポート
const int mqttPort = 00000;
//MQTTクライアントID
const char *clientid = "pubM5S";
//MQTTユーザー名
const char *user = "pub_user_M5S";
//MQTTパスワード
const char *pw = "xdorytbseyrixdfgdsfgns";
//送信トピック名
const char *topic = "topicSW";

//WIFIクライアント初期化
WiFiClient wifiClient;
//MQTTクライアント初期化
PubSubClient mqttClient(wifiClient);

//セットアップ
void setup()
{
//M5Stack初期化
M5.begin();
M5.Lcd.setBrightness(70);

//WIFI接続開始
WiFi.begin(SSID, PW);
M5.Lcd.setTextSize(2);
M5.Lcd.printf("Connecting");
while (WiFi.status() != WL_CONNECTED)
{
//WIWI接続が確立するまで待機
M5.Lcd.printf(".");
delay(500);
}
//WIFI接続完了
M5.Lcd.clear();
M5.Lcd.setCursor(0, 0);
M5.Lcd.printf("Connect!");

//MQTTサーバー接続開始
mqttClient.setServer(mqttHost, mqttPort);
while (!mqttClient.connected())
{
M5.Lcd.printf("Connecting to MQTT...");
if (mqttClient.connect(clientid, user, pw))
{
M5.Lcd.printf("mqttconnected");
break;
}
delay(1000);
randomSeed(micros());
}
}

//ループ処理
void loop()
{
M5.update();
//ボタンAを押したとき
if (M5.BtnA.wasPressed())
{
mqttClient.publish(topic, "{\"data\":{\"button\":\"push-A\"}}");
M5.Lcd.clear();
M5.Lcd.drawJpgFile(SD, "/yaruki1logo.jpg", 0, 40, 320, 200);
}
//ボタンBを押したとき
if (M5.BtnB.wasPressed())
{
mqttClient.publish(topic, "{\"data\":{\"button\":\"push-B\"}}");
M5.Lcd.clear();
M5.Lcd.drawJpgFile(SD, "/yaruki2logo.jpg", 0, 40, 320, 200);
}
//ボタンCを押したとき
if (M5.BtnC.wasPressed())
{
mqttClient.publish(topic, "{\"data\":{\"button\":\"push-C\"}}");
M5.Lcd.clear();
M5.Lcd.drawJpgFile(SD, "/yaruki3logo.jpg", 0, 40, 320, 200);
}
mqttClient.loop();
}

Web(mqtt 受信側)

ブラウザをでの MQTT 送受信は以下を参考にして作成しました。
Node.js で MQTT ブローカーを立てて、ブラウザから確認する

今回は MQTT の受信の結果で動きを見せたいので、
pixi.jsを使用しました。
次などを参考にしています。
PixiJS — The HTML5 Creation Engine
learningPixi

css は skelton-css を使用してます。

以下を作成しました。

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>YARUKI SWITCH</title>

<link rel="stylesheet" href="./Skeleton-2.0.4/css/normalize.css">
<link rel="stylesheet" href="./Skeleton-2.0.4/css/skeleton.css">
<link rel="stylesheet" href="./a.css">

</head>
<body>
<div class="container">
<div class="row">
<h1>YARUKISWICH</h1>
<div>
<div class="row">
<div class="twelve columns cn">
<canvas id="cvtarget"></canvas>
</div>
</div>
</div>
</body>
<script src="bundle.js"></script>
</html>
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import * as PIXI from "pixi.js";
const mqtt = require("mqtt");

//スプライトイラスト画像ファイル定義
let files = [
{ name: "0", pass: "images/yaruki1.png" },
{ name: "1", pass: "images/yaruki2.png" },
{ name: "2", pass: "images/yaruki3.png" },
{ name: "3", pass: "images/yaruki4.png" },
{ name: "back", pass: "images/back.png" },
];
//スプライトロゴ画像ファイル定義
let logofiles = [
{ name: "0", pass: "images/yaruki1logo.png" },
{ name: "1", pass: "images/yaruki2logo.png" },
{ name: "2", pass: "images/yaruki3logo.png" },
];

//テクスチャオブジェクト
let Textures = undefined;
let Textureslogo = undefined;

//テクスチャ読み込み関数
let load = function (files) {
let result = [];
files.forEach((el) => {
result.push({
Texture: PIXI.Texture.fromImage(el.pass),
filename: el,
name: el.name,
});
});
return result;
};

//登録名称から、テクスチャを出力する関数
function name2tex(tex, name) {
let result = undefined;
tex.forEach((el) => {
if (el.name == name) {
result = el.Texture;
}
});
return result;
}

//表示位置調整カウンタ
let count = 0;
//テクスチャ選択カウンタ
let texselect = 0;
//イラスト画像スプライト登録関数
function addsprite(select) {
select = select == undefined ? texselect : select;

Sprites.push(new PIXI.Sprite(name2tex(Textures, `${select}`)));
let len = Sprites.length - 1;
let r = Math.random();
Sprites[len].sp = 6 + 3 * r;
r += 0.5;
let scale = app.screen.width / Sprites[len].texture.baseTexture.width / 3;
Sprites[len].scale.x *= scale * r;
Sprites[len].scale.y *= scale * r;
Sprites[len].x = (app.screen.width / 4) * count * (r > 1 ? 1 : r);
Sprites[len].y = (app.screen.height * 2) / 3;
app.stage.addChild(Sprites[len]);

count++;
texselect++;
if (count > 3) {
count = 0;
}
if (texselect > 2) {
texselect = 0;
}
}

//ロゴ画像スプライト登録関数
function addspritelogo(select) {
console.log("addspritelogo");
select = select == undefined ? 1 : select;

Spriteslogo.push(new PIXI.Sprite(name2tex(Textureslogo, `${select}`)));
let len = Spriteslogo.length - 1;
let r = Math.random();
Spriteslogo[len].sp = 6 + 3 * r;
r += 0.5;
let scale = app.screen.width / Spriteslogo[len].texture.baseTexture.width / 3;
Spriteslogo[len].scale.x *= scale * r;
Spriteslogo[len].scale.y *= scale * r;
Spriteslogo[len].x = (app.screen.width / 4) * count;
Spriteslogo[len].y = (app.screen.height * 1) / 2;
app.stage.addChild(Spriteslogo[len]);
}

//スプライトオブジェクト
let Sprites = [];
let Spriteslogo = [];

//ラッシュ処理カウンタ
let lashcount = 0;

//キャンバスオブジェクト
let app = undefined;

function start() {
//キャンバスサイズ定義
let width = document.getElementById("cvtarget").width;
let height = width * 1.5;
height = height > window.innerHeight ? window.innerHeight : height;

//キャンバス作成
app = new PIXI.Application(width, height, {
backgroundColor: 0x1099bb,
view: document.body.querySelector("#cvtarget"),
});

//テクスチャー読み込み
Textures = load(files);
Textureslogo = load(logofiles);

//背景設定
var back = new PIXI.Sprite(name2tex(Textures, "back"));
//テクスチャのサイズをキャンバスのサイズに調整
back.scale.x *= app.screen.width / back.texture.baseTexture.width;
back.scale.y *= app.screen.height / back.texture.baseTexture.height;
//キャンバスに背景用スプライトを登録
app.stage.addChild(back);

//ルーチン処理
let loop = () => {
//イラスト画像ポップアニメーション
for (let i = 0; i < Sprites.length; i++) {
Sprites[i].y -= Sprites[i].sp;
Sprites[i].sp += -0.18;
if (Sprites[i].y > app.screen.height) {
app.stage.removeChild(Sprites[i]);
}
}
//ロゴ画像ポップアニメーション
for (let j = 0; j < Spriteslogo.length; j++) {
Spriteslogo[j].y -= Spriteslogo[j].sp;
Spriteslogo[j].sp += -0.18;
if (Sprites[j].y > app.screen.height) {
app.stage.removeChild(Spriteslogo[j]);
}
}
//ラッシュ処理用自動画像ポップアップ処理
if (lashcount > 0) {
if (lashcount % 5 == 0 && lashcount > 60) {
addspritelogo(0);
addsprite();
}
if (lashcount == 5) {
addspritelogo(1);
addsprite(3);
}
lashcount--;
}
};
//ルーチン処理登録
app.ticker.add(loop);

//MQTT設定
const client = mqtt.connect(
"ws://[mqttブローカーサーバアドレス]:[mqttブローカサービスポート]",
{
clientId: "[クライアントID]",
username: "[ユーザー名]",
password: "[接続パスワード]",
}
);
//MQTT購読登録
client.subscribe("topicSW");

//MQTTメッセージ受信処理
client.on("message", (topic, message) => {
let data = JSON.parse(message.toString()).data;
if (data.button == "push-A") {
addspritelogo(0);
addsprite();
} else if (data.button == "push-B") {
addspritelogo(1);
addsprite(3);
} else if (data.button == "push-C") {
lashcount += 200;
}
console.log(JSON.parse(message.toString()));
});
}

start();

index.js は Webpack+babel でトランスパイルして使用します。
これらを動かすと冒頭の「ヤルキスイッチ」のフロントエンドが完成します。

この時の package.json は以下のようになります。

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"name": "yarukiswitch",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --watch --progress --mode development",
"dev": "webpack-dev-server --watch --progress --mode development"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.2.2",
"babel": "^6.23.0",
"babel-core": "^6.26.3",
"babel-loader": "^8.0.4",
"babel-preset-es2015-riot": "^1.1.0",
"webpack": "^4.28.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
},
"dependencies": {
"mqtt": "^2.18.8",
"pixi.js": "^4.8.5"
}
}

だいたい 1 か月で、デバイスからインターネットを介して、ブラウザとやり取りができるようになりました。
まさに IoT な感じです。

ツイッターにアップしたテスト版は某プロレスラーをイメージしたいらすとやの画像を混ぜる悪ノリをしたけど、
記事にするにあたって、C ボタンは「ヤルキラッシュ」ということで雪崩のように画像が流れて、
最後に「モエツキ」がポンと一つだけポップするようにしました。
何気にその最後の挙動がお気に入りだったりします。

M5Stack とかを ArduinoIDE で開発をしていると、文字列の型の宣言にchar 変数名*に書いたあたり、
昔 C を書いてた頃を思い出して少し懐かしい感じでした。

次回は MQTT で、デバイスは obniz を使用してドアセンサーを仕上げたいと思います。
間に何か(pixi.js と riot.js)挟むかも・・・。

ではでは