Firebase Realtime Database のデータ保存、取得、ストリーミング受信実験( ESP32 , M5Stack )

M5Stack

10.Firebase Realtime database への書き込みリクエスト

では、今度は Firebase Realtime database へ書き込む場合の HTTP リクエストを考えます。

これも Firebase Realtime database ドキュメントの REST によるデータ保存項目に書かれています。

https://firebase.google.com/docs/database/rest/save-data?hl=ja

これによると、サーバーのデータ書き換えコマンドのうち、以下が使用できるとのこと。

● PUT
● PATCH
● POST
● DELETE

PUT を使ってしまうと、データベース内の子ノードまで全て書き換えてしまう可能性があるので、ここでは PATCH を使います。

これらは、先ほど実験したようなブラウザの URL 入力欄では使えないコマンドですが、HTMLファイルや ESP32 等の Wi-Fiマイコンならばリクエストできます。

例として、PATCH の最低限の HTTP リクエストは以下のようになります。

PATCH /test_user1.json?auth=xxxxxxxxxxxxxxxx HTTP/1.1
Host: testproject01-439eb.firebaseio.com
Connection: keep-alive
Content-Length: 12
(空行)
{"text":"0"}

空行の後の {“text”:”0″} が body 部分で、これによって、text 内のデータが “0” に書き替えられます。
これは JSON形式というもので送ります。

これを PUTコマンドにしてしまうと、他のノードも消されて、text だけになってしまうので、やはりここでは PATCH が良いということになります。

重要なのは、Content-Length で、body部分の文字数を Firebase サーバーへしっかり伝えなければならないことです。

このリクエストを送って、Firebase の Autentication ( auth 認証 )が通れば、HTTP レスポンスが返ってきます。

HTTP/1.1 200 OK
Server: nginx
Date: Sat, 01 Sep 2018 04:50:11 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 12
Connection: keep-alive
Access-Control-Allow-Origin: *
Cache-Control: no-cache
Strict-Transport-Security: max-age=31556926; includeSubDomains; preload

{"text":"0"} 

Firebaseサーバーから返ってきたレスポンス文字列を全て一字も逃さず受信し切ることが重要です。
もし、取りこぼしてしまったら、それが溜まりに溜まって、連続書き込みが不安定になると思われます。

これさえ分かれば、ESP32 で Firebase へデータを書き込むことが可能です。

11.ESP32 および M5Stack で Firebase Realtime database に書き込む

では、前項を踏まえて、ESP32 および M5Stack を使って、Firebase の Realtime database へ書き込んでみたいと思います。

スケッチ(ソースコード)入力

Arduino IDE に以下のスケッチを入力してみてください。
Firebase Realtime database の test_user1 内の text フィールドデータ書き込みです。

コメント投稿でESP32さんから指摘があり、ルートCA証明書が無い場合、client.setInsecure();を入れないと接続できないことが分かり、修正しました。
を追加しました。
ESP32さん、ありがとうございました。

また、Arduino core ESP32 2.0.2 では、WiFiClientSecure のclientインスタンスをstopした後、解放しないと、再接続できないことが分かり、コードを修正しました。
(2022/03/20)

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

//Arduino core for the ESP32 ver 2.0.2
#include <WiFi.h>
#include <WiFiClientSecure.h>

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

const char* host = "xxxxxx-xxxx.firebaseio.com";
const char* firebase_auth = "xxxxxxxxxxxxxxxxxxxxxxxxx"; //database secrets

String user_path = "test_user1";
int count = 0;

