Adafruit Bi-Color LED マトリックスをブラウザで WebSocket コントロール してみた

ESP8266 ( ESP-WROOM-02 )

※ ESP-WROOM-02 の Flashメモリサイズが 4MB のはずが、実は 2MB だったという情報がありました。
以下のページを参照して、Flashメモリサイズを確認しておくことを強くお勧めします。

ESP-WROOM-02 ( ESP8266 ) チップ・メモリ・MACアドレス情報確認方法
(2017/10/2)

今回は前回の記事で解説した Adafruit さんの I2C制御の2色LEDドットマトリックス
Bicolor LED Square Pixel Matrix with I2C Backpack
を自作のWebSocketライブラリを使って、スマホブラウザからコントロールしてみたいと思います。
スマートフォンのタッチセンサーと、ジャイロセンサーでコントロールしてみました。
2色のLEDが1ドットに入っているので、別々のドットマトリックスとしてコントロールできます。
以下の動画をご覧ください。

はじめの方ではスマホのジャイロセンサー値で黄緑色のLEDで縦線と横線をコントロールしていて、その後にタッチ移動すると赤色の縦線と横線が表示されていると思います。
動画で撮影すると赤色LEDがピンクっぽくなってしまいます。
なぜかどうしてもデジカメでは赤くなりません。これは仕方ないですね。
実際はカツンと赤色です。
そして、黄緑色と赤色が交差するところ、つまりは同時に発光しているところはオレンジ色になります。これもデジカメではうまくオレンジ色が出ませんでした。

では、これについて解説していきたいと思います。

スポンサーリンク

準備するもの

このブログを初めて見られる方もいると思いますので、念のために説明しておきます。
これはArduino UNOでは動きません。
ESP-WROOM-02 を使います。
これには ESP8266 チップが入っており、日本の電波法をクリアしたWi-Fi通信ボードです。
しかも、Arduinoスケッチを書き込んでArduinoとして動かすことができる優れものボードです。
私はそのボードをさらに使いやすくした スイッチサイエンス製 ESP-WROOM-02開発ボード というものを使ってます。

ESP-WROOM-02開発ボード
スイッチサイエンス(Switch Science)

ESPr Developer(ピンソケット実装済)
スイッチサイエンス(Switch Science)

とても売れていて、在庫切れの場合があります。
スイッチサイエンスさんのホームページで入荷通知を受け取る設定もできますのでそちらも利用してみてください。

2色LEDドットマトリックスはAmazon.co.jpのマルツオンラインさん販売があります。

Adafruit 1.2インチ2色LEDマトリックス基板 【902】
エイダフルート(Adafruit)
¥3,302(2024/03/19 14:19時点)

スマートフォンは出来るだけ最新の高速CPUのものを使ってください。
そして、OS は Android 、ブラウザは Google Chrome
できるだけ最新版を推奨します。
iOS では最新式のiPhone や iPad でもうまく動かず、途中で通信切断されてしまいますのでお勧めできません。
再接続すれば使えないことはないです。
この辺はいつか改善したいと思いますが、今は原因が全く分かりません。
解る方がいたらぜひ教えていただきたいです。

接続する

接続方法は前回の記事どおりですが、念のため載せておきます。
こんな感じです。

Bi-Color LEDマトリックスの組み立て方法は前回の記事をご覧ください
LEDドライバ HT16K33 はデータシートによると電源電圧は5Vが推奨ですが、3.3Vでも問題なく動作しました。

予めArduino IDE を設定しておく

Arduino IDE はArduino.ccページのIDE 1.6.5を使用してください。
他のバージョンや、Arduino.orgページのIDEでは動きませんのでご注意ください。
(2016/2/24現在)

IDEに StagingバージョンのESP8266ボードのインストールと、私が自作したライブラリ
EasyWebSocket Beta1.1 をインストールしておいてください。
これはこちらの記事を参照してください。

記事が飛び飛びで大変申し訳ございません。
ひとまとめにするのがとても大変なので、いつか時間に余裕ができたらやりたいと思います。
 

スケッチ書き込み、およびコンパイル書込みする

過去の記事にあるように、このEasyWebSocketライブラリではSPIFFSファイルシステムアップローダーを使って、サンプルスケッチのdataフォルダにあるspiffs_01.txtというファイルをESP-WROOM-02のフラッシュに事前に書き込む必要があります。

下図のようにサンプルスケッチを開き


