見出し画像

OBSをM5Stackでコントロールする

自作シリーズ!noteには、M5Stackで遊んでる様子を何個かあげてました。

今回は、OBS Stduio のシーンスイッチャーにしてみましょう。

WebSocketでのやりとり

OBS Studio は、映像録画/配信ソフトです。そして、遠隔操作するにはWebSocketプラグインを追加しましょう。

これをいれたら、あとは決められたシーケンスに従ってデータを送っておけば、遠隔操作が完了します。

ちょっとだけ解説

{
    "request-type":"SetCurrentScene",
    "message-id":"1aba1234Aasvee1a",
    "scene-name":"シーン1"
}

送信されるデータはJSON形式。そして、リクエストタイプが「行うべきアクション」の指定になります。

そのアクションごとに、必要なパラメータがあるので、その分を追加して送付してください。(たとえば、シーン切り替えならシーン名が必要。)

また、メッセージIDは16桁のランダムでユニークな文字列を追加する仕様ですが、そうじゃなくても一応動くようです。

動いた様子

動画は、twitterに公開してます。

謝辞

各種プログラムや、関連ブログの情報を応用させていただいてます。

文字表示関連ライブラリ
ボタン関連ライブラリ
JSONライブラリ
WebSocket通信
フォント関連

(まだまだあるとおもう)

プログラム

エラー処理があまいし、時々リセットするので、もうちょっと作りこんだりする必要はあるんだけど、ひとまず公開。

SDに東雲フォントをおかないと、何も画面にでないのと、OBSのシーン名は「シーン1」~「シーン3」を切り替えるので、名前を合わせてみてください。

//==================================================
// OBS Studio 連携操作例 on M5Stack
// 2019.3.15
//==================================================

//================================================== 
//プラットフォームライブラリ
//================================================== 
#include <M5Stack.h>

//================================================== 
//文字表示関連ライブラリ
//参考:https://www.mgo-tec.com/blog-entry-m5stack-font-scrolle-esp32.html
//================================================== 
#include <ESP32_SD_UTF8toSJIS.h>
#include <ESP32_LCD_ILI9341_SPI.h>
#include <ESP32_SD_ShinonomeFNT.h>

//================================================== 
//ボタン関連ライブラリ
//参考:https://www.mgo-tec.com/blog-entry-m5stack-button-arduino-esp32.html
//================================================== 
#include <ESP32_Button_Switch.h>

//================================================== 
//JSON分解ライブラリ
//参考: http://shuzo-kino.hateblo.jp/entry/2016/05/06/203603
//================================================== 
#include <ArduinoJson.h>

//================================================== 
//WS通信ライブラリ
//参考:https://log.niccol.li/2012/05/arduinowebsocketsocketio.html
//================================================== 
#include <WebSockets.h>
#include <WebSocketsClient.h>
#include <WebSocketsServer.h>

//================================================== 
//WiFi通信ライブラリ
//================================================== 
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiMulti.h>
#include <WiFiClientSecure.h>

//================================================== 
//個別設定の定数
//================================================== 
const char SSID[] = "WIFI000";       //WiFi SSID
const char PASSWORD[] = "PASSWORD";    //WiFi パスワード
const char WebServerIP[] = "192.168.0.2"; //OBS IP
const int  WebServerPort = 4444;            //OBS port
//================================================== 
//ハード依存の定数
//================================================== 
const int8_t sck = 18; // SPI clock pin
const int8_t miso = -1; // MISO(master input slave output) don't using
const int8_t mosi = 23; // MOSI(master output slave input) pin

const int8_t cs = 14; // Chip Select pin
const int8_t dc = 27; // Data/Command pin
const int8_t rst = 33; // Reset pin

const int8_t LCD_LEDpin = 32;

const uint8_t CS_SD = 4; //SD card CS ( Chip Select )

