Google Home と ESP32 で、音声をテキスト表示させた WiFi 電光掲示板を作ってみた

ESP32 ( ESP-WROOM-32 )

Arduino IDE スケッチの入力

では、ようやく、ESP32 の Arduino IDE スケッチの入力です。
ESP32 では、Firebase からのデータ取得だけで、あとは普通の OLEDディスプレイ表示だけなので、一般的な HTTP GET リクエストプログラムとそれほど変わらず、比較的とっつきやすいと思います。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include <WiFiClientSecure.h>
#include <WiFiMulti.h>
#include "ESP32_I2C_SSD1306.h" //mgo-tec library beta ver 1.10
#include "ESP32_SPIFFS_ShinonomeFNT.h" //mgo-tec library beta ver 1.32
#include "ESP32_SPIFFS_UTF8toSJIS.h" //mgo-tec library beta ver 1.2

const char *ssid = "xxxxx"; //ご自分のルーターのSSIDに書き換えてください。
const char *password = "xxxxx"; //ご自分のルーターのパスワードに書き換えてください。
const int httpsPort = 443;

//-----------------------------------------------------------------
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/shnm8x16.bdf"; //半角フォントファイル名を定義

const uint8_t ADDRES_OLED =  0x3C;
const int sda = 5;
const int scl = 4;
const uint8_t Horizontal_pixel = 128;
const uint8_t Vertical_pixel = 64;

WiFiMulti wifiMulti;

ESP32_I2C_SSD1306 ssd1306(ADDRES_OLED, sda, scl, Horizontal_pixel, Vertical_pixel);
ESP32_SPIFFS_ShinonomeFNT _SFR;

enum { MaxTxtByte = 300, Disp_txt = 16};

String utf8_str; //Firebase から送られてきた文字列を格納
uint8_t sj_txt[MaxTxtByte] = {0}; //Shift_JISコード格納
uint16_t sj_length = 0; //Shift_JISコードの長さ
uint16_t sj_cnt = 0; //Shift_JISコード半角文字数カウント

uint8_t font_buf[2][16] = {0}; //スクロール時に投入するフォント1文字のバッファ
uint8_t disp_buf[Disp_txt][16] = {0}; //実際にディスプレイに表示するフォントバッファ

uint32_t SclTime1 = 0;
uint8_t Zen_or_Han = 0; //全角か半角かを判断する変数

uint32_t WebGetLastTime;
boolean scl_reset = false;
boolean FirstWebGet = true;

uint8_t H_size = 2, V_size = 4;

//**********セットアップ関数************************
void setup(){
  Serial.begin(115200);
  delay(10);

  Serial.println();
  Serial.print(F("Connecting to "));
  Serial.println(ssid);

  wifiMulti.addAP(ssid, password);

  Serial.println(F("Connecting Wifi..."));
  if(wifiMulti.run() == WL_CONNECTED) {
      Serial.println("");
      Serial.println(F("WiFi connected"));
      Serial.println(F("IP address: "));
      Serial.println(WiFi.localIP());
  }
  delay(1000);

  ssd1306.SSD1306_Init(400000); //I2C Max=400kHz
  ssd1306.Display_Clear_All();
  _SFR.SPIFFS_Shinonome_Init3F(UTF8SJIS_file, Shino_Half_Font_file, Shino_Zen_Font_file); //ライブラリ初期化。2ファイル同時に開く

  TaskHandle_t th; //ESP32 マルチタスク ハンドル定義
  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 5, &th, 0); //マルチタスク core 0 実行

  WebGetLastTime = millis();
  SclTime1 = millis();
}
//************* メインループ ***********************************
void loop() {
  if(FirstWebGet || ((millis() - WebGetLastTime) > 30000)){ //Tweet get every 3 minutes.
    Firebase_Data_GET();

    sj_length = _SFR.UTF8toSJIS_convert(utf8_str, sj_txt);
    Serial.printf("length = %d\r\n", sj_length);

    sj_cnt = 0;
    Zen_or_Han = _SFR.Sjis_inc_FntRead_Rot(&sj_cnt, -90, 0, 0, sj_txt, sj_length, font_buf);

    scl_reset = true;
    FirstWebGet = false;
    WebGetLastTime = millis();
  }
}
//************* マルチタスク ****************************************
void Task1(void *pvParameters){
  while(1){
    if(millis() - SclTime1 > 0){
      if(ssd1306.Scroller_Font8x16_PageReplace(&scl_reset, Disp_txt/H_size, 0, Zen_or_Han, font_buf, disp_buf)){
        Zen_or_Han = _SFR.Sjis_inc_FntRead_Rot(&sj_cnt, -90, 0, 0, sj_txt, sj_length, font_buf);
      }
      ssd1306.SizeUp_8x16_Font_DisplayOut(H_size, V_size, Disp_txt/H_size, 0, 0, disp_buf);
      SclTime1 = millis();
    }
    delay(1);//マルチタスクのwhileループでは必ず必要
  }
}
//*************** HTTP GET **********************************************
void Firebase_Data_GET(){
  WiFiClientSecure client;
  //client.setCACert(firebase_ca); //これは任意

  const char *host = "xxx-xxx-xxx-xxx-xxx-xxx.cloudfunctions.net"; //ご自分の Deploy した Cloud Functions ホスト
  const char *UUID = "xxxxxxxxxxxxxxxxx"; //ご自分で設定した UID 

  if (client.connect(host, 443)) {
    Serial.print(host); Serial.print(F("-------------"));
    Serial.println(F("connected"));
    Serial.println(F("--------------------Realtime Database HTTP GET Request Send"));

    String send_head[4];
    String send_str_query;

    send_head[0] = "GET /FirebaseGetRequest?UUID=" + String(UUID);

    send_str_query = "&Request=message1";

    send_head[1] = " HTTP/1.1\r\n";
    send_head[2] = "Host: " + String(host) + "\r\n";
    send_head[3] = "Connection:close\r\n\r\n";

    client.print( send_head[0] );
    Serial.print( send_head[0] );
    client.print( send_str_query );
    Serial.print( send_str_query );

    client.print( send_head[1] );
    client.print( send_head[2] );
    client.print( send_head[3] );

    Serial.print( send_head[1] );
    Serial.print( send_head[2] );
    Serial.print( send_head[3] ); 
    Serial.println();
    Serial.println(F("--------------------Realtime Database HTTP Response"));

    String ret_str;

    while(client.connected()){
      while (client.available()) {
        ret_str = client.readStringUntil('\n');
        Serial.println(ret_str);
        if( ret_str == "\r" ){
          while (client.available()) {
            ret_str = client.readStringUntil('\n');
            Serial.println(ret_str);
            if( ret_str.length() > 1 ){
              utf8_str = ret_str;
              utf8_str.replace("\"", "");
              utf8_str = "★" + utf8_str + " ";
            }
            delay(1);
          }
          delay(10);
          client.stop();
          delay(10);
          Serial.println(F("--------------------client Stop"));
          break;
        }
        delay(1);
      }
    }
  }else {
    // if you didn't get a connection to the server2:
    Serial.println(F("connection failed"));
  }
}

