2020年12月31日木曜日

WS2812BマトリクスLEDで電光掲示板

冬らしく寒くなった大晦日の今日、午前中に木工工作の締めの作業でニス塗を行いましたが、気温が低くニスがあまり伸びず少しムラができてしまいました。仕様では5℃以下では塗布しないようにとのことだったので、作業部屋の室温をみたら9℃だったので敢行したのですが。まあ、仕方ないですね。

午後は電光掲示板のコードの仕上げと動作撮影・編集です。追加注文してあるWS2812Bパネルはまだ届きません。先日、国内に届いたと書いたのは間違いで、届いているのはつい最近注文した別のものでした。あとから注文したものがこんなに早いと思わなかったので勘違いしてました。

という訳でパネル2枚で撮影した電光掲示板の動画です。iPhoneで撮影しましたが、LEDの輝度が高く自動で絞りが効いてしまいました。スミアも出でしまっています。肉眼だともっと明るくハッキリくっきり見えます。


動画の最後のネギ振りは、ソース内で制御しています(下記スケッチ参照)。

--追記--
2年強ぶりにパネル3枚を追加購入して、計5枚で試してみました。

回路図

バグがあるかも知れません、無保証です。

使い方

microSD内にconfig.txtファイル、表示するテキスト(ShiftJIS)を収めたmessage.txt、bmpファイル(高さ16pixel以下)を格納します。


config.txt でスクロール速度や輝度、デフォルトカラーを指定しています。
表示するメッセージは、message.txt内に記述します。

テキスト内に以下のような制御コマンドを記載できます。
<!red!>赤い文字<!white!>白い文字<!/hoge.bmp!>


スケッチ

configやテキストファイルのパーサは簡易的なものです。記述エラーがあるとおかしなことが起こるかもしれません。バグがあるかも知れません、無保証です。著作権は留保しますが、改変などご自由にどうぞ。

以下のライブラリを使っています。感謝です。
・WS2812B LEDドライバ

・東雲フォントライブラリ

・bmpファイルデコーダ

//---------------------------------------------------------------------
//                                                     2020.12.31 naka
//  16x16ドットマトリックスLED(WS2812B)電光掲示板 
//  
//  ・microSDに格納したテキストファイル(ShiftJIS)、bmp画像を表示
//  ・config.txtファイルにスクロール速度、テキストの表示色デフォルト、輝度定義
//  ・表示するテキストに文字色、bmpファイル名を埋め込み可能
//    例)<!red!>赤い文字<!white!>白い文字<!/hoge.bmp!>
//---------------------------------------------------------------------
#include <ESP32_SPIFFS_ShinonomeFNT.h>
#include <SD.h>
#include <FS.h>
#include <Adafruit_NeoPixel.h>
#include "BITMAPDecoder.h"

// IOポート
#define PIN       5 // Neopixel 制御用ピン番号
#define SD_CS    16 // SD card chip select

// configデフォルト
uint8_t text_r = 255;
uint8_t text_g = 255;
uint8_t text_b = 255;
int     scroll_wait_ms = 40;
int     brightness = 5; // 1~100
int     brightness_div = 100 / brightness;

// マトリクスパネルサイズ
#define UNIT_W      16                // 1枚のパネルのピクセル数(横)
#define UNIT_H      16                // 1枚のパネルのピクセル数(縦)
#define UNIT_NUM    2                 // 並べるパネルの枚数
#define NUMPIXELS  (UNIT_W*UNIT_H*UNIT_NUM) // LED数

Adafruit_NeoPixel         pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
ESP32_SPIFFS_ShinonomeFNT SFR;
BITMAPDecoder             bitmap = BITMAPDecoder();
uint32_t           scrollBuff[UNIT_H]; // パネル1列分

// LEDパネルのスクロール(デフォルトは1列スクロール)
void scrollPixel(int16_t n = 1) {
  int16_t i,x,y;
  for (i=0;i<n;i++) {
    for (x=UNIT_W*UNIT_NUM-1; x>=0; x--) {
      for (y=0;y<UNIT_H;y++) {
        setPixel(x+1,y,getPixel(x,y));
      }
    }
    for (y=0;y<UNIT_H;y++) {
      setPixel(0,y,scrollBuff[y]);
      scrollBuff[y] = 0; // クリア
    }
    scroll_wait(scroll_wait_ms);
    showPixel();
  }
}

