はじめに
フルカラーLEDのWS2813Bを7セグLED風に並べて数字を表示する基板(WS2813-7segPanel)を作成したので、その基板を12枚連結してカレンダー時計を作成します。
フルカラーLEDの色を単純に変えて時計を作っても面白くないので、インターネットから天気予報の情報を取得して降水確率に対応した色で文字色を表現することにします。
#基板の説明
基板単体の大きさは82×100mmの大きさで、表面にはWS2813Bを24個実装して7セグ風の数字と、ピリオド(.)とコロン(:)表現できます。また右上に温度、湿度、気圧センサのBME280と、中央下側に照度センサを搭載しています。
裏面は制御用マイコンWROOM-02とその周辺回路となっています。
基板の入手方法はスイッチサイエンス殿で販売させてもらってます。
#完成品
完成写真です。基板を6×2枚連結してトータル12枚で492×200mmの大きなパネルになります。
基板には上下左右に連結するためのはんだパターンがあるので半田で連結して作成します。
上の段は「月日」、「温度・湿度」、「気圧」を3秒間隔で切替て表示します。
下の段は時間を表示します。せっかくフルカラーLEDを使っているので時間の文字色は天気予報の降水確率を色に変換して表示することにします。
#パネル作成#
制御マイコンを搭載した基板1枚と、マイコン無しの基板11枚を使ってパネルを作成します。
裏面からみて左下にマイコン搭載基板を置いて、信号が青の矢印方法に流れるようにマイコン無し基板を並べて、緑の部分と赤のジャンバーを半田でショートさせて連結します。また、上下の基板を固定するためGNDパターンも半田でショートします。
基板連結は半田のみでは無理なので、リード線としてピンヘッダのピンを抜いて使います。硬さ長さともちょうどいい感じです。
#降水確率取得の準備#
マイコンで天気予報を取得できるサイトをいろいろ探しましたが、日本のサイトでは見つからなかったので海外のサイトから取得することにしました。
無料で5日先までの天気予報を取得できるAPIを提供してくれているOpenWeatherを使うことにします。
このサービスを使うためにはOpenWeatherにアカウントを作成する必要があります。
Create New Account画面で必要な情報を入力してアカウントを作成します。
アカウントが作成出来たらアカウント名の右の▼から[My API Keys]を選択してAPIキーを取得します。
実際に天気予報が取得できるか試してみます。場所はlatに緯度、lonに経度を設定して、appidに取得したAPIキーに置き換えてブラウザーのアドレスに入力します。
https://api.openweathermap.org/data/2.5/onecall?lat=34.68750348806358&lon=135.5259251795476&exclude=current&appid=**************************&units=metric%22
以下のような表示になれば問題ありません。
{"lat":34.6875,"lon":135.5259,"timezone":"Asia/Tokyo","timezone_offset":32400,"minutely":
・・・
次にhttpsサイトに接続する場合に必要になるフィンガープリントを取得します。
OpenWeatherをブラウザーで表示した状態で以下の手順でフィンガープリントを確認します。
[Chrome]の場合
1.Developer Tools を開く
2.Security タブを開く
3.View Certificate ボタン押下
4.詳細タブを選択して拇印を選択すると下に16進の数字40桁が表示される
この数字がフィンガープリントでプログラム作成時に必要になります。
これで天気予報をプログラムから取得する準備は完了です。
#プログラム
全ソースを記載します。
ソース内の[APDKEY]、[LAT]、[LON]は自分のAPIキー、緯度、経度に置き換えます。
緯度、経度はGoogleMapで簡単に取得できます。
またfingerprint[20]の配列にフィンガープリントを2桁づつ区切った値に置き換えます。
/********************************************/
// NTPサーバから時刻を取得する時計
// パネル12枚を使って、時分秒を表示
// OpenWeatherのAPIで降水確率をとり、文字色を変える
//
// 2021.4.03 Ver 2.0.0 初期
// 2021.4.20 Ver 2.0.1 予報の日数を3,6日切替機能追加
/********************************************/
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <WiFiClientSecure.h>
#include <ESP8266HTTPClient.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h> //https://github.com/tzapu/WiFiManager
#include <EEPROM.h>
#include <TimeLib.h>
#include <Time.h>
#include "WS2813Panel.h"
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
//-----------
#define VER 201
#define PANEL_NUM 12
#define JST 3600 * 9
#define TRIGGER_PIN 0
const String HOST = "https://api.openweathermap.org/data/2.5/onecall";
const String APIKEY = "***********************";
const String LAT = "xx.xxxx"; //緯度
const String LON = "xxx.xxxx"; //経度
//-----------
//色
const uint32_t GREEN = 0xFF0000;
const uint32_t RED = 0x00FF00;
const uint32_t BULE = 0x0000FF;
const uint32_t YELLOW = 0xFFFF00;
const uint32_t CYAN = 0xFF00FF;
const uint32_t MAGENTA = 0x00FFFF;
const uint32_t WHITE = 0xFFFFFF;
const uint8_t fingerprint[20] = {0xXX, 0xXX, 0xXX, 0xXX, 0xXXf, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX};
static const char *wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"};
uint32_t COLOR[3] = {0xff0000, 0x00ff00, 0x0000ff};
Adafruit_BME280 bme; // I2C
//-----------
WS2813Panel MyPanel(PANEL_NUM);
uint8_t bright = 50;
uint32_t swstat = 0;
float RainRates[8] = {999}; //8日分の降水確率
uint8_t lights[64] = {};
bool DEMO = false;
int Mode = -1; //月、温湿度、気圧 表示切替
uint8_t ForecastDay = 0; //0=3dyas 1=6days
int ChangeTime = 3000; //表示切替時間(msec)
uint NextGetTime;
float Temp;
float Humidity;
float preasure;
//-----------
void setup()
{
Serial.begin(115200);
Serial.println("Program Start");
Serial.println("LED Initialze");
MyPanel.Begin();
delay(10);
MyPanel.Clear();
MyPanel.Show();
//照度自動調整ON
MyPanel.AutoBright = true;
pinMode(TRIGGER_PIN, INPUT);
//WiFiManager
//Local intialization. Once its business is done, there is no need to keep it around
WiFiManager wifiManager;
wifiManager.autoConnect("AutoConnectAP");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
Serial.println("connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
//NTP
configTzTime("JST-9", "ntp.nict.jp", "ntp.jst.mfeed.ad.jp"); // 2.7.0以降, esp32コンパチ
delay(1000);
time_t t = time(NULL);
struct tm *tlocal = localtime(&t);
#if 1
Serial.printf(" %04d/%02d/%02d(%s) %02d:%02d:%02d\n",
tlocal->tm_year + 1900, tlocal->tm_mon + 1, tlocal->tm_mday,
wd[tlocal->tm_wday],
tlocal->tm_hour, tlocal->tm_min, tlocal->tm_sec);
#endif
//BME280初期化 I2c SDA=4 SDC=5 ピン
Wire.begin(4, 5);
unsigned status = bme.begin(0x76);
if (!status)
{
Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
Serial.print("SensorID was: 0x");
Serial.println(bme.sensorID(), 16);
while (1)
delay(10);
}
//Ver表示
DispVer();
//予報取得日数
GetForecastDay();
//次回画面切替タイム設定
NextGetTime = millis() + ChangeTime;
//降水確率
GetRainFallOpenWeather();
}
//-------------------------------------------------
void DispVer()
{
MyPanel.DispNum(2, (VER / 100), 0xffffff);
MyPanel.DispDot(2, 0xffffff);
MyPanel.DispNum(1, ((VER / 10) %10), 0xffffff);
MyPanel.DispDot(1, 0xffffff);
MyPanel.DispNum(0, (VER % 10), 0xffffff);
MyPanel.Show();
delay(2000);
MyPanel.Clear();
MyPanel.Show();
}
//-------------------------------------------------
uint8_t GetForecastDay()
{
EEPROM.begin(16);
EEPROM.get<uint8_t>(0, ForecastDay);
Serial.print("ForecastDay : ");
Serial.println(ForecastDay);
if (ForecastDay == 0)
MyPanel.DispNum(1, 3, 0x0000ff);
else
MyPanel.DispNum(1, 6, 0x0000ff);
MyPanel.DispNum(0, 0x1a, 0x0000ff);//d
MyPanel.Show();
}
//-------------------------------------------------
void loop()
{
static int presec;
static int startsec;
static uint16_t cnt = 0;
uint32_t colors[6] = {};
time_t t = time(NULL);
struct tm *tlocal = localtime(&t);
#if 0
Serial.printf(" %04d/%02d/%02d(%s) %02d:%02d:%02d\n",
tlocal->tm_year + 1900, tlocal->tm_mon + 1, tlocal->tm_mday,
wd[tlocal->tm_wday],
tlocal->tm_hour, tlocal->tm_min, tlocal->tm_sec);
#endif
//スイッチ監視(チャタリング防止)
if (digitalRead(TRIGGER_PIN) == LOW)
{
swstat = (swstat << 1) | 0x1;
Serial.println(swstat);
//4秒長押しでデモモード
if (swstat == 0xFFFFFFFF)
{
DEMO = !DEMO;
swstat = 0;
MyPanel.Clear();
MyPanel.Show();
}
//予報日数切替
if (swstat == 0x0F)
{
ForecastDay = (ForecastDay == 0) ? 1 : 0;
EEPROM.put<byte>(0, ForecastDay);
EEPROM.commit();
if (ForecastDay == 0)
MyPanel.DispNum(0, 3, 0xffffff);
else
MyPanel.DispNum(0, 6, 0xffffff);
MyPanel.Show();
}
delay(100);
return;
}
else
{
swstat = 0;
}
presec = tlocal->tm_sec;
//毎時1分、31分に降水確率取得
if (((tlocal->tm_min % 30) == 1 && tlocal->tm_sec == 0))
{
GetRainFallOpenWeather();
Serial.println("GetRainFallOpenWeather");
}
//DEMOモード 降水確率ランダムに変更
if (DEMO)
{
randomSeed(tlocal->tm_sec);
RainRates[0] = random(0, 100);
RainRates[1] = random(0, 100);
RainRates[2] = random(0, 100);
}
for (int i = 0; i < 6; i++)
{
byte R;
byte G;
byte B;
if (RainRates[i] > 100.0) //取得失敗
{
R = 128.0;
B = 0;
G = 128.0;
}
else if (RainRates[i] > 50)
{
R = (255.0 * (RainRates[i] - 50.0) / 50.0);
B = (255.0 * (100.0 - RainRates[i]) / 50.0);
G = 0;
}
else
{
B = (255.0 * RainRates[i] / 50.0);
G = (255.0 * (50.0 - RainRates[i]) / 50.0);
R = 0;
}
// Serial.printf("rain_max[%d] = %d B = %d G = %d R = %d\n", i, rain_max[i], B, G, R);
colors[i] = B | (G << 8) | (R << 16);
#if 0 //
colors[i] = Wheel((byte)((cnt / 10) % 0xff));
#endif
}
if (ForecastDay == 0) //3日モードの時は、2桁ずつ同じ数字にする
{
colors[5] = colors[2];
colors[4] = colors[2];
colors[3] = colors[1];
colors[2] = colors[1];
colors[1] = colors[0];
}
//指定時間ごととに表示モードを更新
if (millis() > NextGetTime)
{
Mode = (Mode + 1) % 3;
NextGetTime = millis() + ChangeTime;
Temp = bme.readTemperature();
Humidity = bme.readHumidity();
preasure = bme.readPressure() / 100.0F;
//DEMOモード 温度湿度をランダムに変更
if (DEMO)
{
Temp = random(-9, 50);
Humidity = random(1, 99);
}
}
switch (Mode)
{
case 0: //月日表示
DispDate(tlocal, colors[0]);
break;
// case 1://温度
// DispTemp(Temp); break;
// case 2://湿度
// DispHumid(Humidity); break;
case 1:
DispTempHumid(Temp, Humidity);
break;
case 2: //気圧
DispPress(preasure);
break;
}
//時分秒表示
DispTime(tlocal, colors);
}
//-------------------------------------------------
void GetRainFallOpenWeather()
{
char *key[] = {"daily", "dt\":", "pop\":"};
String host1 = HOST;
host1 += "?lat=" + LAT;
host1 += "&lon=" + LON;
host1 += "&exclude=current&units=metric";
host1 += "&appid=" + APIKEY;
Serial.println(host1);
std::unique_ptr<BearSSL::WiFiClientSecure> client(new BearSSL::WiFiClientSecure);
client->setFingerprint(fingerprint);
HTTPClient https;
char buff[16] = {0};
String temp;
char c;
int searchidx = 0; //検索キー
int pos = 0;
int dayindex = 0;
char *pbuff;
Serial.print("[HTTPS] begin...\n");
// HTTPS
if (https.begin(*client, host1))
{
Serial.print("[HTTPS] GET...\n");
// start connection and send HTTP header
int httpCode = https.GET();
// httpCode will be negative on error
if (httpCode > 0)
{
// HTTP header has been send and Server response header has been handled
Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
// file found at server
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY)
{
//取得データ長
int len = https.getSize();
Serial.printf("getSize() = %d\n", len);
WiFiClient *stream = https.getStreamPtr();
// get available data size
int strmsize = (int)(stream->available());
Serial.printf(" size = %d len = %d\n", strmsize, len);
if (strmsize == 0)
{
Serial.println("************* break *****************");
https.end();
return;
}
bool findflg = false;
while (len > 0)
{
stream->readBytes(&c, 1);
//検索文字に一致するか
if (key[searchidx][pos] == c)
{
if (pos == (strlen(key[searchidx]) - 1))
{
if (searchidx == 0) //"daily"検索
{
Serial.printf("Find %s\n", key[searchidx]);
searchidx = 1; //検索文字を"dt\"
}
else if (searchidx == 1)
{
Serial.printf("Find %s\n", key[searchidx]);
pbuff = &buff[0];
while (1)
{
stream->readBytes(&c, 1);
if (c == ',')
{
*pbuff = 0;
searchidx = 2;
time_t ttt = atoi(buff);
struct tm *tttlocal = localtime(&ttt);
Serial.printf(" %04d/%02d/%02d",
tttlocal->tm_year + 1900, tttlocal->tm_mon + 1, tttlocal->tm_mday);
break;
}
//Serial.print(c);
*pbuff = c;
pbuff++;
}
}
else if (searchidx == 2) //"pop"が見つかった
{
// Serial.printf("Find %s\n", key[searchidx]);
pbuff = &buff[0];
while (1)
{
stream->readBytes(&c, 1);
if (c == ',')
{
*pbuff = 0;
searchidx = 1;
float rate = atof(buff);
Serial.printf(" %3.0f%%\n", round(rate * 100.0));
RainRates[dayindex++] = round(rate * 100.0);
break;
}
//Serial.print(c);
*pbuff = c;
pbuff++;
}
if (dayindex > 6)
break;
}
}
pos++;
}
else
{
pos = 0;
}
len--;
}
https.end();
Serial.printf("https.end()\n");
}
else
{
Serial.printf("[HTTPS] Unable to connect\n");
}
}
}
}
//------------------------------------
//月日を表示
//------------------------------------
void DispDate(struct tm *tm, uint32_t clr)
{
int8_t mnum[2]; //月の数字
mnum[1] = (tm->tm_mon + 1) / 10; //10の位
mnum[0] = (tm->tm_mon + 1) % 10; //1の位
int8_t dnum[2]; //日の数字
dnum[1] = tm->tm_mday / 10;
dnum[0] = tm->tm_mday % 10;
for (int i = 6; i < 12; i++)
MyPanel.DispNum(i, 0x10, 0); //空白
MyPanel.DispNum(8, 0x11, clr); //月と日の間に「-」
//月
for (int i = 0; i < 2; i++)
{
if (i == 1 && mnum[i] == 0)
MyPanel.DispNum(7 - i, 0x10, clr); //空白
else
MyPanel.DispNum(7 - i, mnum[i], clr);
}
//日
for (int i = 0; i < 2; i++)
{
if (i == 1 && dnum[i] == 0)
MyPanel.DispNum(10 - i, 0x10, clr);
else
MyPanel.DispNum(10 - i, dnum[i], clr);
}
MyPanel.Show();
}
//------------------------------------
//時間表示 パネルごとに色を設定
//------------------------------------
void DispTime(struct tm *tm, uint32_t clr[])
{
static int presec;
static int startsec;
if (presec != tm->tm_sec)
{
startsec = millis();
}
presec = tm->tm_sec;
int8_t hnum[2];
int8_t mnum[2];
int8_t snum[2];
hnum[1] = tm->tm_hour / 10; //10の位
hnum[0] = tm->tm_hour % 10; //1の位
mnum[1] = tm->tm_min / 10;
mnum[0] = tm->tm_min % 10;
snum[1] = tm->tm_sec / 10;
snum[0] = tm->tm_sec % 10;
// Serial.printf("%d %d %d %d %d %d \n", hnum[1], hnum[0], mnum[1], mnum[0], snum[1], snum[0]);
for (int i = 0; i < 2; i++)
{
if (i == 1 && hnum[i] == 0) //10の位が0は表示しない
MyPanel.DispNum(i + 4, 0x10, 0);
else
MyPanel.DispNum(i + 4, hnum[i], clr[4 + i]);
}
for (int i = 0; i < 2; i++)
{
MyPanel.DispNum(i + 2, mnum[i], clr[2 + i]);
}
for (int i = 0; i < 2; i++)
{
MyPanel.DispNum(i, snum[i], clr[i]);
}
//時計のコロンを0.5sec毎に点滅
if (((float)millis() - (float)startsec) < 500)
{
MyPanel.DispColon(2, clr[2]);
MyPanel.DispColon(4, clr[4]);
}
else
{
MyPanel.DispColon(2, 0);
MyPanel.DispColon(4, 0);
}
MyPanel.Show();
delay(10);
}
//------------------------------------
//時間表示 全パネル同じ色
//------------------------------------
void DispTime(struct tm *tm, uint32_t clr)
{
uint32_t clrs[6];
for (int i = 0; i < sizeof(clrs) / sizeof(clrs[0]); i++)
clrs[i] = clr;
DispTime(tm, clrs);
}
//--------------------------------
// 気温・湿度表示
//--------------------------------
void DispTempHumid(float t, float h)
{
bool tempsign = (t >= 0); //符号
uint8_t dec_temp[6]; // [5][4]が温度[3]は'℃' [2][1]は湿度 [0]は%
dec_temp[3] = 20; //℃
dec_temp[0] = 21; //%
//温度
int temp = abs(round(t));
dec_temp[5] = temp / 10;
dec_temp[4] = temp % 10;
if (tempsign == false) //温度マイナス
dec_temp[5] = 0x11;
else if (dec_temp[5] == 0)
dec_temp[5] = 0x10;
//湿度
int humd = abs(round(h));
dec_temp[2] = humd / 10;
dec_temp[1] = humd % 10;
if (dec_temp[2] == 0)
dec_temp[2] = 0x10;
//不快指数
float thi = GetTempHumidDHT(t, h);
uint32_t clr = ConvertColorTHI(thi);
for (int i = 6; i < 12; i++)
MyPanel.DispNum(i, 0x10, 0); //空白
for (int pnl = 0; pnl < 6; pnl++)
{
MyPanel.DispNum(6 + pnl, dec_temp[5 - pnl], clr);
}
MyPanel.Show();
}
//--------------------------------
// 気温表示
//--------------------------------
void DispTemp(float t)
{
uint8_t dec_temp[4]; // [3][2].[1] [0]℃ -の場合は[3]が「-」で-9.9℃まで
dec_temp[0] = 20; //℃
//温度を10倍
int tempx10 = int(abs(t * 10.0));
for (int i = 1; i < 4; i++)
{
dec_temp[i] = tempx10 % 10;
tempx10 = tempx10 / 10;
}
//マイナス
if (t < 0)
{
dec_temp[3] = 0x11;
}
else
{
if (abs(t) < 10)
dec_temp[3] = 0x10;
}
//不快指数
float thi = GetTempHumidDHT(Temp, Humidity);
uint32_t clr = ConvertColorTHI(thi);
for (int i = 6; i < 12; i++)
MyPanel.DispNum(i, 0x10, 0); //空白
for (int pnl = 0; pnl < 4; pnl++)
{
MyPanel.DispNum(10 - pnl, dec_temp[pnl], clr);
}
//パネル2枚目に小数点
MyPanel.DispDot(10 - 2, clr);
MyPanel.Show();
}
//------------------------------------
// 湿度表示
//------------------------------------
void DispHumid(float h)
{
uint8_t dec_temp[4];
dec_temp[0] = 21; //%
//湿度を10倍
int tempx10 = int(h * 10.0);
for (int i = 1; i < 4; i++)
{
dec_temp[i] = tempx10 % 10;
tempx10 = tempx10 / 10;
}
//不快指数
float thi = GetTempHumidDHT(Temp, Humidity);
uint32_t clr = ConvertColorTHI(thi);
for (int i = 6; i < 12; i++)
MyPanel.DispNum(i, 0x10, 0); //空白
for (int pnl = 0; pnl < 4; pnl++)
{
if (pnl == 3 && dec_temp[pnl] == 0)
MyPanel.SetPanelColor(10 - pnl, 0);
else
MyPanel.DispNum(10 - pnl, dec_temp[pnl], clr);
}
//パネル2枚目に小数点
MyPanel.DispDot(10 - 2, clr);
MyPanel.Show();
}
//------------------------------------
//気圧表示
//------------------------------------
void DispPress(float p)
{
uint8_t dec_temp[6];
dec_temp[0] = 25; //P
dec_temp[1] = 22; //h
int tempx10 = p;
for (int i = 2; i < 6; i++)
{
dec_temp[i] = tempx10 % 10;
tempx10 = tempx10 / 10;
}
for (int i = 6; i < 12; i++)
MyPanel.DispNum(i, 0x10, 0); //空白
if (dec_temp[5] == 0)
dec_temp[5] = 0x10;
for (int pnl = 0; pnl < 6; pnl++)
{
MyPanel.DispNum(11 - pnl, dec_temp[pnl], 0xdfdfdf);
}
MyPanel.Show();
}
//-------------------------------------------------
//不快指数
//-------------------------------------------------
float GetTempHumidDHT(float t, float h)
{
float THI = 0.81 * t + 0.01 * h * (0.99 * t - 14.3) + 46.3;
#if 0
Serial.print("THI = ");
Serial.print(THI);
Serial.println();
#endif
return THI;
}
//-------------------------------------------------
//不快指数を色にする、0(青)-50(緑)-100(赤) 寒い0 暑い100
//-------------------------------------------------
uint32_t ConvertColorTHI(float thi)
{
uint32_t LEDColor = 0;
byte R;
byte G;
byte B;
if (thi > 85)
{
R = 0xff;
B = 0;
G = 0;
}
else if (thi < 55)
{
R = 0;
B = 0xff;
G = 0;
}
else if (thi > 67.5)
{
R = (thi - 67.5) / (85.0 - 67.5) * 0xff;
G = 0xff - R;
B = 0;
}
else if (thi <= 67.5)
{
B = (thi - 67.5) / (55.0 - 67.5) * 0xff;
G = 0xff - B;
R = 0;
}
// Serial.printf("%2X %2X %2X\n", R, G, B);
LEDColor = (R << 16) + (G << 8) + B;
#if 0
Serial.printf("%2X %2X %2X %8X\n", R, G, B, LEDColor);
#endif
return LEDColor;
}
###プログラム書き込み方法
1.制御基板のシリアルポート(J3)にUSBシリアル変換モジュールを接続します。モジュール側のTXは基板側のRX、RXはTX、GNDはGNDになるよう接続してください。
2.基板のUSB-Cコネクタから5Vを供給します。
3.基板上側のWriteONのスイッチを押したまま、RSTスイッチを押して離します。その後WriteONスイッチを離します。
4.ArduinoIDEのボードの設定、シリアルポートの設定をして[マイコンボートに書き込む]をクリックします。しばらくして完了のメッセージが表示されれば書き込み完了です。
5.RSTボタンを押すとプログラムが実行されます。
#参考リンク
GitHub WS2813-7segPanelライブラリー
YouTube RainClock2
WifiManagerの使い方
コントローラ付基板
コントローラ無し基板