一旦、名前を付けて保存しておきます。

次にスケッチのフォルダを開きます

dataフォルダを開き、

spiffs_01.txtファイルをテキストエディタで開きます。


メモ帳で開くと下図のようにいまいち改行がうまく表示してくれませんが、下図のようなところのIPアドレスをご自分の環境でESP-WROOM-02に割り当てたローカルIDアドレスに書き換えてください。

ちゃんとしたテキストエディタならばこのように表示されます。
私の場合はフリーソフトのAdobe Bracketsを使ってます。


一旦保存したら、下図のようにアップローダーをクリックするとESP-WROOM-02のフラッシュに書き込みが始まります。

書き込み終わっても何もメッセージは出ません。ひたすら待つだけです。かなり時間がかかりますので、気長に・・・。
※このSPIFFSファイルシステムアップローダーは1度行えばOKです。そうすれば、SPIFFSメモリーに記憶されます。
スケッチを修正したときも、その都度アップロードするする必要はありません。

次に、先ほど開いたサンプルスケッチをすべて削除して、以下のソースコードをコピペします。

コードを修正しました。
139行目に
html_str5 = "";
がありませんでした。失礼いたしました。(2016/1/5現在)

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

#include <EasyWebSocket.h>
#include <ESP8266WiFi.h>
#include <Hash.h>
#include <Wire.h>

#define LDaddrs1 (0x70)  //LEDドライバーHT16K33 アドレス

const char* ssid = "xxxx";
const char* password = "xxxx";

byte LedDot[8] = {0,0,0,0,0,0,0,0};//ここで初期化しておかないと、正しく表示されない
byte LedDot_dummy_green[8] = {0,0,0,0,0,0,0,0};
byte LedDot_dummy_red[8] = {0,0,0,0,0,0,0,0};
byte LedDot_cnv[8] = {0,0,0,0,0,0,0,0};

long CountTestTime;

byte cnt = 0;

EasyWebSocket ews;

String html_str1;
String html_str2;
String html_str3;
String html_str4;
String html_str5;
String html_str6;
String html_str7;

String ret_str;

int PingSendTime = 3000;

#define ledPin1 12
#define ledPin2 13
#define ledPin3 14

