OGIMOノート~家族のためのモノづくり~

OGIMOテックノート ~家族のためのモノづくり~

重度障害の息子を持つ父親エンジニアの備忘録。自作の電子工作おもちゃ/リハビリ器具/ロボット関係の製作記録、思った事を残していきます

「ポータブル電光掲示板」を作ってみた ~ESP32×LED Matrixで日本語表示~

f:id:motokiinfinity8:20201219015030j:plain

製作目的

最近、文字に興味を持ち始めてきた息子。
本を読んでる中でも、ふと介助者の指をひっぱって「これ読んで」と言わんばかりの動作をする。

しかし、知的障害を有し好き嫌いの激しい息子に対して、普通の教科書などでの言葉の学習はなかなか難しい。
興味のない本に対しては、わずか一分も持たずポイっとされてしまう。
また、iPadなどのアプリを使って文字学習を行う事もあるが、
最近ではiPad = YouTube動画を見るモノ」という先入観が打ち勝って、
なかなかiPadを渡しても、文字勉強に活用するのが難しい…

そこで、息子が興味を持つ様な表示方法を用いて、「自然と文字に目がいく様な & 伝えたい言葉を自然に覚える仕掛け」を考えてみた。

製作物

スマホから入力操作できるポータブル電光掲示

スマートフォンやPCでウェブブラウザを開いて文字を入力、その文字を電光掲示板上に大きく表示するというシンプルな機能です。日本語も表示できるので、小学生の漢字のお勉強等に活用できます。


ハード構成

電光掲示板(LEDマトリックス)には、以下の部品を使っています。
amzn.to
amzn.to

4mmピッチ(P4)もしくは3mmピッチ(P3)の横64Pixel×縦32Pixelの電光掲示板を使っています。こちらの制御規格は"HUB75"と呼ばれるものだそうです。

ryosukeeeee.hatenablog.com
qiita.com

本規格について詳細は上サイトを参考にさせて頂きました。
この規格I/Fの良いところは、デイジーチェーンの様に複数のパネルをつなげる事でパネルを大きくする事ができる点です。




f:id:motokiinfinity8:20201219015435j:plain





こんな風に、2枚を接続するのも簡単です。
なお、電光掲示板はピッチサイズも様々なモノが販売されている様なので、必要なサイズに応じて選択すればよさそうです。


制御マイコンは、単体でネットワークに接続可能なESP32 DevKit Cを使いました。
amzn.to

esp32 dev kitCとLEDマトリクスのHUB75端子と直結して使用します。(IDCケーブル フラットリボンケーブル)
Amazon | uxcell メスコネクタ エクステンションワイヤー アダプタ IDCケーブル フラットリボンケーブル 8ピン 2.54mmのピッチ 30cmの長さ 5個入り | DIY・工具・ガーデン

接続ピン構成は以下としました。
R1 : 25 G1 : 26
B1 : 27
R2 : 21 G2 : 22
B2 : 23
A : 12 B : 16
C : 17 D : 18
CLK : 15 LAT : 32
OE : 33

ソフト構成① LEDマトリクス接続

LEDマトリクスの描画方法についていくつかのライブラリを試してみましたが、以下のライブラリを使うと、後述する日本語フォントと相性が良さそうでした。
github.com

描画ライブラリの使い方は、M5Stack等の液晶表示に使われるAdafruit_GFX.hと同じ感覚で使えます。
例えば、以下の様な描画サンプルになります。

#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
MatrixPanel_I2S_DMA matrix;
matrix.begin(R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN ); 
matrix.drawRect(0, 0, matrix.width(), matrix.height(), matrix.color444(15, 15, 15));
matrix.drawRect(1, 1, matrix.width()-2, matrix.height()-2, matrix.color444(15, 0, 0));
matrix.drawRect(2, 2, matrix.width()-4, matrix.height()-4, matrix.color444(0, 0, 15));

matrix.setTextSize(1);  //1サイズは8×8サイズ
matrix.setCursor(23, 7); 
matrix.setTextColor(matrix.color444(15,15,15));
matrix.println("HELLO");
matrix.setCursor(12, 16);
matrix.println("WORLD!");

なお、2枚以上を接続する場合、以下のサンプルコードや接続方法を参考にすると良さそうです。

github.com


ソフト構成② 日本語表示

上記のライブラリを使った場合、8x8の英数字表記のみになります。子供の日本語勉強などのためには日本語表示は必須となります。
そこで、日本語フォントを導入して簡単に表示可能な環境を作りたいと思います。
・東雲フォント 16Pixel x 16Pixel
・美咲フォント 8Pixel x 8Pixel

の2種類のフォントを導入したいと思います。

これらは予めフォントデータをストレージ等に格納しておく必要がありますが、M5Stackと異なりSDカードスロットがないため、今回はESP32内蔵のフラッシュメモリ(SPIFFS)に格納して使いたいと思います。