// bmpファイルのスクロール表示
void scrollImgFile(char* fileName) {
  File bmpFile = SD.open(fileName, "r");
  bitmap.checkFile(bmpFile);

  for (uint8_t x=0; x<bitmap.width(); x++) {
    for (uint8_t y=0; y<16; y++) {
      uint8_t r,g,b;
      PIXEL p = bitmap.readPixel(bmpFile,x,y);
      r = p.r/brightness_div; g = p.g/brightness_div; b = p.b/brightness_div;
      scrollBuff[y] = pixels.Color(r,g,b);
    }
    scrollPixel();
  }
  bmpFile.close();
}

// bmpファイルの表示(右上角がLEDパネルの右上;offsetxで左にズレる)
void dispImgFile(char* fileName, uint16_t offsetx=0) {
  File bmpFile = SD.open(fileName, "r");
  bitmap.checkFile(bmpFile);
  
  for (uint8_t x=0; x<bitmap.width(); x++) {
    for (uint8_t y=0; y<16; y++) {
      uint8_t r,g,b;
      PIXEL p = bitmap.readPixel(bmpFile,x,y);
      r = p.r/brightness_div; g = p.g/brightness_div; b = p.b/brightness_div;  
      setPixel(offsetx+bitmap.width()-x-1,y,pixels.Color(r,g,b));
    }
  }
  showPixel();
  bmpFile.close();
}

// LEDパネルクリア
void clearPixel() {
  uint16_t pos;
  for (pos=0; pos<UNIT_H; pos++)
    scrollBuff[pos] = 0;
    
  for (pos=0;pos<NUMPIXELS;pos++)
    pixels.setPixelColor(pos, 0);
  pixels.show();
}

// LEDパネル状のx,y位置(x=0,y=0がパネルの右上)をLEDの番号に変換
uint16_t xy2pos(uint16_t x,uint16_t y) {
  uint16_t pos; // シリーズに連結されたLEDの番号(右上0起点)
  if (y%2==0) { // 偶数行
     pos = UNIT_W * y + x;
     pos =  pos + UNIT_W * UNIT_H * (int)(x/UNIT_W) - UNIT_W * (int)(x/UNIT_W);
  }
  else {         // 奇数行
     pos = UNIT_W * y + (UNIT_W - x) - 1;
     pos = pos + UNIT_W * UNIT_H * (int)(x/UNIT_W) + UNIT_W * (int)(x/UNIT_W);;
  }
  return pos;
}

// x,y座標にカラーrgb値を設定
void setPixel(int16_t x, int16_t y, uint32_t rgb) {
  int16_t pos = xy2pos(x,y);
  if (pos<NUMPIXELS)
     pixels.setPixelColor(pos,rgb);
}

// x,y座標のカラーrgb値を取り出す
uint32_t getPixel(int16_t x, int16_t y) {
  int16_t pos = xy2pos(x,y);
  if (pos<NUMPIXELS)
    return pixels.getPixelColor(pos);
  else
    return 0;
}

// LEDパネルの表示更新
uint32_t showPixel() {
  pixels.show();
}

// 1列スクロールするときの待ち時間
void scroll_wait(int msec) {
  static uint32_t prevtime = millis();
  while((millis()-prevtime) < msec);
  prevtime = millis();
}

// 文字フォントのスクロール表示
void scrollFont(uint8_t *fnt, uint8_t R, uint8_t G, uint8_t B) {
  for (uint8_t i=0; i<8; i++) {
    // フォントパターン1列分をスクロールバッファにセット
    for (uint8_t y=0; y<16; y++) {
      if (fnt[y] & (0x80 >> i)) {
        scrollBuff[y] = pixels.Color(R/brightness_div,G/brightness_div,B/brightness_div);
      } else {
        scrollBuff[y] = 0;
      }
    }
    scrollPixel();
  }
}

#define MAX_STRLEN 2
#define FONT_SIZE  16
// 文字のスクロール表示(1文字)
void scrollText(char* msg, uint8_t R, uint8_t G, uint8_t B) {
  uint8_t  fontBuff[MAX_STRLEN][FONT_SIZE] = {0};
  uint16_t fontNum;
  // ShiftJIS文字(1文字)をビットマップに変換(オリジナルのライブラリになかったので追加)
  fontNum = SFR.SjisStrDirect_ShinoFNT_readALL(msg, fontBuff);
  for (int i=0;i<fontNum;i++) {
    scrollFont(fontBuff[i], R, G, B);         
  }
}