void setup() 
{
  Wire.begin(); // initialise the connection
  Wire.setClock(400000L);
  LED_Driver_Setup( LDaddrs1 ); //Ledドライバアドレス、On-Off、点滅周期(0~3)、明るさ0~15
  LED_Driver_Blink( LDaddrs1, 1, 0);
  LED_Driver_Brightness( LDaddrs1, 1 );
  LED_Driver_DisplayInt( LDaddrs1 );

  html_str1 = ews.EWS_Body_style("white", "black");
  html_str1 += ews.EWS_BrowserSendRate();
  html_str1 += "<br>\r\n";

  html_str1 += "<canvas id='canvas' width='320' height='320'></canvas>\r\n";
  html_str1 += "  <script type='text/javascript'>\r\n";
    
  html_str1 += "  Touch_Dot();\r\n";
  html_str1 += "  function Touch_Dot(){\r\n";
  html_str1 += "    var cW = 320;\r\n";
  html_str1 += "    var cH = 320;\r\n";
  html_str1 += "    var canvas = document.getElementById('canvas');\r\n";
  html_str1 += "    var ctx = canvas.getContext('2d');\r\n";
  html_str1 += "    var i,j;\r\n";
  html_str1 += "    for(i=0;i<8;i++){\r\n";
  html_str1 += "      for(j=0;j<8;j++){\r\n";
  html_str1 += "        ctx.beginPath();\r\n";
  html_str1 += "        ctx.fillStyle = '#444444';\r\n";
  html_str1 += "        ctx.arc(20*(i*2+1), 20*(j*2+1), 18, 0, Math.PI * 2, false);\r\n";
  html_str1 += "        ctx.fill();\r\n";
  html_str1 += "      }\r\n";
  html_str1 += "    }\r\n"; 
  html_str1 += "    canvas.addEventListener('touchmove', Dot_Display, false);\r\n";  
  html_str1 += "    function Dot_Display(e00){\r\n";
  html_str1 += "      e00.preventDefault();\r\n";
//  html_str1 += "      e00.stopPropagation();\r\n";//これは無くても良いかも?
  html_str1 += "      var e0000=e00.touches[0];\r\n";//一本目の指だけ処理する
  html_str1 += "      ctx.clearRect(0, 0, cW, cH);\r\n";
  html_str1 += "      var i,j;\r\n";
  html_str1 += "      for(i=0;i<8;i++){\r\n";
  html_str1 += "        for(j=0;j<8;j++){\r\n";
  html_str1 += "          ctx.beginPath();\r\n";
  html_str1 += "          ctx.fillStyle = '#444444';\r\n";
  html_str1 += "          ctx.arc(20*(i*2+1), 20*(j*2+1), 18, 0, Math.PI * 2, false);\r\n";
  html_str1 += "          ctx.fill();\r\n";
  html_str1 += "        }\r\n";
  html_str1 += "      }\r\n";
  html_str1 += "      var OffSet = e0000.target.getBoundingClientRect();\r\n";
  html_str1 += "      var ex = e0000.clientX - OffSet.left;\r\n";
  html_str1 += "      var ey = e0000.clientY - OffSet.top;\r\n";
  html_str1 += "      if( ex < 0 ){ex = 0;}\r\n";
  html_str1 += "      else if(ex>320){ex = 320;}\r\n";
  html_str1 += "      if( ey < 0 ){ey = 0;}\r\n";
  html_str1 += "      else if(ey>320){ey = 320;}\r\n";
  html_str1 += "      var Ex = 7-Math.floor(ex/42);\r\n";
  html_str1 += "      var Ey = Math.floor(ey/42);\r\n";
  html_str1 += "      ctx.globalAlpha = 1;\r\n"; //透明度の設定
  html_str1 += "      ctx.beginPath();\r\n";
  html_str1 += "      ctx.fillStyle = '#FFFFFF';\r\n";
  html_str1 += "      ctx.arc(40*(Math.floor( ex/42 ))+20, 40*(Math.floor( ey/42 ))+20, 20, 0, Math.PI * 2, false);\r\n";
  html_str1 += "      ctx.fill();\r\n";
  html_str1 += "      ctx.beginPath();\r\n";
  html_str1 += "      ctx.fillStyle = '#0000FF';\r\n";
  html_str1 += "      ctx.arc(40*(Math.floor( ex/42 ))+20, 40*(Math.floor( ey/42 ))+20, 15, 0, Math.PI * 2, false);\r\n";
  html_str1 += "      ctx.fill();\r\n";
  html_str1 += "      var Exy = '0' + String(Ex) + String(Ey);\r\n";
  html_str1 += "      doSend_canvas(Exy, 'touch');\r\n";
  html_str1 += "    };\r\n";
  html_str1 += "    };\r\n";
  html_str1 += "</script><br>\r\n";
        //スマホジャイロコントロール
  html_str2 = "  <script type='text/javascript'>\r\n";
  html_str2 += "  var global_gyro_dummy1;\r\n";//ここはグローバル変数にする
  html_str2 += "  window.addEventListener('deviceorientation', Gyro_Dot, false);\r\n";  
  html_str2 += "  function Gyro_Dot(event11) {\r\n";
  html_str2 += "    var alpha = event11.alpha;\r\n";
  html_str2 += "    var beta = Math.floor((event11.beta)/3);\r\n";
  html_str2 += "    var gamma = Math.floor(8-((event11.gamma+16)/2));\r\n";
  html_str2 += "    if(beta<0){ beta = 0;\r\n";
  html_str2 += "    }else if(beta>7){ beta = 7;}\r\n";
  html_str2 += "    if(gamma<0){ gamma = 0;\r\n";
  html_str2 += "    }else if(gamma>7){ gamma = 7;}\r\n";
  html_str2 += "    var GB = '0' + String(gamma) + String(beta);\r\n";
  html_str2 += "    if(global_gyro_dummy1 != GB){\r\n";//同じ値を送信しないようにする
  html_str2 += "      doSend_canvas(GB, 'gyro');\r\n";
  html_str2 += "      global_gyro_dummy1 = GB;\r\n";
  html_str2 += "    }\r\n";
  html_str2 += "  };\r\n";
  html_str2 += "</script>\r\n";
        //ドットマトリックス明るさ調整スライダー
  html_str3 = ews.EWS_Canvas_Slider_T("Dot_dim",200,40,"#777777","#ffffff");
  html_str3 += "<br>\r\n";
        //ESP-WROOM-02からのデータ送信をブラウザに表示する
  html_str4 = "from WROOM DATA =<font size=3>\r\n";
  html_str4 += ews.EWS_BrowserReceiveTextTag("text",25,"#FF00FF");
  html_str4 += "<br>\r\n";
  html_str4 += ews.EWS_Status_Text(20,"RED");
  html_str4 += "<br></font>\r\n";
  html_str4 += "<br><br>\r\n";  
  html_str4 += ews.EWS_Close_Button("WS CLOSE",150,40,17);
  html_str4 += "</body></html>\r\n";

  html_str5 = "";
  html_str6 = "";
  html_str7 = "";
  
  ews.AP_Connect(ssid, password);

  ews.EWS_HandShake(html_str1, html_str2, html_str3, html_str4, html_str5, html_str6, html_str7);
  
  CountTestTime = millis();
}