SPIFFSの使い方、格納方法は以下のサイトを参考にしました。
www.mgo-tec.com

これは非常に便利で3KB以下のデータであれば、SDカード不要でデータを格納して使う事が出来そうです!

■日本語 東雲フォント(16x16)の導入方法
www.riraotech.com

■日本語 美咲フォント(8x8)の導入方法
www.mgo-tec.com


製作ソフトコードの一部は以下になります。

#include <Adafruit_GFX.h>   // Core graphics library
#include <ESP32_SPIFFS_ShinonomeFNT.h>
#include <ESP32_SPIFFS_UTF8toSJIS.h>
#include <ESP32_SPIFFS_MisakiFNT.h>
#include "FS.h"
#include "SPIFFS.h"

const char* UTF8SJIS_file = "/Utf8Sjis.tbl"; //UTF8 Shift_JIS 変換テーブルファイル名を記載しておく
const char* Shino_Zen_Font_file = "/shnmk16.bdf"; //全角フォントファイル名を定義
const char* Shino_Half_Font_file = "/shnm8x16.bdf"; //半角フォントファイル名を定義
const char* Misaki_Zen_Font_file = "/MSKG_13.FNT"; //全角フォントファイル名を定義
const char* Misaki_Half_Font_file = "/mgotec48.FNT"; //半角フォントファイル名を定義
#define MAX_FONT_NUM 20  //表示文字(16x16)の最大文字数

ESP32_SPIFFS_ShinonomeFNT SFR;
ESP32_SPIFFS_MisakiFNT MFR;

#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
MatrixPanel_I2S_DMA matrix;

void setup() {
  matrix.begin(R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN );  // setup the LED matrix
  //フォントデータバッファ
    if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){
        Serial.println("SPIFFS Mount Failed");
        return;
    }
    SFR.SPIFFS_Shinonome_Init3F(UTF8SJIS_file, Shino_Half_Font_file, Shino_Zen_Font_file);
    MFR.SPIFFS_Misaki_Init3F(UTF8SJIS_file, Misaki_Half_Font_file, Misaki_Zen_Font_file);
   // LED表示タスク設定
   TaskHandle_t th; //マルチタスクハンドル定義
   xTaskCreatePinnedToCore(DisplayLED, "DisplayLED", 4096, NULL, 10, &th, 0); //マルチタスク起動
}

void loop() {
  mode_switch = digitalRead(MODE_INPUT_PIN);
  if(mode_switch == 0 && mode_switch_old ==1){
    if(font_16x16==true)  font_16x16= false;
    else                  font_16x16= true;
    display_is_changed = true;
    Serial.println(font_16x16);
  }
  
  delay(100);
  mode_switch_old = mode_switch;
}

void DisplayLED(void *pvParameters) {
    while(1){
      //文字表示
      displayStrCenterToLeftShift(displayStr, matrix.color444(15, 15, 15), 100, true);
      display_is_changed = false;
      delay(100);
    }
}

void displayStrCenterToLeftShift(String str, uint16_t color, int shiftTime_ms, bool skip){
  uint16_t sj_length = 0;//半角文字数 
  uint8_t font_buf[MAX_FONT_NUM*2][16] = {0};
  uint8_t misaki_font_buf[MAX_FONT_NUM][8];
  String strmes = str + " ";
  bool stopflg = false;

  //20文字以上の場合、20文字までに制限する
  //if(strmes.length() > MAX_FONT_NUM*3) strmes = strmes.substring(0, MAX_FONT_NUM-1);
  
  matrix.fillRect(0, 8, matrix.width(), 16, matrix.color444(0, 0, 0));
  //displayStrCenter(strmes, color);


  // 東雲フォント(16x16)
  if(font_16x16 == true){
    sj_length = SFR.StrDirect_ShinoFNT_readALL(strmes, font_buf);
    for(int i=0; i<4*sj_length; i++){
      for(int j=0; j<sj_length; j++){
        drawFont16x16(font_buf[j],font_buf[j+1], 8*j-2*i, 8, color);
      }
      if(stopflg == false){
          delay(1000);
          stopflg = true;
      }
      //matrix.fillRect(0, 8, matrix.width(), 16, matrix.color444(0, 0, 0));
      if(display_is_changed == true && skip == true) return;
      delay(shiftTime_ms);
    }
  //美咲フォント
  }else{
    sj_length = MFR.StrDirect_MisakiFNT_readALL(strmes, misaki_font_buf); //String 文字列から一気にフォント変換
    for(int i=0; i<2*sj_length; i++){
      for(int j=0; j<(sj_length/2+1); j++){
        drawFont8x8(misaki_font_buf[j], 4+8*j-2*i, 12, color);
      }
      if(stopflg == false){
        delay(1000);
        stopflg = true;
      }
      //matrix.fillRect(0, 8, matrix.width(), 16, matrix.color444(0, 0, 0));
      if(display_is_changed == true && skip == true) return;
      delay(shiftTime_ms);
    }
  }
}