押されたボタンの状態をMQTTで送信する、IoTデバイス"DC-01"を作りました。
今回は、このデバイスを「ワイヤレスMIDIコントローラ」として使用した際の技術について解説します。
#システム概要
このシステムは、大きく分けて3つのコンポーネントから成り立ちます。
##1.ハードウェアデバイス「DC-01」
ESPr Developerにボタンが備わっており、ボタンが押された/離した際にMQTTトピックを送信します。
トピックはdc-01/button/* (*はボタンの番号)
メッセージは1(押された際)か0(離した際)です。
##2.VPS
MQTTブローカーとしてMosquittoを運用しています。
将来的には、fluentdでボタンの使用状況をログとして貯めておこうと考えています。
##3.クライアントPC
作曲ソフト「Cubase」にMIDI信号を送信するため、MQTTトピックをMIDIデータに変換するプログラムをC#で実装しています。
MIDI信号は、バーチャルMIDIデバイス「loopMIDI」を使用してCubaseに伝えます。
Cubase側で設定することで、ドラムのキックやスネアを鳴らすドラムマシンとして使ったり、
再生や停止機能を持たせたコントローラとして使ったりすることが出来ます。
#回路図
fritzingにESPr Developerのモジュールがないため、同チップを搭載したAdafruit社製モジュールを使って描きました。
5つの三和製ボタン、1つのLEDをESPr Developerに取り付けます。
ボタンはIO16,5,4,14,12番ポートを、LEDは13番ポートを使用します。
ボタンにはそれぞれプルダウン抵抗10kΩを、LEDにはプルアップ抵抗270Ωを使用します。
#ソースコード・スケッチ解説
##ESPr Developer
上記回路図で組んだIoTデバイスのスケッチです。
ライブラリとしてPubSubClientを使用し、ソースコードはこれのExampleを基に作りました。
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#define BUTTON_NUM 5
int pin[BUTTON_NUM] = {16, 5, 4, 14, 12};
const int play = 100;
int ct[BUTTON_NUM], pt[BUTTON_NUM];
bool flag[BUTTON_NUM];
bool connect_complete = false;
const char* ssid = "... your ssid ...";
const char* password = "... your password ...";
const char* mqtt_server = "... your mqtt server address ...";
const char* mqtt_user = "... your mqtt username ...";
const char* mqtt_pass = "... your mqtt password ...";
WiFiClient espClient;
PubSubClient client(espClient);
void setup() {
for (int i = 0; i < BUTTON_NUM; i++) {
ct[i] = 0; pt[i] = 0;
flag[i] = false;
pinMode(pin[i], INPUT);
}
pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output
pinMode(13, OUTPUT);
digitalWrite(13, HIGH);
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void setup_wifi() {
delay(10);
// We start by connecting to a WiFi network
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
connect_complete = true;
digitalWrite(13, LOW);
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
// Switch on the LED if an 1 was received as first character
if ((char)payload[0] == '1') {
digitalWrite(BUILTIN_LED, LOW); // Turn the LED on (Note that LOW is the voltage level
// but actually the LED is on; this is because
// it is acive low on the ESP-01)
} else {
digitalWrite(BUILTIN_LED, HIGH); // Turn the LED off by making the voltage HIGH
}
}
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Attempt to connect
if (client.connect("ESP8266Client", mqtt_user, mqtt_pass)) {
connect_complete = true;
digitalWrite(13, LOW);
Serial.println("connected");
// Once connected, publish an announcement...
//client.publish("outTopic", "hello world");
// ... and resubscribe
//client.subscribe("inTopic");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
void loop() {
if (!client.connected()) {
connect_complete = false;
digitalWrite(13, HIGH);
reconnect();
}
if (connect_complete == true) digitalWrite(13, LOW);
client.loop();
for (int i = 0; i < BUTTON_NUM; i++) {
//ボタン押下判定
if (digitalRead(pin[i]) != flag[i]) {
//チャタリング判定
ct[i] = millis();
if ((ct[i] - pt[i]) > play) {
//ボタンONOFF時の動作
if (connect_complete == true) digitalWrite(13, HIGH);
flag[i] = digitalRead(pin[i]);
char msg[50];
char topic[50];
sprintf(msg, "%d", flag[i]);
sprintf(topic, "dc01/button/%d", i);
Serial.print("changed. current is ");
Serial.println(flag[i]);
client.publish(topic, msg);
pt[i] = ct[i];
}
}
}
}
setup()関数ではボタン用変数の初期化・Wifiセットアップ・MQTTセットアップを行います。
loop()関数ではMQTT接続のチェックと、ボタンが押されたかどうかの判定を行っています。
ボタンが押された場合は対応する番号のトピックにメッセージを送信します。
LEDは、MQTT接続が確立されてないときは常灯し、接続完了となると消灯します。
また、MQTTメッセージを送信する際、一瞬点滅するようになっています。
##C#プログラム
ESPr DeveloperのメッセージをSubscribeし、MIDIノートを送信します。
MQTT通信にはM2MQttを、MIDIにはNextMIDIを使用しており、サンプルを基に作りました。
using NextMidi.DataElement;
using NextMidi.MidiPort.Output;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using uPLibrary.Networking.M2Mqtt;
using uPLibrary.Networking.M2Mqtt.Messages;
namespace MQTT_MIDI_Converter_CUI
{
class Program
{
static void Main(string[] args)
{
// MIDI ポート名を指定して MIDI ポートを開く
if (args.Length != 1)
{
Console.WriteLine("1 port name required");
return;
}
var port = new MidiOutPort(args[0]);
try
{
port.Open();
}
catch
{
Console.WriteLine("no such port exists");
return;
}
MqttClient client;
client = new MqttClient(@const.MyTopic.address);
client.MqttMsgPublishReceived += (sender, eventArgs) =>
{
String msg = Encoding.UTF8.GetString(eventArgs.Message);
String topic = eventArgs.Topic;
#if DEBUG
Console.WriteLine(topic.Split('/')[2] + ", " + msg);
#endif
byte note = (byte)(60 + int.Parse(topic.Split('/')[2]));
if (msg.Equals("1"))
{
port.Send(new NoteOnEvent(note, 127));
}
else
{
port.Send(new NoteOffEvent(note));
}
};
string clientId = Guid.NewGuid().ToString();
byte ret = client.Connect(clientId, @const.MyTopic.user, @const.MyTopic.pass);
Console.WriteLine("Connected with result code {0}", ret);
client.Subscribe(new[] { "dc01/button/0" }, new[] { MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE });
client.Subscribe(new[] { "dc01/button/1" }, new[] { MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE });
client.Subscribe(new[] { "dc01/button/2" }, new[] { MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE });
client.Subscribe(new[] { "dc01/button/3" }, new[] { MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE });
client.Subscribe(new[] { "dc01/button/4" }, new[] { MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE });
client.Subscribe(new[] { "dc01/button/5" }, new[] { MqttMsgBase.QOS_LEVEL_EXACTLY_ONCE });
while (client.IsConnected)
{
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MQTT_MIDI_Converter_CUI
{
class @const
{
public struct MyTopic
{
public static string address = "... your mqtt server address ...";
public static string user = "... your mqtt username ...";
public static string pass = "... your mqtt password";
}
}
}
ここでは、トピックdc01/button/[0,1,2,3,4,5]に対し、
メッセージが1の際は、それぞれMIDIノート[60,61,62,63,64,65]をベロシティ127で送信しています。
メッセージが0の際は、ノートオフを送信します。
#使用方法
上記の回路図として設計されたESPr Developerが必要です。
- VPSでMosquittoをインストールし、MQTTブローカーとして機能させます。
- ESPr Developerのスケッチを書き込み、電源をオンにします。
- クライアントPCで、MIDIデバイス「loopMIDI」とC#で書いたMIDI変換プログラムを立ち上げ、作曲ソフトを立ち上げます。
- ESPr Developerのボタンを押せば、作曲ソフトで反応があるはずです。
詳しい使い方や組み立て方、使った際のデモについてはこちらのブログ記事も参考にしてください。
#今後の課題
現状では「ただデータを流すだけ」のシステムなので、イマイチIoTっぽくありませんね。
今後はfluentdにログを貯めて、ライフログを可視化したりとなにか面白いことが出来ないか考えていきたいと思います。