見出し画像

M5Stack を字幕受信機にしてみる

ちょっと興味本位で買ってみたプロトタイプマイコンを、使ってみる練習かねて記事にしてみました。

といっても、先駆者がたくさんいるので後追いですが。

M5Stackっていうのは何者?

スイッチサイエンスという会社が売り出しているハードの事です。中身にはESP32っていうArduinoマイコンが使われています。

この製品のよいところは、基本的なハードが搭載されているところです。なので、ソフトさえ書いてしまえばとりあえず動かせる。ちょっとした用途だったら、たぶん業務なんかで使うのにも耐えられるでしょう。

IoTの世界だと、プロトタイプ系マイコンってのはPICとか、Raspberry Pie とか、IchigoJam とかが有名ですね。

今回は、

・電池が使える
・液晶画面が比較的簡単につかえる
・日本語が表示できる
・WiFiが使える

という希望が叶う、M5Stack をつかいました。

M5Stackはいくつか種類がある

基本的構成は、画面がついてる本体部分、電池ユニット、キーボードユニット、センサユニットに分かれる。積み重ねる(=STACK)させられるというところから製品名がついているらしい。

開発の段階では、なんにでも応用がきくM5Stack Facesがいいと思うけど、ちょっとお値段するので(11,000円ぐらい)、これが嫌なら、Basicのほうでもいいだろう。

※本体部分はBasicGrayがあって、9軸センサーがついている・ついてないがあるので、これだけは買うときに考えたほうがいい。LEGOと一緒に遊ぶなら、M5Goという商品もあるらしい。(IoT学習向けらしい)

まずは環境を作りましょう

先駆者がたくさんいるので、ライブラリは下記参照。結構いろいろいれておかないといけないので、トライ&エラーを繰り返してみてください。

IDEはM5Stack用のArduinoIDEをおとしてみましょ。USBシリアル通信でダウンロードするのが基本形だから、COMドライバも忘れずに。

開発環境系のライブラリなどは、ここを参照してみてください。

環境ができたら、ソフトをかいてみます。文中にかくととっても長いので、一番最後に載せておきますね。それを参考にかいてみてください。

今回の受信のさせ方は

UDトーク®で認識させた文章を、まあちゃんAPIを通して受信させる方法をためしてみたいとおもいます。

受信端末は、M5Stack で、通信はWiFi経由、プロトコルはWebSocketでやってみましょう。

まあちゃんAPI自体はWebSocket Serverなので、端末側もWebSocket通信方式のクライアントであれば、文字を受け取ることができます。

完成イメージはこんな感じ。

LCDに東雲フォントをつかって出してます。本当は半角処理をもう少ししっかりやらないと文字が割れてしまう(ビットマップ境界を考えずに中途半端におくってしまっている)ので、実用に耐えるにはもう少し作りこみがいりますね。

ハードをもっているかたは、まあちゃんを起動して通信→ツール用API通信をONにしてみてください。

M5Stackを起動すると、自動的にWiFiにつなぎ、まあちゃんを捜索しにいきます。

この状態で、まあちゃん側から文字をおくると、画面にでてきます。

ここで、左ボタン(Aボタン)を少し長めに押してみましょう。すると、字幕コントローラモードに移行します。


そうすると、字幕コントローラに読み込まれている字幕を送ることができます。左(A)ボタン単押しで戻る、真ん中(B)ボタン単押しで送信+次へ、右(C)ボタン単押しで、ブランク(画面消し)ができます。

この機能で文字を送ってUDライブシステムで見るなんていうのもありです。地元で行う演劇とか、映画とかのときに、ちょっと離れたところから操作するのに使えってみることもできるかもしれませんね。

まとめ

割と簡単な実装で、パソコンと連携させることができるようになりました。今回のサンプルソースでは、

JSON文字列のデコード、ライブラリの使い方
・WebSocket 送受信のやり方
・日本語文字列の出し方、キャスト
・子機としてWiFiにつながる
・並走処理の使い方

がわかるようなサンプル(実例)になっているので、もし使うのに困っている人がいたら、参考になればいいなぁ、とおもいます。

(だいぶ先駆者の皆様に助けられたので、形になったものはオープンにして次のだれかにつなげられたらいいな、とおもいます)

感想

結局のところ、Arduinoは中でC++が動いているので、スケッチ(~.ino)といいつつ、コンパイル時は ちゃんと拡張子がcppになっているのでした。

WebSocketライブラリあたりは、Hppとか扱っているから、中で処理しているコンパイラはBorlandあたりの流れを汲んでいるクロスコンパイラなんだろうか。

実際のところ、どんなライブラリを入れても、Intellisenseみたいな機能がないから、ソースをよんだりサンプルをひらくことにはなる。ライブラリフォルダの中のソースは cppとhで構成されていることが多いから、これを読み込めば、なんとなくどう使えばいいかわかる。