//*****セットアップ******************
void setup() {
  Serial.begin(115200);
  Serial.println();
  delay(1000);

  WiFi.begin( ssid, password );

  Serial.print( "connecting" );
  while ( WiFi.status() != WL_CONNECTED ) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.print( "connected: " );
  Serial.println( WiFi.localIP() );

  delay(2000);
}
//****メインループ********************
void loop() {
  sendPatchRequest( "text", String( count ) );
  count++;
  if( count > 10 ) count = 0;
  delay(3000);
}
//******Firebase Realtime Database 書き替え**************
void sendPatchRequest( String str1, String str2 ){
  WiFiClientSecure client; //再接続する場合、ここでインスタンス作成して、stopしたら解放する必要あり
  client.setInsecure(); //ルートCAの検証を行わない
  if( !client.connect( host, 443 ) ){
    Serial.println( "Connection failed!" );
  }else{
    Serial.println( "Connected to server!" );
    String req_url_str;
    req_url_str = "PATCH /";
    req_url_str += user_path;
    req_url_str += ".json?auth=";
    req_url_str += String( firebase_auth );
    req_url_str += " HTTP/1.1\r\n";

    String body = "{\"" + str1 + "\":\"" + str2 + "\"}";

    String head;
    head = "Host: ";
    head += String( host ) + "\r\n";
    //head += "Accept: text/event-stream\r\n";
    head += "Connection: close\r\n";
    //head += "Connection: keep-alive\r\n";
    head += "Content-Length: ";
    head += String( body.length() ) + "\r\n";
    head += "\r\n"; //空行

    client.print( req_url_str );
    client.print( head );
    client.print( body );

    Serial.println( "####### Send HTTP Patch Request #######" );
    Serial.print( req_url_str );
    Serial.print( head );
    Serial.println( body );

    checkServerRespons(client);

    client.stop(); //これ絶対必要
  }
}
//***********************************
void checkServerRespons(WiFiClientSecure &client){
  //Firebaseサーバーからのレスポンスを全て受信し切ることが重要
  Serial.println("####### Firebase server HTTP Response #######");
  while( client.connected() ){
    String resp_str = client.readStringUntil('\n'); //ラインフィードまで文字列を読み込む
    Serial.println( resp_str );
    if( resp_str == "\r" ) { //空行検知
      Serial.println("------------- Response Headers Received");
      break;
    }
  }
  //レスポンスヘッダが返ってきた後、body部分が返って来る
  while( client.available() ){
    char c = client.read();
    Serial.print( c );
  }
  Serial.println("\r\n----------- Body Received");
}

【ザッと解説】

5-6行目で、ご自分の Wi-FiルーターのSSID とパスワードに書き換えてください。

8-11行目はご自分の Firebaseプロジェクトに合わせて書き換えてください。
8行目の host は下図の様に Firebase console 画面を開き、Realtime database 画面のアドレスをコピペしてください。

auth は先ほど述べた Database secrets ( データベースシークレット )です。

67-69行目で Firebase サーバーへ PATCH リクエストを送った後、76行目の関数でFirebaseサーバーからのレスポンスを待ちます。
重要なのは、一文字も漏らさずに文字列を受け取らねばならないことです。

86行目の readStringUntil( ‘\n’ ) では、’\n’ というラインフィード制御文字まで検索して、resp_strに格納されます。
ですが、’\n’ は捨てられます。
ですから、空行の場合は必然と ‘\r’ だけ格納されますので、88行のように空行を検出できます。
空行を検出したら、後は body 部分が返って来るので、それを漏らさず受信します。

このように、サーバーからのレスポンス文字列を一文字も漏らさず受信することが、安定してサーバーと通信するためには重要だと個人的に思っています。

そして、Firebase からのレスポンス文字列を全て受信し切ったら、78行目にあるようにclient.stop();を入れなければなりません。
そうしないと、なぜかデータの連続書き込みができませんでした。

以上までが終われば、新たに PATCH リクエストを送信できます。
これが終わる前に、次の PATCH リクエストを送ってしまうと、サーバーとの通信が不安定になると思われます。