#define CNTLBUFF_SIZE 30
#define SPLIT_SIZE 3
// ','区切りでテキストを分割
int splitChar(char* data, char splitdata[][CNTLBUFF_SIZE+1]) {
  int n = strlen(data);
  int splitno = 0;
  int j = 0;
  int i;
  for (i=0;i<n;i++) {
    if (data[i]==',') {
      splitdata[splitno++][j] = '\0';
      j = 0;
      if (splitno>=SPLIT_SIZE)
        splitno = 0;
    }
    else
      splitdata[splitno][j++] = data[i];
  }
  splitdata[splitno++][j] = '\0';
  return splitno;
}

#define COLOR_NUM 8
// テキスト中に埋め込まれた制御コマンドを実行
// 制御コマンド(<! !>の間にある文字列)は以下
//   <!r,g,b!>      ... 文字色R,G,B値(0-255)をカンマ区切り
//   <!color!>      ... 文字色 color:white,red,green,blue,magenta,cyan,yellow,orange
//   <!/hoge.bmp!>  ... 表示ビットマップファイル名
void applyCntl(char* buff) {
  static struct {
          char*   name;
          uint8_t r,g,b;
  } color[COLOR_NUM] =
    {"white"  ,255,255,255,
     "red"    ,255,  0,  0,
     "green"  ,  0,255,  0,
     "blue"   ,  0,  0,255,
     "magenta",255,  0,255,
     "cyan"   ,  0,255,255,
     "yellow" ,255,255,  0,
     "orange" ,255,50, 0};
  
  char splitdata[3][CNTLBUFF_SIZE+1];
  int i;
  if (strlen(buff)==0)
    return;
  else {
    int n = splitChar(buff,splitdata);
    if (n==3) { // r,g,b
      String val = splitdata[0];
      text_r = val.toInt();
      val = splitdata[1];
      text_g = val.toInt();
      val = splitdata[2];
      text_b = val.toInt();
    }
    else if (n==1) {
      int colorFlag = 0;
      for (i=0;i<COLOR_NUM;i++) {
        if (strcmp(splitdata[0],color[i].name)==0) {
          text_r = color[i].r; text_g = color[i].g ; text_b = color[i].b;
          colorFlag = 1;
          break;
        }
      }
      if (colorFlag==0) {
        int m = strlen(splitdata[0]);
        strcmp(splitdata[0],"bmp");
        if (m>5) { // "/x.bmp"
          if (strcmp(&splitdata[0][m-4],".bmp")==0) {
            scrollImgFile(splitdata[0]);       
          }
        }
      }
    }
  }
}

// ShiftJIS文字が全角か判定する
int is_zenkaku(char *sjis){
    unsigned char c = sjis[0];
    if ( (c>=0x81 && c<=0x9f) || (c>=0xe0 && c<=0xfc)){
        c=sjis[1];
        if (c>=0x40 && c<=0xfc) return 1;
    }
    return 0;
}

// テキストファイル内のテイストのスクロール表示(制御コマンドの抽出を含む)
void scrollMsgFile(char* fileName) {
    char buff[3],wk;
    int cntlflag;
    char cntlbuff[CNTLBUFF_SIZE+1];
    char c;
    int zh;
    File textFile;
    textFile = SD.open(fileName,FILE_READ);
    int i = 0, j = 0;
    while(textFile.available()){
      c = char(textFile.read());
      if (c=='\n') {    // 改行コードなら表示中のpixelをすべてスクロール
        scrollPixel(UNIT_W*UNIT_NUM);
        continue;
      }
      buff[i++] = c;
      if (i==2) {
        buff[i] = 0;
        zh = is_zenkaku(buff);
        if (zh==0) { // 半角
          if (buff[0]=='<' && buff[1]=='!') { // 制御コード開始
            cntlflag = 1;
            i = 0;
            continue;
          }
          else if (buff[0]=='!' && buff[1]=='>') { // 制御コード終了
            cntlflag = 0;
            cntlbuff[j] = '\0';
            j = 0;
            applyCntl(cntlbuff);
            i = 0;
            continue;
          }
          else if (cntlflag==1) { // 制御コードバッファリング
             cntlbuff[j++] = buff[0];
             if (j>CNTLBUFF_SIZE) j=0;
             buff[0] = buff[1];
             buff[1] = '\0';
             i = 1;
             continue;
          }
          wk = buff[1];
          buff[1] = 0;
        }
        scrollText(buff,text_r,text_g,text_b);
        if (zh==0) {
          buff[0] = wk;
          i = 1;
        }
        else {
          i = 0;
        }
      }

    }
    if (i>0) {
      buff[i]= 0;
      scrollText(buff,text_r,text_g,text_b);
    }
  textFile.close();
}

