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

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
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)挟むかも・・・。

ではでは