void loop() {
  String str;
  if(ret_str != "_close"){
    
    if(millis()-CountTestTime > 200){
      if(cnt > 9){
        cnt = 0;
      }
      switch(cnt){
        case 0:
          str = "_____";
          break;
        case 1:
          str = "____H";
          break;
        case 2:
          str = "___He";
          break;
        case 3:
          str = "__Hel";
          break;
        case 4:
          str = "_Hell";
          break;
        case 5:
          str = "Hello";
          break;
        case 6:
          str = "ello_";
          break;
        case 7:
          str = "llo__";
          break;
        case 8:
          str = "lo___";
          break;
        case 9:
          str = "o____";
          break;
      }
      
      ews.EWS_ESP8266_Str_SEND(str, "text");
      CountTestTime = millis();
      cnt++;
    }

    ret_str = ews.EWS_ESP8266CharReceive(PingSendTime);
    if(ret_str != "\0"){
      Serial.println(ret_str);
      if(ret_str != "Ping"){
        int ws_data = (ret_str[0]-0x30)*100 + (ret_str[1]-0x30)*10 + (ret_str[2]-0x30);
        switch(ret_str[4]){
          case 'D':
            ws_data = floor(ws_data/12); //LEDドライバHT16K33の明るさは0~16
            LED_Driver_Brightness(LDaddrs1, ws_data);
            break;
          case 't':
            LED_8X8matrix_BiCol('r', ret_str[1], ret_str[2]);
            break;
          case 'g':
            LED_8X8matrix_BiCol('g', ret_str[1], ret_str[2]);
            break;
        }
      }
    }
  }else if(ret_str == "_close"){
    delay(100);
    ews.EWS_HandShake(html_str1, html_str2, html_str3, html_str4, html_str5, html_str6, html_str7);
    CountTestTime = millis();
    ret_str = String('\0');
  }
}
//*************************************************************
void LED_8X8matrix_BiCol(char col, char x, char y)
{
  double xpow = pow(2,x-0x30);//0~7の数値をドットに置き換える演算
  byte i;

  for(i=0; i<8; i++){
    LedDot[i] =xpow;
  }
  
  LedDot[y-0x30]=B11111111;
  LED_Ada88_BiColor_cnv(LedDot, LedDot_cnv);
  
  switch(col){
    case 'r':
      LED_Driver_DisplayOutput(LDaddrs1, LedDot_dummy_green, LedDot_cnv);
      for(i=0; i<8; i++){
        LedDot_dummy_red[i] = LedDot_cnv[i];
      }
      break;
    case 'g':
      LED_Driver_DisplayOutput(LDaddrs1, LedDot_cnv, LedDot_dummy_red);
      for(i=0; i<8; i++){
        LedDot_dummy_green[i] = LedDot_cnv[i];
      }
      break;
  }
}