// microSDから1行読み取る(config.txt用)
int readLine(File fp,char *lineData){
  int i = 0;
  if (!fp.available())
    return -1;
  while (fp.available()) {
    lineData[i] = fp.read();
    if (lineData[i]=='\n') {
      lineData[i] = 0;
      return i-1;
    }
    i++;
  }
  return i;
}

// config.txtのパース
void parse_config(String line) {
  String key,val;
  int n;
  if (line.length()==0)
    return;
  else if ((n=line.indexOf(':'))>1) {
    key = line.substring(0,n);
    key.trim();
    val = line.substring(n+1);
    val.trim();
    if (key=="text_r")
      text_r = val.toInt();
    else if (key=="text_g")
      text_g = val.toInt();
    else if (key=="text_b")
      text_b = val.toInt();
    else if (key=="scroll_wait")
      scroll_wait_ms = val.toInt();
    else if (key=="brightness") {
      brightness = val.toInt();
      if (brightness>100) brightness = 100;
      else if (brightness<=0) brightness = 1;
      brightness_div = 100 / brightness;
    }
  }
}

// config.txtをロードする
void load_config() {
  char lineData[32];
  int n;
  String line,line2;
  String comment = "#";
  File ConfigFile = SD.open("/config.txt",FILE_READ);
  if (ConfigFile) {
    while ((n = readLine(ConfigFile,lineData))>=0) {
      line  = lineData;
      line.trim();
      if (line.startsWith(comment))
        continue;
      else
        parse_config(line);
    }
    ConfigFile.close();
  }
}

void scrollMsg(char* msg) { // 半角文字のみ
    char buff[2];
    int n = strlen(msg);
    for (int i=0;i<n;i++) {
      if (msg[i]=='\n') {    // 改行コードなら表示中のpixelをすべてスクロール
        scrollPixel(UNIT_W*UNIT_NUM);
        continue;
      }
      else {
        buff[0] = msg[i];
        buff[1] = '\0';
        scrollText(buff,text_r,text_g,text_b);
      }
    }
}

void setup() {
  const char* Shino_Zen_Font_file   = "/shnmk16.bdf";    //全角フォントファイル名を定義
  const char* Shino_Half_Font_file  = "/shnm8x16r.bdf";  //半角フォントファイル名を定義
  SFR.SPIFFS_Shinonome_Init2F(Shino_Half_Font_file, Shino_Zen_Font_file);

  if (!SD.begin(SD_CS, SPI, 24000000)) {
    scrollMsg("microSD Error\n");
  }
  else {
    scrollMsg("OK\n");
    load_config();
    delay(1000);
  }
}
    
void loop() {  
  scrollMsgFile("/message.txt");

  for (int i=0;i<5;i++) {
    dispImgFile("/negi1.bmp",16);
    delay(200);
    dispImgFile("/negi2.bmp",16);
    delay(200);
  }
  scrollPixel(16);
  
  delay(1000);
 }

ESP32_SPIFFS_ShinonomeFNT

ESP32_SPOFFS_ShinonomeFNTライブラリに、ShiftJIS文字列をフォントパターンに変換するSjisStrDirect_ShinoFNT_readALL関数追加しました。オリジナルはUTF8用です。

ヘッダファイルとcppファイルの追加箇所を反転表示しています。



オリジナルにあったUTF8からSJISへの変換をスキップしただけです。
//*******************東雲フォント全変換(SJIS)*************************
uint16_t ESP32_SPIFFS_ShinonomeFNT::SjisStrDirect_ShinoFNT_readALL(String str, uint8_t font_buf[][16]){
  uint8_t sj_txt[str.length()+1];
  int16_t i;
  for(i=0;i<str.length();i++) sj_txt[i] = str[i];
  sj_txt[i] = 0;
  uint16_t sj_length = i;

  ESP32_SPIFFS_ShinonomeFNT::SjisToShinonome16FontRead_ALL(_SinoZ, _SinoH, 0, 0, sj_txt, sj_length, font_buf);
  return sj_length;
}

BITMAP decoder

BITMAP decoderのヘッダとcppソースはスケッチを同じフォルダに置きました。


関連記事

 

0 件のコメント:

コメントを投稿