const uint8_t buttonA_GPIO = 39;
const uint8_t buttonB_GPIO = 38;
const uint8_t buttonC_GPIO = 37;

String prev_message="";

//================================================== 
//fontの配置(SD上)
//参考;https://www.mgo-tec.com/kanji-font-shinonome
//================================================== 
const char* UTF8SJIS_file = "/font/Utf8Sjis.tbl"; //UTF8 Shift_JIS 変換テーブルファイル名を記載しておく
const char* Shino_Zen_Font_file = "/font/shnmk16.bdf"; //全角フォントファイル名を定義
const char* Shino_Half_Font_file = "/font/shnm8x16r.bdf"; //半角フォントファイル名を定義
 
//================================================== 
//変数定義(操作用オブジェクト)
//================================================== 
WebSocketsClient wsclient;
ESP32_LCD_ILI9341_SPI LCD(sck, miso, mosi, cs, dc, rst, LCD_LEDpin);
ESP32_SD_ShinonomeFNT SFR(CS_SD, 40000000);
ESP32_Button_Switch BTN;

uint8_t btn_stateA = _Release;
uint8_t btn_stateB = _Release;
uint8_t btn_stateC = _Release;
TaskHandle_t th; //マルチタスクハンドル定義

//================================================== 
//WS : →M5stackに通信がきたとき
//================================================== 
void onMessage(WStype_t type, uint8_t * payload, size_t length){

  //分解
  StaticJsonDocument<1200> jsonBuffer;
  DeserializationError error =deserializeJson(jsonBuffer,(char *)payload);

  Serial.println((const char*)payload);
  
  if (!error) 
  {
    JsonObject root = jsonBuffer.as<JsonObject>();

      //画面を切り替えた
      if(root["update-type"]=="SwitchScenes")
      {
          M5.Lcd.fillScreen(BLACK);  
          printjp(0, 0,"[現在]",0xff,0xff,0xff,3);
          printjp(0,48,root["scene-name"],0xff,0xff,0xff,3);
          Serial.println((const char*)root["scene-name"]);
      }
  }
}

//================================================== 
//LCDに日本語を出すためのラッパー関数
//================================================== 
void printjp(int x,int y,String moji,int red,int green,int blue,int fsize)
{
  uint8_t test_buf[100][16] = {};
  uint8_t test_sj_length = SFR.StrDirect_ShinoFNT_readALL(moji, test_buf);
  uint8_t LineLen=18;
  uint8_t tLineLenq=LineLen;

  uint8_t i2=0;
  for(uint8_t i=0;i<test_sj_length;i+=tLineLenq)
  {
    tLineLenq=LineLen;
    if(test_sj_length-i2*LineLen<LineLen) { tLineLenq=test_sj_length-i2*LineLen;}
    LCD.HVsizeUp_8x16_Font_DisplayOut(fsize, fsize, tLineLenq, x, y+i2*fsize*16, red, green, blue, test_buf+i);
    i2++;
  }

}