//**********************LEDドライバ HT16K33 セットアップ******************************************
void LED_Driver_Setup(byte LD_addrs)
{
  //HT16K33-28Wは8X8マトリックスLEDを2台まで制御できる。
  //ドライバIC#1 アドレスは0x70から設定する。
  Wire.beginTransmission(LD_addrs);
  Wire.write(0x20 | 1);  //システムオシレータをONにする
  Wire.endTransmission();
}
//**********************LEDドライバ HT16K33 点滅周期設定******************************************
void LED_Driver_Blink(byte LD_addrs, byte on_off, byte blink_Hz)
{
  //blink_Hz=0 点滅off, 1は2Hz, 2は1Hz, 3は0.5Hz, on_off=0は消灯、1は点灯 
  Wire.beginTransmission(LD_addrs);
  Wire.write(0x80 | (blink_Hz<<1) | on_off); 
  Wire.endTransmission();
}
//**********************LEDドライバ HT16K33 明るさ設定******************************************
void LED_Driver_Brightness(byte LD_addrs, byte brightness)
{
  // brightness= 0~15
  Wire.beginTransmission(LD_addrs);
  Wire.write(0xE0 | brightness);
  Wire.endTransmission();
}
//**********************LEDドライバ HT16K33 画面初期化******************************************
void LED_Driver_DisplayInt(byte LD_addrs)
{
  Wire.beginTransmission(LD_addrs);
  Wire.write(0x00);
  for(int i=0;i<8;i++){  
        Wire.write(B00000000);                   //1つ目の8x8LED初期化
        Wire.write(B00000000);                   //2つ目の8x8LED初期化
  }
  Wire.endTransmission();
}
//**********************Adafruit8x8 Bi-Color ドット bit変換******************************
void LED_Ada88_BiColor_cnv(byte* Bdot1, byte* Bdot2)
{
  for(byte i=0; i<8; i++){
    for(byte j=0; j<8; j++){
     bitWrite(Bdot2[j],i,bitRead(Bdot1[i],j));
    }
  }
}
//**********************LEDドライバ HT16K33 8X8データ送信******************************************
void LED_Driver_DisplayOutput(byte LD_addrs, byte* DotB1, byte* DotB2) // 引数の並びが見た目のLEDの並びなので注意
{
  int i,j;
        Wire.beginTransmission(LD_addrs);
        Wire.write(B00000000);
        for(i = 0; i<8; i++){
          Wire.write(DotB1[i]); //1つ目のLEDがある場合はここで送る
          Wire.write(DotB2[i]); //2つ目のLEDがある場合はここで送る
        }
        Wire.endTransmission();
}

このコードをザッと説明します。

8~9行目でご自分のルーター(アクセスポイント)のSSIDおよびパスワードに書き換えます。

ライブラリの関数の使い方は過去の記事を参照してください。

ここでのポイントは102行目のスマホのタッチセンサー値のEx, Eyを文字列にして、’0’という文字列も足して3桁の文字列にしてESP-WROOM-02に送信していることです。過去の記事のソースもある程度使いまわせるようにしてみました。
そして、WROOMでタッチセンサーとジャイロセンサー値を分類できるように ‘touch’ と ‘gyro’ という文字列を付加して送信します。
すると、201~212行で受信した文字列を分類できるようになります。

また、119~123行目にあるように同じ値をブラウザが送信しないようにしています。同じ値は通信トラフィックの無駄ですので、できるだけ排除します。

223行目~250行目ではBi-ColorマトリックスへのI2C制御送信を工夫しています。
228行目~230行目でターゲットのビットの縦1列を点灯させてます。
232行で、ターゲットのビットの横1列を点灯させてます。
LEDドライバのHT16K33はビットを送信したら、それを延々と保持しますので、LEDを光らせたらゼロというビットを送って消さなければならないわけです。

以上、ホントにザッとした説明です。
WebSocket通信について興味ある方はこちらの記事こちらの記事を参照してみてください。
このライブラリはこれらを全て盛り込んであります。

以上、ソースコード入力ができたら、コンパイル書き込みしてみましょう。
そしたらスマホのブラウザのURL入力欄にESP-WROOM-02のIPアドレスを入力すると、上記の動画のように動くと思います。

どうでしょうか? うまく動きましたでしょうか?

とりあえず、今回は以上です。

毎回思うことですが、SPIFFSファイルシステムを記事で説明すると結構たいへんで、長くなってしまいます。
もっとサクッと記事を上げないと自分の体力を消耗しまくってしまいます。

こりゃ、なんとかしないと・・・

ということで、また次回・・・

最新記事ではWebSocketライブラリがバージョンアップして、格段に使いやすくなりました。
現在BETA1.3です。
こちらのページをご覧ください。2016/2/22

Amazon.co.jp 当ブログのおすすめ

スイッチサイエンス ESPr Developer 32 Type-C SSCI-063647
スイッチサイエンス
¥2,420(2024/03/19 08:55時点)
ZEROPLUS ロジックアナライザ LAP-C(16032)
ZEROPLUS
¥22,504(2024/03/19 15:23時点)
Excelでわかるディープラーニング超入門
技術評論社
¥2,068(2024/03/19 03:23時点)

コメント

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