【解説】

●1行目:
今回は、Firebase からデータを取得するので、SSLサイトとのコネクションが必要です。
したがって、WiFiClientSecure をインクルードします。

●3-5行:
私の自作ライブラリのインクルードです。
最新バージョンを使って下さい。

●7-8行:
ご自分のルーター環境の SSID と パスワードに書き換えてください。

●12-14行:
SPIFFSフラッシュにアップロードしたフォントファイル名を定義しています。
ここで気を付けていただきたいのは、ルートに保存したか、/font/フォルダに保存したかを間違えないことです。
私は前回の記事で/font/フォルダにアップロードしたのを忘れていて、ここの行でルートを指定してしまって、うまく表示されず、何度もハマりました。

●27行:
Firebase から送られてきた文字列を Shift_JIS 変換した場合の最大バイト数を 300 としています。
これはメモリの許す限り自由に変えてください。
Disp_txt はOLED SSD1306 では 半角 16文字以内しか表示できないという数値です。

●44行:
ESP32_I2C_SSD1306 ライブラリの beta ver 1.10 から可能になった、縦と横で異なる文字サイズを設定できます。
サイズは、1, 2 ,4 のみの数値にできます。
3は不可です。

●55-63行:
今回は、WiFiMulti ライブラリでコネクションしてみました。

●68行:
SPIFFS のファイルを3つ同時に開く関数です。

●70-71行:
ESP32 をマルチタスクで動かす定義です。
93-104行と連動しています。
マルチタスクの使い方については、以下の記事を参照してください。

Arduino – ESP32 のマルチタスク ( Dual Core ) を試す

●77-91行:
メインループです。
マルチタスクでは CPU コア1番で動作しています。
ここでは、Firebase に HTTPリクエストを送って、文字列を取得するループです。
30秒毎にGET しているので、ESP32 がフル稼働してしまいます。
通信トラフィックを減らしたい場合、78行の数値を変えてみてください。

79行で Web上の Firebase サイトから、データベース文字列を GET します。
106-174行で関数化しています。

81行で String型 UTF-8コード文字列を Shift_JIS コード文字列に変換しています。
85行で Shift_JISコード文字列の最初の1文字を 16×16 pixel の東雲フォントに変換しています。
この時、スクロールを最初の文字からにリセットするために、87行の scl_reset を true にして、96行の関数に投げています。

●93-104行;
マルチタスクの CPUコア0番のループです。
有機EL ( OLED )ディスプレイの文字スクロールをするタスクです。
マルチタスクにすることによって、Web GET 中でもスクロールが止まらないという利点があります。
95行を見ても分かる通り、OLED SSD1306 ではこれが最速です。
スクロールを遅くするしかできませんが、そうしたい場合、95行の数値をゼロ以上にしてください。

96行の Scroller_Font8x16_PageReplace関数では、ESP32_I2C_SSD1306 ライブラリ ver 1.10 から追加の、スクロールリセット引数が入っています。
scl_reset = true; とすることによって、スクロールを最初の文字からにリセットします。
その後すぐに、scl_reset = false; にして返します。

