冬らしく寒くなった大晦日の今日、午前中に木工工作の締めの作業でニス塗を行いましたが、気温が低くニスがあまり伸びず少しムラができてしまいました。仕様では5℃以下では塗布しないようにとのことだったので、作業部屋の室温をみたら9℃だったので敢行したのですが。まあ、仕方ないですね。
午後は電光掲示板のコードの仕上げと動作撮影・編集です。追加注文してあるWS2812Bパネルはまだ届きません。先日、国内に届いたと書いたのは間違いで、届いているのはつい最近注文した別のものでした。あとから注文したものがこんなに早いと思わなかったので勘違いしてました。
という訳でパネル2枚で撮影した電光掲示板の動画です。iPhoneで撮影しましたが、LEDの輝度が高く自動で絞りが効いてしまいました。スミアも出でしまっています。肉眼だともっと明るくハッキリくっきり見えます。
- WS2812BマトリクスLEDの電光掲示板、リベンジ 2023.2.20
//---------------------------------------------------------------------
// 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);
}
//*******************東雲フォント全変換(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;
}