まぁ、組み込み用の開発環境だから、こんな感じなのかもしれないけど、もっとリッチな開発環境がいいなら、VisualStudioでも動くものがあるらしい

今後は、こういう環境を使うのもいいかもしれない

参考リンク先

・http://shuzo-kino.hateblo.jp/entry/2016/05/06/203603
・https://www.mgo-tec.com/blog-entry-m5stack-font-scrolle-esp32.html
・https://www.mgo-tec.com/arduino-core-esp32-install
・https://log.niccol.li/2012/05/arduinowebsocketsocketio.html
・https://www.mgo-tec.com/kanji-font-shinonome
・https://kerikeri.top/posts/2017-06-24-esp32-dual-core/

ソースはこちら

//==================================================
// まあちゃんAPI 連携操作例 on M5Stack
// 2018.8.17
//==================================================

//================================================== 
//プラットフォームライブラリ
//================================================== 
#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[] = "WiFI00000";       //WiFi SSID
const char PASSWORD[] = "pass";    //WiFi パスワード
const char WebServerIP[] = "192.168.0.2"; //まあちゃんのPCアドレス

//================================================== 
//ハード依存の定数
//================================================== 
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 isJimakuMode=0; //0=画面表示 1=字幕リモコン
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);
  
  if (!error) 
  {
    JsonObject root = jsonBuffer.as<JsonObject>();

    if(isJimakuMode)
    {
      if((root["Method"]=="CAPTION") && (root["Mode"]=="MONITOR") && (root["Target"]=="ALL") )
      {
        //表出していいよデータがきたとき        
        String message = root["MsgTxt_Next"];
        if(prev_message!=message)
        {
          M5.Lcd.fillScreen(BLUE);  
          printjp(0,0,message,0xff,0xff,0xff,2);
          Serial.println(message);
          prev_message=message;
        }
      }

    
    }else{
      if((root["Method"]=="INPUT") && (root["Mode"]=="DATA") && 
            ((root["Action"]=="FIN") || (root["Action"]=="DIRECT")))
      {
        //表出していいよデータがきたとき        
        String message = root["MsgTxt"];
        if(prev_message!=message)
        {
          M5.Lcd.fillScreen(BLACK);  
          printjp(0,0,message,0xff,0xff,0xff,2);
          Serial.println(message);
          prev_message=message;
        }
      }
    }
  }
}

//================================================== 
//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);  

  //まあちゃんAPIに接続トライ
  M5.Lcd.println("Connect to Machan....");
  wsclient.begin(WebServerIP,50000, "/", "websocket");
  wsclient.setReconnectInterval(5);
  wsclient.onEvent(onMessage);

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

}

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

  //これがないとWSイベントが起きない
  while(1)
  {
    wsclient.loop();
    M5.update();
    delay(1); //most important
  }
}
//================================================== 
//メッセージループ
//================================================== 
void loop() {
    
  //Aボタン応答
  btn_stateA = BTN.Button(0, buttonA_GPIO, true, 10, 500);
  switch( btn_stateA ){
    case _MomentPress:
      Serial.println("-------------Button A Moment Press");
      if(isJimakuMode)
      {
        wsclient.sendTXT("{\"Method\":\"CAPTION\",\"Mode\":\"BACK\",\"Target\":\"ALL\"}");
      }
        break;

    case _ContPress:
      Serial.println("-------------Button A Cont Press");
      isJimakuMode = (isJimakuMode+1)%2;
      if(isJimakuMode){M5.Lcd.fillScreen(BLUE);}else{M5.Lcd.fillScreen(BLACK);}
      break;
    default:
      break;
  }    
  //Bボタン応答
  btn_stateB = BTN.Button(1, buttonB_GPIO, true, 10, 300);
  switch( btn_stateB ){
    case _MomentPress:
      Serial.println("-------------Button B Moment Press");
      if(isJimakuMode)
      {
        wsclient.sendTXT("{\"Method\":\"CAPTION\",\"Mode\":\"SEND\",\"Target\":\"ALL\"}");
      }
      break;
      
    case _ContPress:
      Serial.println("-------------Button B Cont Press");
      break;
    default:
      break;
  }    
  
  //Cボタン応答
  btn_stateC= BTN.Button(2, buttonC_GPIO, true, 10, 300);
  switch( btn_stateC ){
    case _MomentPress:
      Serial.println("-------------Button C Moment Press");
      if(isJimakuMode)
      {
        wsclient.sendTXT("{\"Method\":\"CAPTION\",\"Mode\":\"BLANK\",\"Target\":\"ALL\"}");
      }
      break;
      
    case _ContPress:
      Serial.println("-------------Button C Cont Press");
      break;
    default:
      break;
  }    
  
}



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