【(2022/03/19追記】
Firebase Realtime DatabaseへのPATCHの連続リクエストは、client.stop後にclientインスタンスを開放しないと、再接続できなくなっていました。
よって、コードを修正しました。

コンパイル書き込み実行

では、Arduino IDE でコンパイル書き込み実行してみてください。

シリアルモニタを 115200bps で起動してみると、下図の様に表示されると思います。

Firebaseサーバーに連続書き込みしているので、Firebase console の Realtime database 画面を見てみると、下図の様に常に黄色の枠で囲われていると思います。
これは、データ書き込みが正常完了しているという証拠です。

このプログラムでは見てお分かりの通り、Firebaseサーバーからのレスポンス受信が終わってから次の PATCH 書き込みリクエストを送っています。
私の環境の場合、1.5秒ほどのサイクルで次のデータ書き込みしていました。
SSL通信で暗号化しているので、HTTP 通信ではこれが限界だと思います。

動画ではこんな感じになります。

「動画」

でも、高速連続通信を望まないのであれば、これで必要充分だと思います。
特に、センサデータを書き込む場合は、最速で3秒もあれば十分ではないかと思います。

ラジコンのように高速リアルタイム性を求めるのであれば、Blynk とか WebSocket を使えば良いと思います。

えらい記事が長くなったので、スマホ側のプログラミングは次回の記事にしたいと思います。

12.データの書き込みとストリーム受信の両方を行う

(2022/03/19追記)
コメント投稿でESP32さんからの質問を受け、久々にESP32とFirebaseを使ってみたら、動かなくなっていました。
Arduino core for the ESP32 のバージョン2.0.2 の場合、WiFiClientSecureライブラリがかなり修正されていたようです。
よって、この記事上のコードを修正したついでに、コメント投稿のESP32さんからの要望もあり、Realtime DatabaseのPATCHリクエスト送信とEventストリーム受信の両方を行うサンプルスケッチを作ってみました。
3秒毎にPATCHリクエストしてRealtime Databaseを書き換え、その後Eventストリームで受信するというものです。

ポイントは、WiFiClientSecureのインスタンスを、Eventストリーム受信用と、PATCHリクエスト送信用の2つを作ることです。
ただ、PATCHリクエスト用の場合は、先に紹介したコード同様に、client.stopしたらclientインスタンスを開放しないと再接続しないので、関数内でインスタンス生成することです。
こんな感じです。

//Arduino core for the ESP32 ver 2.0.2
#include <WiFi.h>
#include <WiFiClientSecure.h>

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

const char* host = "xxxxxx-xxxx.firebaseio.com";
const char* firebase_auth = "xxxxxxxxxxxxxxxxxxxxxxxxx"; //database secrets

String user_path = "test_user1";
int count = 0;
uint32_t interval_time = 3000;
uint32_t last_time = 0;
WiFiClientSecure client_rcv;

//*****セットアップ******************
void setup() {
  Serial.begin(115200);
  Serial.println();
  delay(1000);
  
  WiFi.begin( ssid, password );

  Serial.print( "connecting" );
  while ( WiFi.status() != WL_CONNECTED ) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.print( "connected: " );
  Serial.println( WiFi.localIP() );

  delay(2000);
  interval_time = millis();

  getServerSentEvents(client_rcv);
}
//****メインループ********************
void loop() {
  if(millis() - last_time > interval_time){
    sendPatchRequest( "text", String( count ) );
    count++;
    if( count > 10 ) count = 0;
    last_time = millis();
  }
  checkEventStreamRespons(client_rcv);
}
//******Firebase Realtime Database 書き替え**************
void sendPatchRequest( String str1, String str2 ){
  WiFiClientSecure client; //再接続する場合、ここでインスタンス作成して、stopしたら解放する必要あり
  client.setInsecure(); //ルートCAの検証を行わない
  if( !client.connect( host, 443 ) ){
    Serial.println( "Connection failed!" );
  }else{
    Serial.println( "Connected to server!" );
    String req_url_str;
    req_url_str = "PATCH /";
    req_url_str += user_path;
    req_url_str += ".json?auth=";
    req_url_str += String( firebase_auth );
    req_url_str += " HTTP/1.1\r\n";

    String body = "{\"" + str1 + "\":\"" + str2 + "\"}";

    String head;
    head = "Host: ";
    head += String( host ) + "\r\n";
    //head += "Accept: text/event-stream\r\n";
    head += "Connection: close\r\n";
    //head += "Connection: keep-alive\r\n";
    head += "Content-Length: ";
    head += String( body.length() ) + "\r\n";
    head += "\r\n"; //空行

    client.print( req_url_str );
    client.print( head );
    client.print( body );

    Serial.println( "####### Send HTTP Patch Request #######" );
    Serial.print( req_url_str );
    Serial.print( head );
    Serial.println( body );

    checkPatchRequestRespons(client);

    client.stop(); //これ絶対必要
  }
}
//***********************************
void getServerSentEvents(WiFiClientSecure &client){
  Serial.println( "\nStarting connection to server..." );
  client.setInsecure(); //ルートCAの検証を行わない
  if( !client.connect( host, 443 ) ){
    Serial.println( "Connection failed!" );
  }else{
    Serial.println( "Connected to server!" );
    String req_url_str;
    req_url_str = "GET /";
    req_url_str += user_path + ".json?auth=";
    req_url_str += String( firebase_auth ) + " HTTP/1.1\r\n";
 
    String req_header_str;
    req_header_str = "Host: ";
    req_header_str += String( host ) + "\r\n";
    req_header_str += "Accept: text/event-stream\r\n";
    req_header_str += "Connection: close\r\n";
    req_header_str += "\r\n"; //空行
 
    Serial.println( "Send Server-Sent Events GET request." );
    //FirebaseサーバーへGETリクエスト送信
    client.print( req_url_str );
    client.print( req_header_str );
 
    Serial.print( req_url_str );
    Serial.print( req_header_str );
  }
}
//***********************************
void checkPatchRequestRespons(WiFiClientSecure &client){
  //Firebaseサーバーからのレスポンスを全て受信し切ることが重要
  Serial.println("####### Firebase server HTTP Response #######");
  while( client.connected() ){
    String resp_str = client.readStringUntil('\n'); //ラインフィードまで文字列を読み込む
    Serial.println( resp_str );
    if( resp_str == "\r" ) { //空行検知
      Serial.println("------------- PATCH Request, Response Headers Received");
      break;
    }
  }
  //レスポンスヘッダが返ってきた後、body部分が返って来る
  while( client.available() ){
    char c = client.read();
    Serial.print( c );
  }
  Serial.println("\r\n----------- PATCH Request, Body Received");
  Serial.println("###############################");
}
//***********************************
void checkEventStreamRespons(WiFiClientSecure &client){
  while( client.available() ){
    char c = client.read();
    if( c == '\r' ) Serial.print( "\\r"); //キャリッジリターン
    if( c == '\n' ) Serial.print( "\\n"); //ラインフィード
    Serial.print( c );
  }
}

編集後記

いかがでしょうか。

今回は、Firebase Realtime database との HTTP 通信のみによるデータの読み込み、および書き込みの方法を紹介してみました。

これさえできれば、アマチュアプログラマや電子工作家には十分すぎるくらい色々な IoT 機器開発ができそうですね。
しかもインターネットさえつながる環境があれば、地球の裏側でも通信可能です。

Firebase はあまりにも素晴らしいクラウドサービスです。
これからは Firebase の時代だと個人的に思っています。
ヤバイ電子工作や IoT がどんどんできそうですね。

今回は、Wi-Fi マイコンモジュールの ESP32 および M5Stack 側のプログラミングでしたが、次回はスマホ側の HTML および JavaScript プログラミングも紹介したいと思います。
それで、スマホと双方向通信が可能になります。
アプリを開発しなくても、ブラウザがあれば通信可能です。
アマチュアにとっては画期的なことだと思います。
なんかワクワクしますね。

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

コメント

  1. Yosuke より:

    突然のコメントにて失礼します。
    Firebase Realtime Databaseの内容でもないので申し訳ないのですが、
    ESP32でキーマトリクス回路を作りたいと思い、
    ESP32DEVkitCに74HC138のデマルチプレクサをつないで
    作ったのですが、ちとうまくいかないところがありまして、
    もしESP32でデマルチプレクサかマルチプレクサを使われた経験が
    ございましたら、伺いたいことがございます。
    いかがでしょうか。
    ※Arduinoではうまくいったキーマトリクス回路を
    そのままESP32に流用しようとしています。

  2. Yosuke より:

    mgo-tecさん

    ご返信いただきまして誠ありがとうございます。
    リンクして頂いたページを中心に、
    SPI通信を勉強してみようと思います。

    すみませんが、もう1点伺いたいことがございます。
    Arduino(ATmega328p)に74HC138をつなげば、
    キーマトリクス回路を正常に組めています。
    そこで、ESP32と74HC138の間にATmega328pを噛ませて、
    ESP32 – ATmega328p – 74HC138 – ボタン達のようにつなぎ、
    ESP32とATmega328pで何らかの通信をする方法を
    素人考えで考えてみたのですが、
    SPI通信などを利用して、ATmega328pからESP32に値を送る
    ようなことは可能でしょうか?

    お忙しいところ恐れ入りますが、何かご存知でしたら、
    ご返信いただければ助かります。

    • mgo-tec mgo-tec より:

      Yosukeさん

      多分、ESP32 と Arduino で SPI データ通信は可能だと思いますが、そのデバイス間ではシリアル通信しかやったことがありませんので、アドバイスできません。
      私が個人的に思うには、Arduino を噛ませないでも、ESP32 だけで十分可能ではないかと思います。
      ESP32 のクロックの方が高速ですし・・・。
      ということで、あまりお役に立てず、申し訳ございません。

  3. Yosuke より:

    mgo-tecさん

    ご返信頂きましてありがとうございます。
    EPS32にマルチプレクサやデマルチプレクサをつなぎますと、
    押していないボタンが押されたような反応になってしまう等、
    笑ってしまうほど意図しない挙動になってしまい、
    Arduinoで出来ていたことがこんなにできないのかと困っておりました。
    ただ、やはり安価にBluetooth通信が出来るの機能は捨てがたいので、
    アドバイス頂いた通り、EPS32のみでもう少し試行錯誤を
    続けてみようと思います。

    EPS32ユーザーの方とお話できただけでも嬉しかったです。
    本当にありがとうございました。

  4. さっさ より:

    質問です!!
    M5Stackを使っています。
    Wi-Fi、サーバーとの接続共に良好ですが、「400 Bad request」が返ってきてしまいます。
    これってfirebaseのデータベースのシークレットが既に廃止されてるからなんでしょうか?
    Firebase Admin SDKを使用しないといけませんか?

    • mgo-tec mgo-tec より:

      さっさ さん
      記事をご覧いただき、ありがとうございます。

      今、直ぐに検証できない状態ですので、もう少々お待ちくださいませ・・・。

      • さっさ より:

        返信ありがとうございます。
        いつも参考にしています。
        めちゃめちゃ助かってます!

        • mgo-tec mgo-tec より:

          さっさ さん

          お待たせしました。
          当方のM5Stackで確認したところ、特に問題有りませんでした。

          久々にFirebaseコンソールを開きましたが、従来のデータベースシークレットはまだ存在しています。
          赤文字で「データベースのシークレットは廃止された」と表示されておりますが、「レガシーの Firebase トークン生成ツールを使用する」と表示されているので、問題ありません。

          因みに、このページの7項目

          7.ブラウザのURL 入力欄にURL を入力して、Realtime Database の値を取得してみる

          を参照して、ブラウザのURL欄にデータベースシークレット入りのURLを入力してみてください。
          エラーが出ていなければ、問題無く使用できるはずです。

          あと、気になったのは、Arduinoスケッチのhostのところのアドレスには「https://」は不要ですのでご注意ください。
          以上、再度確認してみてくださいませ。

  5. ESP32 より:

    突然のコメントにて失礼します。
    4ページ目のソースコード39行目の前に
    client.setInsecure();
    が必要だと思われます。これを付け足すことで、DBに接続することができました。

    • mgo-tec mgo-tec より:

      ESP32さん

      ご指摘、ありがとうございます。
      この記事は3年以上前に書いたもので、いろいろと変わってしまったようですね。
      確かに当方で試した結果、ルート証明書が無いためにアクセスしてくれませんでした。
      Arduino core ESP32 のバージョンは2.0.2 です。
      おっしゃる通り、client.setInsecure(); を足せばアクセスできました。
      いつの間にかこんな関数ができていたんですね。
      教えて頂き、ありがとうございます。

      早速、ソースコードも修正しました。
      ただ、5ページ目のPATCHを使うコードはまだ動かないので、検証中です。
      できたらまたこのコメント欄でご報告します。

  6. ESP32 より:

    以前、指摘をさせていたいただいた者です。修正ありがとうございます。
    質問です。Firebase Realtime Databaseで読み込みしながら、書き込みをしたいです。ソースコードを教えていただけないでしょうか。
    「client.stop(); //これ絶対必要」をデータベースに書き込み終わったときに実行してしまうと、読み込みが途切れてしまい、データベースのデータが変わっても、変わったのがわからなくなってしまいます。(わかりにくい文章ですみません。)
    お忙しいところ恐れ入りますが、ご返信いただければ助かります。

    • mgo-tec mgo-tec より:

      ESP32さん

      PATCHリクエストの連続書き込みがようやくできました。5ページ目のコードを修正しました。
      また、そのついでにご要望にお応えして、データの送信とEventストリーム受信の両方を行うスケッチを書いてみました。
      Arduino core ESP32 のバージョンが2.0.2になると、だいぶ仕様が変わっていましたね。
      PATChリクエストはclient.stopしてしまうと、clientインスタンスを開放しないと再接続してくれなかったので、関数内でインスタンス生成する方式に変え、Eventストリーム受信のclientインスタンスは別で生成しました。
      以下のリンクにコードを載せておきます。
      https://www.mgo-tec.com/blog-entry-firebase-realtime-database-sever-sent-events-esp32-m5stack.html/5#title12

      • ESP32 より:

        教えていただきありがとうございます。
        読み込みと書き込み同時にできました!
        来年の夏休みの工作に活用していきたいです。

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