99行の SizeUp_8x16_Font_DisplayOut関数では、ver1.10 から新たに縦横別々に文字サイズを変えられるようになっています。

また、SSD1306 は Page という単位で区切られているので、Page 単位でスクロールした方が処理が速いです。
これについては以下の記事も合わせて参照してください。

有機EL ( OLED ) SSD1306 を再検証してみました ( I2C 通信用 )

OLED SSD1306 で点、線、四角形、円を描く ( ESP32 , ESP8266 , Wire ライブラリのみ使用 )

ESP32 で I2C OLED SSD1306 に東雲フォントを4倍角で表示させてみた

●106-174行;
Web上の Firebase サイトに HTTP GET リクエストを送信して、文字列を取得する関数です。
Firebase は SSLサイトなので、WiFiClientSecure ライブラリで client を定義します。
108行目の Firebase ルート証明書セットは、やらなくても通信できますが、セキュリティを高めたい場合は任意に行ってください。
ルート証明書の取得方法は以下の記事を参照してください。

Arduino – ESP32 WiFiClientSecure ライブラリで、安定して https ( SSL )記事をGETする方法

110行目では、先ほど Google Cloud Shell のコマンドラインで Deploy した、Cloud Functions の host URL に書き換えてください。
111行では、登録した UID に書き換えてください。

113行で Google Cloud Functions の HOST にコネクションします。
HTTP リクエストのヘッダ情報は、118-127行で定義しています。
121行でUID 情報をセットし、123行でデータベースの項目名を message1 と設定しています。
これは、先ほど述べた Firebase ページのデータベース情報とリンクしています。

実際に HTTP リクエストを送信しているのは、129-136行です。

146-169行で、Web上の Firebase からのHTTPレスポンスを受信します。
HTTPレスポンスはヘッダ情報の後、空行が1行送られてくるので、それは、150行目のキャリッジリターンで判断します。
148行目の文字列抽出ではラインフィード”¥n” がカットされるので、空行ではキャリッジリターンのみとなります。

空行の後、Firebase のデータが文字列で送られてくるので、155-157行で、ダブルクォーテーションをカットして、「★」マークやスペースを付け足しています。

Firebaseサーバーから送られてくるデータが無くなったら、162行目にあるようにコネクションを切断します。

コンパイル書き込み実行

では、スケッチをコンパイル書き込んで実行してみてください。
最新版(2018/01/17時点)のArduino core for ESP32 では、コンパイル書き込みは一発でOKになったようです。
ほんの1週間前は連続2回書き込みしないと成功しなかったのですが、恐らく ESP-IDF の Tool Chain がアップデートされたことによるものと思われます。
まぁ、とにかく良くなったのでヨシとします。

115200 bps で起動したシリアルモニターはこんな感じになります。

30秒毎に GET しているので、ちょっと忙し過ぎるかもしれません。
この間隔は通信トラフィックが許す限りにおいて、好きなように調節してみてください。
いずれにしても、短すぎるのは良くないということは言うまでもありません。

あとは、最初の方で紹介した動画のように動作すればOKです。

終わりに

いかがでしたでしょうか。
うまく動きましたでしょうか?

これで、声からテキスト変換して、遠くにいる人に分かりやすい形で伝達する手段ができました。
既にある技術かもしれませんが、これが個人レベルの電子工作でできるようになったことに大きな意味があります。
既存のメーカー製品では、かゆいところに手が届かなかったことが、個人の力で出来るようになったということです。
改めて、Firebase などの昨今の開発ツールの凄さを思い知ったという感じです。

でも、消費電力や、通信トラフィックなど、まだまだ課題はあります。
それに、Firebase の Authorization 認証についても、まだ勉強中です。
今後はセキュリティも高めていきたいところです。

ということで、今回はここまでです。

ではまた・・・。

コメント

  1. juchang より:

    mgo-tec 様

    大変お世話になっております。
    「センサー値を Google Home に喋らせる実験」を何とかクリアさせて頂き、早速、「音声をテキスト表示させた WiFi 電光掲示板を作ってみた」に挑戦をしております。
    今回テキストには、index.js のみ記載されていますが、package.json はどのように入力したらよろしいでしょうか。
    又、Firebase Realtime Detabase の設定について、もう少し詳しくご説明いただけると幸いです。
    よろしくお願い致します。

  2. juchang より:

    mgo-tec 様

    またまた単純ミスを犯してしまいました。
    Dialogflow の Fulfiliment の URL 欄で、GoogleHome_Dialogflow_Action と入力するのを忘れていました。
    それを修正するとテキスト表示が出るようになりました。
    Database の表示はできていませんが、いろいろ調べながら解決していきたいと思います。
    毎度お騒がせしまして申し訳ありません。
    今後ともご指導の程よろしくお願い致します。

    • mgo-tec mgo-tec より:

      juchangさん

      これからは出来るだけご自分のコードを見直してからご質問していただくようお願いいたします。
      でも、動いて良かったです。

タイトルとURLをコピーしました