//================================================== 
//イニシャライズ
//================================================== 
void setup() {
  
  //ハード初期化
  m5.begin();
  M5.Speaker.mute();
  dacWrite(25, 0); // Speaker OFF
  
  pinMode(buttonA_GPIO, INPUT); //GPIO #39 は内部プルアップ無し
  pinMode(buttonB_GPIO, INPUT); //GPIO #38 は内部プルアップ無し
  pinMode(buttonC_GPIO, INPUT); //GPIO #37 は内部プルアップ無し

  //シリアルモニタ用初期化
  Serial.begin(115200);

  //フォント読み込み
  SFR.SD_Shinonome_Init3F(UTF8SJIS_file, Shino_Half_Font_file, Shino_Zen_Font_file); //ライブラリ初期化。3ファイル同時に開く

  //画面出力系初期化
  LCD.ILI9341_Init(false, 40000000);
  LCD.Display_Clear(0, 0, 319, 239);
  LCD.Brightness(200); //LCD LED Full brightness

  //WiFiを子機として起動
  WiFi.begin(SSID, PASSWORD);
  M5.Lcd.print("WiFi connecting");

  //接続をまつ
  while (WiFi.status() != WL_CONNECTED) {
    M5.Lcd.print(".");
    delay(100);
  }

  //取得したIPの表示(DHCP対応)
  IPAddress ipadr =WiFi.localIP();
  char ipad_st[20];
  sprintf(ipad_st,"IP:%d.%d.%d.%d",ipadr[0],ipadr[1],ipadr[2],ipadr[3]);  
  M5.Lcd.println(" connected");
  M5.Lcd.println(ipad_st);  

  //OBS-APIに接続トライ
  M5.Lcd.println("Connect to OBS....");
  wsclient.setReconnectInterval(5);
  wsclient.onEvent(onMessage);
  wsclient.begin(WebServerIP,WebServerPort, "/", "websocket");

  M5.Lcd.println("press ABC button");
  randomSeed(0);

  xTaskCreatePinnedToCore(
    Task1,  //関数ポインタ
    "Task1",  //タスク名
    65534, //スタック
    NULL, //引数
    5,  //優先度
    &th, //ハンドル
    1   //コア番号
    );
    
}

//================================================== 
//WSメッセージループ
//================================================== 
void Task1(void *pvParameters) {

  //これがないとWSイベントが起きない
  while(1)
  {
    wsclient.loop();
    M5.update();
    delay(1); //most important
  }
}

//================================================== 
//OBS用のメッセージID生成
//================================================== 
void NewMessageID(char *result,int length = 16)
{
    const char pool []= "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    for (int i = 0; i < length; i++)
    {
        int index = random(0,(int)sizeof(pool)-1);
        result[i] = pool[index];
    }

}

//================================================== 
//メッセージループ
//================================================== 
void loop() {
    
  char wsSend[200];
  char result[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
  
  //Aボタン応答
  btn_stateA = BTN.Button(0, buttonA_GPIO, true, 10, 300);
  switch( btn_stateA ){
    case _MomentPress:
      Serial.println("-------------Button A Moment Press");
      
      NewMessageID(result);
      Serial.println(result);

      sprintf(wsSend,"{\"request-type\":\"SetCurrentScene\",\"message-id\":\"%s\",\"scene-name\":\"シーン1\"}",result);
      wsclient.sendTXT(wsSend);
      break;

      
    case _ContPress:
      Serial.println("-------------Button C Constant Press");
      M5.Lcd.println("connecting");
      wsclient.begin(WebServerIP,WebServerPort, "/", "websocket");
      break;
      
  }    
  //Bボタン応答
  btn_stateB = BTN.Button(1, buttonB_GPIO, true, 10, 300);
  switch( btn_stateB ){
    case _MomentPress:
      Serial.println("-------------Button B Moment Press");
      NewMessageID(result);
      sprintf(wsSend,"{\"request-type\":\"SetCurrentScene\",\"message-id\":\"%s\",\"scene-name\":\"シーン2\"}",result);
      wsclient.sendTXT(wsSend);
      break;
  }    
  
  //Cボタン応答
  btn_stateC= BTN.Button(2, buttonC_GPIO, true, 10, 300);
  switch( btn_stateC ){
    case _MomentPress:
      Serial.println("-------------Button C Moment Press");
      NewMessageID(result);
      sprintf(wsSend,"{\"request-type\":\"SetCurrentScene\",\"message-id\":\"%s\",\"scene-name\":\"シーン3\"}",result);
      wsclient.sendTXT(wsSend);
      break;
      
    case _ContPress:
      Serial.println("-------------Button C Constant Press");
      M5.Lcd.println("disconnecting");
      wsclient.disconnect();
      break;
  }    
} 


開発したり研究したりするのに時間と費用がとてもかかるので、頂いたお気持ちはその費用に補填させていただきます。