ちょっとすごいと思わせるデジタルアートを作ってみた

この記事は [福島高専 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nitfc) 21日目の記事です。

# 1. はじめに
こんにちは、高専3年のAkkunlabです。主にプログラミングや電子工作などをしています。
この記事がQiita初投稿なので読みにくいところがあるかもしれませんが、ぜひ最後まで読んでいただけたら幸いです。

今回は、**インタラクティブアート**を制作していきます。

# 目次
[1. はじめに](#1-はじめに)
[2. インタラクティブアートとは](#2-インタラクティブアートとは)
[3. 制作するアート](#3-制作するアート)
[4. LEDユニット製作](#4-ledユニット製作)
[5. Firebase設定](#5-firebase設定)
[6. 制御パネル制作](#6-制御パネル制作)
[7. 完成したアート](#7-完成したアート)
[8. 開発環境](#8-開発環境)
[9. おわりに](#9-おわりに)
[10. 参考](#10-参考)


# 2. インタラクティブアートとは
皆さん、インタラクティブアートをご存知でしょうか。
**インタラクティブ**とは、「相互に作用する」、「双方向の」という意味があり、一方的に送信するだけでなく応答することも可能であるという意味です。つまり、インタラクティブアートとは観客がアートに影響を与えることのできる芸術作品であるといえます。

このようなアートを制作している方々はたくさんいらっしゃいますが、その中でも有名なのが[チームラボ株式会社](https://www.team-lab.com/)だと思います。チームラボは様々な分野のスペシャリストから構成され、デジタルアート制作からWebサイト制作、アプリ開発、製品開発などに至るまで様々なことを行っています。下の写真はお台場にある「[teamLab Borderless](https://borderless.teamlab.art/jp/)」に行った時のものです。
![IMG_6511.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/3520e951-e9f5-0ef3-8a86-90136ac85a93.jpeg)
![IMG_6506.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/43fa76da-678c-388c-be6a-1a096f9a14e8.jpeg)
![IMG_6509.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/56be595d-4317-9d80-cb05-789b6c531feb.jpeg)

# 3. 制作するアート
今回制作するものは、**スマホやPCからLEDを制御**し、様々な色に光らせることができるアートです。
LEDは複数個あり、**全て無線で通信**しています。そのため、直線状に並べたり、円状に並べたり、無作為に並べたり...とパターンは無限大です。たくさん作ればドット絵も描けるかもしれません（笑）

もちろん、**色を同期させたり**、**個別に設定したり**することができます。
下の写真が、完成したアートです。
![led1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/03abdb8b-f620-64ce-7f10-8a5a70512bb8.jpeg)
![led2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/74f4ebd6-ee27-8f00-f646-6abe62e7fa37.png)
![led3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/01c55573-5cfb-8149-d071-286686d02274.png)

## システム構成
今回のアートを作るにあたって、システムの構成は下の図のようにします。

①PCやスマホからWebサイトにアクセスし、Firebaseを操作します。
②Firebaseから変更したデータをマイコンに送信します。
③マイコンでLEDを制御します。
![p1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/7c867a49-2cd4-ffc7-4ca5-b5f57616bfc6.png)

さらに、マイコンの部分を詳しく見ると**２つの方法**で通信します。

1つ目は、マイコンを親機と子機に分け、親機が全ての子機にデータを送信する方法で、下の図のような構成となります。
ここでは、この方法を「**ブロードキャスト**」と呼びます。

④マイコン（親機）からマイコン（子機）にデータを送信します。
![p2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/5d071c48-0dd6-c5f6-aba2-57cdd1f0735f.png)

2つ目は、順番にマイコンから次のマイコンへ数珠つなぎにデータを送信する方法で、下の図のような構成となります。
ここでは、この方法を「**デイジーチェーン**」と呼びます。

⑤マイコンから次のマイコンにデータを送信します。
![p3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/b03b0d8e-fad3-debe-ad72-b006a646738a.png)


## 通信するデータの定義
今回通信に使用するデータを次のように定義します。
このデータを使用するのは、WebサイトからFirebaseへの通信、Firebaseからマイコンへの通信、マイコンからマイコンへの通信の**3箇所**です。
各配列の値の最小は0、最大は255となっています。

※倍率を設定する時のみ、このデータの定義は適用されません。

| 配列 | 名称 | 定義 | 使用範囲 |
|:--|:--|:--|:--|
| 0 | 分類 | どのデータか指定します | 0 - 100 |
| 1 | ユニット番号 | どのユニットか指定します | 1 - 200 |
| 2 | LED（赤） | 赤色LEDの出力を指定します | 0 - 255 |
| 3 | LED（緑） | 緑色LEDの出力を指定します | 0 - 255 |
| 4 | LED（青） | 青色LEDの出力を指定します | 0 - 255 |
| 5 | 遅延  | 次のユニットに送信するまでの時間を指定します | 0 - 255 |

（例）`[0,1,255,255,255,0]` → ユニット番号1のLEDを最大出力で点灯

# 4. LEDユニット製作
はじめにLEDユニットを制作してきます。
ここでは、マイコン、LED、電源などを含めてそれ自体で発光するものを**ユニット**と呼んでいます。

## 使用部品・製品
下の表にあるのが**ユニット1個**を制作に必要な部品で、ほとんどは[秋月電子](https://akizukidenshi.com/)で購入しました。

| 部品 | 用途 | 数 |
|:--|:--|:--|
| １ＷフルカラーＲＧＢＬＥＤ | 光らせる | 1 |
| トランジスタ　２ＳＤ４６８Ｌ |  LED電流増幅 | 3 |
| カーボン抵抗　１／４Ｗ　１０ｋΩ | マイコン電流制限抵抗 | 3 |
| カーボン抵抗　１／４Ｗ　３．９ｋΩ | マイコン電流制限抵抗 | 3 |
| カーボン抵抗　１Ｗ　５．１Ω | LED電流制限抵抗（G、B用） | 2 |
| カーボン抵抗　１Ｗ　１０Ω | LED電流制限抵抗（R用） | 1 |
| ＥＳＰ－ＷＲＯＯＭ－０２ | マイコン（LED制御、通信用） | 1 |
| 分割ロングピンソケット | マイコンを挿す |  16 |
| ユニバーサル基板　Ｃタイプ | 回路作成 |  1 |
| 低損失ＣＭＯＳ三端子レギュレータ | 電源電圧降圧 |  1 |
| 電解コンデンサ　１００μＦ | レギュレータ用コンデンサ |  1 |
| セラミックコンデンサ　０．１μＦ | レギュレータ用コンデンサ |  1 |
| 電池ボックス　単３×３本 | 電源用ボックス | 1 |
| プラスチックケース　ＳＷ－８５Ｂ | ユニットを入れるケース | 1 |

何を思ったのか当時の僕は一気に**ユニットを50個**作ろうとか考えていたので、50個分の部品を購入しました。
![EHKRyR-U0AEPF6E.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/214fb253-ddf0-1264-7934-63c8322ef560.jpeg)

## 回路作成
回路を作成していきます。
以前に**ESP-WROOM-02の使い方及び回路作成方法**についての記事を書いたので、そちらを参考にしてください。
[とりあえず使ってみる　ESP-WROOM-02 (ESP8266)](https://akkunlab.hateblo.jp/entry/2019/12/22/123139)

## 書き込み
ESP-WROOM-02にプログラムを書き込んでいきます。
書き込むプログラムは、**親機用**と**子機用**の２つがあります。

### 親機用プログラムの書き込み
はじめに、親機用のプログラムを書き込みます。
ここでは、プログラムの説明については省略します。申し訳ありません。（時間がある時に書きます...）

コード全体を表示したい場合は、下のサンプルコードをクリックしてください。

<details><summary>**サンプルコード (mother.ino)**</summary><div>

```c:mother.ino
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiClientSecure.h>
extern "C" {
  #include <espnow.h>
  #include <user_interface.h>
}

/* 基本設定 */
uint8_t mac[] = {****,****,****,****,****,****};      // 子機 MAC
const char *AP_ssid = "********************";         // Wi-Fi_AP SSID
const char *AP_password = "********************";     // Wi-Fi_AP Password
const char *STA_ssid = "********************";        // Wi-Fi_STA SSID
const char *STA_password = "********************";    // Wi-Fi_STA Password
const char *host = "********************";            // Firebase ホスト
const char *firebase_auth = "********************";   // Firebase 認証
String firebase_path = "data/LED/";                   // Firebase パス
 
BearSSL::WiFiClientSecure client;

/* 初期化 */
void setup() {

  // Wi-Fi
  Serial.begin(115200);
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(STA_ssid, STA_password);
  Serial.print("\n[setup]connecting");
  
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(1000);
  }
  
  Serial.print("\n[setup]connected Wi-Fi:");
  Serial.println(WiFi.localIP());
  delay(1000);

  WiFi.softAP(AP_ssid, AP_password);
  Serial.println("[setup]AP started");
  delay(1000);
  
  // MAC
  uint8_t macaddr[6];
  wifi_get_macaddr(STATION_IF, macaddr);       // STATION MAC
  Serial.print("[setup]MAC(STATION_IF):");
  printMacAddress(macaddr);
  
  wifi_get_macaddr(SOFTAP_IF, macaddr);        // SOFT_AP MAC
  Serial.print("[setup]MAC(SOFTAP_IF):");
  printMacAddress(macaddr);
  delay(1000);
  
  ESPNowSetup();                               // ESPNow初期化
  getServerSentEvents();                       // Firebase初期化

}

/* ESPNow初期化 */
void ESPNowSetup() {

  if (esp_now_init() == 0) {
    Serial.println("[setup]ESPNow started");
  } else {
    Serial.println("[setup]ESPNow failed");
    ESP.restart();
    return;
  }

  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);                   // 自分の役割                                                    // 自分の役割
  esp_now_add_peer(mac, (uint8_t)ESP_NOW_ROLE_SLAVE, 1, NULL, 0);   // デバイス追加

}

/* Firebase初期化 */
void getServerSentEvents() {
  
  client.setInsecure();

  if (client.connect( host, 443 )) {
    Serial.println("[setup]Firebase started");

    String req_url = "GET /"
    + firebase_path + ".json?auth="
    + String(firebase_auth) + " HTTP/1.1\r\n";
 
    String req_header = "Host: "
    + String(host) + "\r\n"
    + "Accept: text/event-stream\r\n"
    + "Connection: close\r\n"
    + "\r\n";

    client.print(req_url);
    client.print(req_header);
  } else {
    Serial.println("[setup]Firebase failed");
  }
  
}

void loop() {

  checkServerRespons();               // Firebase応答

}

/* Firebase受信 */
void checkServerRespons() {

  String data;
  boolean running = false;
  
  while(client.available()) {
    
    char c = client.read();

    // 抽出
    if (c == ']') {
      running = !running;
      ESPNow_send(data);              // ESPNow送信
      data = "";
    } else if (running == true) {
      data += c;
    } else if (c == '[') {
      running = !running;
    }
    
  }
  
}

/* ESPNow送信 */
void ESPNow_send(String data) {

  int data_length = 6;                // dataの長さ
  uint8_t data_array[data_length];    // data配列
  int x = 0;                          // data変換変数
  int y = data.indexOf(",", 0);       // data変換変数

  // 文字列 -> 配列
  for (int i = 0; i < data_length; i++) {
    data_array[i] = data.substring(x,y).toInt();
    x = y + 1;
    y = data.indexOf(",", x);
  }
  
  esp_now_send(mac, data_array, sizeof(data_array));  // ESPNow送信
  Serial.print("[ESPNow]Sent ");                      // ログ出力
  Serial.println(data);                               // ログ出力

}

/* MAC出力 */
void printMacAddress(uint8_t* macaddr) {

  Serial.print("{");
  for (int i = 0; i < 6; i++) {
    Serial.print("0x");
    Serial.print(macaddr[i], HEX);
    if (i < 5) Serial.print(',');
  }
  Serial.println("}");

}
```

</div></details>

### 子機用プログラムの書き込み
次に、子機用のプログラムを書き込みます。
ここでは、プログラムの説明については省略します。申し訳ありません。（時間がある時に書きます...）

コード全体を表示したい場合は、下のサンプルコードをクリックしてください。

<details><summary>**サンプルコード (child.ino)**</summary><div>

```c:child.ino
#include <Arduino.h>
#include <ESP8266WiFi.h>
extern "C" {
  #include <espnow.h>
  #include <user_interface.h>
}

/* 基本設定 */
const int UnitNumber = 1;                            // 個体番号
uint8_t message[6];                                  // 送受信メッセージ
uint8_t mac[] = {****,****,****,****,****,****};     // 親機 MAC
uint8_t mac_2[] = {****,****,****,****,****,****};   // 子機 MAC
uint8_t mac_b[] = {****,****,****,****,****,****};   // ブロードキャスト MAC(FF)
const char *AP_ssid = "********************";        // Wi-Fi_AP SSID
const char *AP_password = "********************";    // Wi-Fi_AP Password
const int LED_R = 12;                                // 赤色LED Pin番号
const int LED_G = 14;                                // 緑色LED Pin番号
const int LED_B = 13;                                // 青色LED Pin番号
uint8_t LED_Data[3];                                 // 現在のLED出力(0-255)(赤,緑,青)
float mag[] = {1, 1, 1};                             // LED出力倍率(赤,緑,青)

/* 関数 */
bool f_send_Data = false;                            // データ送信(ESPNow)
bool f_RB1 = false;                                  // グラデーション(Rainbow)

/* 初期化 */
void setup() {
  
  // Wi-Fi
  Serial.begin(115200);
  WiFi.mode(WIFI_AP);
  WiFi.softAP(AP_ssid, AP_password, 1, true);
  Serial.println("\n[setup]AP started");
  delay(1000);
  
  // LED
  analogWriteRange(255);
  pinMode(LED_R, OUTPUT);
  pinMode(LED_G, OUTPUT);
  pinMode(LED_B, OUTPUT);
  
  // MAC
  uint8_t macaddr[6];
  wifi_get_macaddr(STATION_IF, macaddr);             // STATION MAC
  Serial.print("[setup]MAC(STATION_IF):");
  printMacAddress(macaddr);
  
  wifi_get_macaddr(SOFTAP_IF, macaddr);              // SOFT_AP MAC
  Serial.print("[setup]MAC(SOFTAP_IF):");
  printMacAddress(macaddr);

  ESPNowSetup();                                     // ESPNow初期化

}

/* ESPNow初期化 */
void ESPNowSetup() {

  if (esp_now_init() == 0) {
    Serial.println("[setup]ESPNow started");
  } else {
    Serial.println("[setup]ESPNow failed");
    ESP.restart();
    return;
  }
  
  esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);                              // 自分の役割
  esp_now_add_peer(mac, (uint8_t)ESP_NOW_ROLE_CONTROLLER, 1, NULL, 0);    // デバイス追加 (親機)
  esp_now_add_peer(mac_2, (uint8_t)ESP_NOW_ROLE_CONTROLLER, 1, NULL, 0);  // デバイス追加 (子機)

  // 受信時
  esp_now_register_recv_cb([](uint8_t *macaddr, uint8_t *data, uint8_t len) {

    Serial.print("[ESONOW]Got ");
    for (int i = 0; i < 6; i++) {
      message[i] = data[i];
      Serial.print(message[i]);
      Serial.print(",");
    }
    Serial.println("");

    //モード1
    switch (message[0]) {
      case 0:   setLED(message); break;               // 通常LED設定
      case 1:   setMag(message); break;               // 倍率設定
      case 2:   setRandom(message); break;            // ランダム設定
      case 100: f_RB1 = true; break;                  // グラデーション(Rainbow)
    }
    
  });
  
}

void loop() {

  if (f_send_Data == true) send_Data();                        // データ送信(ESPNow)
  if (f_RB1 == true) RB1();                                    // グラデーション(Rainbow)
  
}

/* LED設定 */
void setLED(uint8_t *data) {
  switch (data[1]) {
    case UnitNumber:                                           // 個体番号
      setLED_RGB(data);
      break;
    case 100:                                                  // デイジーチェーン
      f_send_Data = true;
      setLED_RGB(data);
      break;
    case 200:                                                  // ブロードキャスト
      esp_now_send(mac_b, message, sizeof(message));
      Serial.println("[ESPNow]Sent data");
      setLED_RGB(data);
      break;
    default:                                                   // 通過
      esp_now_send(mac_2, message, sizeof(message));
      Serial.println("[ESPNow]Sent data");
      break;
  }
}

/* LED三色遅延設定 */
void setLED_RGB(uint8_t *data) {

  int count, i, j;
  int color[3] = {LED_R, LED_G, LED_B};

  for (i = 0; i < 3; i++) {
    count = LED_Data[i] - data[i + 2];
    if (count < 0) {
      for (j = 0; j <= abs(count); j++) {
        aw(color[i], LED_Data[i]++);
        delayMicroseconds(1000);
      }
    } else {
      for (j = 0; j <= abs(count); j++) {
        aw(color[i], LED_Data[i]--);
        delayMicroseconds(1000);
      }
    }
    
    LED_Data[i] = data[i + 2];
  }
}

/* 倍率設定 */
void setMag(uint8_t *data) {
  if (data[1] == UnitNumber) {
    mag[0] = data[2] / 10.0;
    mag[1] = data[3] / 10.0;
    mag[2] = data[4] / 10.0;
    Serial.println("[setting]Set magnification");
  } else {
    if (message[1] == 100) esp_now_send(mac_2, message, sizeof(message));  // デイジーチェーン
    else esp_now_send(mac_b, message, sizeof(message));                    // ブロードキャスト
    Serial.println("[ESPNow]Sent data");
  }
}

/* ランダム設定 */
void setRandom(uint8_t *data) {
  if (message[1] == 100) esp_now_send(mac_2, message, sizeof(message));    // デイジーチェーン
  else esp_now_send(mac_b, message, sizeof(message));                      // ブロードキャスト
  Serial.println("[ESPNow]Sent data");
  data[2] = random(1,256);
  data[3] = random(1,256);
  data[4] = random(1,256);
  setLED_RGB(data);
}

/* データ送信(ESPNow) */
void send_Data() {
  delay(message[5] * 10);
  esp_now_send(mac_2, message, sizeof(message));
  Serial.println("[ESPNow]Sent data");
  f_send_Data = false;
}

/* グラデーション(Rainbow) */
void RB1() {

  if (message[1] == 100) esp_now_send(mac_2, message, sizeof(message));  // デイジーチェーン
  else esp_now_send(mac_b, message, sizeof(message));                    // ブロードキャスト
  Serial.println("[ESPNow]Sent data");

  int i;
  Serial.println("[macro]GD_Rainbow");
  aw(LED_R, 255);
  aw(LED_G, 0);
  aw(LED_B, 0);
  
  for (i = 0; i <= 255; i++) {
    aw(LED_G, i);
    delay(20);
  }
  for (i = 0; i <= 255; i++) {
    aw(LED_R, 255 - i);
    delay(20);
  }
  for (i = 0; i <= 255; i++) {
    aw(LED_B, i);
    delay(20);
  }
  for (i = 0; i <= 255; i++) {
    aw(LED_G, 255 - i);
    delay(20);
  }
  for (i = 0; i <= 255; i++) {
    aw(LED_R, i);
    delay(20);
  }
  for (i = 0; i <= 255; i++) {
    aw(LED_B, 255 - i);
    delay(20);
  }
  for (i = 0; i <= 255; i++) {
    aw(LED_R, 255 - i);
    delay(20);
  }
  
  f_RB1 = false;
}

/* analogWrite設定(倍率掛け) */
void aw(int color, int value) {
  switch (color) {
    case LED_R: analogWrite(color, int(value * mag[0]) ); break;
    case LED_G: analogWrite(color, int(value * mag[1]) ); break;
    case LED_B: analogWrite(color, int(value * mag[2]) ); break;
  }
}

/* MAC出力 */
void printMacAddress(uint8_t* macaddr) {
  Serial.print("{");
  for (int i = 0; i < 6; i++) {
    Serial.print("0x");
    Serial.print(macaddr[i], HEX);
    if (i < 5) Serial.print(',');
  }
  Serial.println("}");
}
```

</div></details>

# 5. Firebase設定
[Firebase](https://firebase.google.com/)とは、Googleが提供している**バックエンドサービス**で、バックエンド初心者でも簡単に作ることができます。今回はFirebaseの「**Realtime Database**」という機能を利用していきます。これは一種のデータベースで、接続しているクライアントにリアルタイムでデータを送信し同期させる事が可能です。この機能を使うことで、Webサイトからデータベースを操作し、データベースからマイコンを制御し、LEDを制御することが可能になります。

## Firebase登録
ここでは、**Firebaseの登録、使い方**については省略します。
登録、使い方を知りたい方は、とても分かりやすい記事があるのでそちらをご覧ください。
[Firebaseの始め方](https://qiita.com/kohashi/items/43ea22f61ade45972881)

## データの追加
Realtime Databaseにデータを追加してきます。
はじめに、下のように「data」、「LED」、「magnification」という階層を追加し、「LED」内に「value」というオブジェクトを追加します。
この時のvalueの値は何でも良いのですが、ここでは`"[0,0,0,0,0,0]"`としておきます。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/940f0d30-5b8d-d8ee-4dcb-f548c65f5afb.png)

magnificationの中には下のように**各ユニットの倍率**を設定しときます。ユニットの倍率とは**各LEDの出力倍率**のことで、倍率が0の時LEDは光らず、倍率が10の時LEDは最も強く光ります。本来なら倍率は0から1までの間で調整したかったのですが、値に小数は入れられませんので、10倍して0から10まので間で調整することにします。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/859cb2ca-da75-95ec-b70d-95c6a56f8535.png)




# 6. 制御パネル制作
次に制御パネルを制作していきます。
スマホやPCなどの様々なデバイスから制御できるようにしたいので、**Webサイト**を作り、そこからFirebaseと通信して制御する感じです。

## HTMLファイルの作成
HTMLファイルの作成をしてきます。
今回のWebサイトでは、色々と要素を追加したり変化させたりしたいのでメインはJavaScriptのファイルに書いていきます。

```html:index.html
<div id="settings">
    <h2>全体設定</h2>
    <button type="button" id="units_update" onclick="updateUnits()">更新</button>
    <label for="units">ユニット数</label>
    <input type="number" id="units" value="1" min="1" max="50">
    <label for="method">通信方式</label>
    <select id="method" name="method">
        <option value="200">ブロードキャスト</option>
        <option value="100">デイジーチェーン</option>
    </select>
    <label for="delay">遅延</label>
    <input type="number" id="delay" value="0" min="0" step="100" max="2500">
</div>
```

全体設定のdivを追加し、中にはいくつかのbuttonとselect、inputを追加します。
3行目のidが`units_update`であるbuttonでは、クリックされた時に`updateUnits`関数を実行しています。

```html:index.html
<div id="controlPanel">
    <h2>コントロールパネル</h2>
    <div id="control_all">
        <button type="button" id="macro_RB1">グラデーション(Rainbow)</button>
        <button type="button" id="macro_random">ランダム</button>
    </div>
    <div id="control_part"></div>
</div>
```

さらに、コントロールパネルのdivを追加し、中にはいくつかのbuttonを追加します。

コード全体を表示したい場合は、下のサンプルコードをクリックしてください。

<details><summary>**サンプルコード (index.html)**</summary><div>

```html:index.html
<!DOCTYPE html>
<html lang="ja"></html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>LED Controller</title>
    <link href="./assets/css/style.css" rel="stylesheet">
</head>

<body>

    <div class="wrap">

        <!-- ▼ ヘッダー ▼ -->
        <header>
            <div class="logo"><h1>LED Controller</h1></div>
        </header>

        <!-- ▼ メイン ▼ -->
        <main>
            <div id="settings">
                <h2>全体設定</h2>
                <button type="button" id="units_update" onclick="updateUnits()">更新</button>
                <label for="units">ユニット数</label>
                <input type="number" id="units" value="1" min="1" max="50">
                <label for="method">通信方式</label>
                <select id="method" name="method">
                    <option value="200">ブロードキャスト</option>
                    <option value="100">デイジーチェーン</option>
                </select>
                <label for="delay">遅延</label>
                <input type="number" id="delay" value="0" min="0" step="100" max="2500">
            </div>
            
            <!-- ▼ コントロールパネル ▼ -->
            <div id="controlPanel">
                <h2>コントロールパネル</h2>
                <div id="control_all">
                    <button type="button" id="macro_RB1">グラデーション(Rainbow)</button>
                    <button type="button" id="macro_random">ランダム</button>
                </div>
                <div id="control_part"></div>
            </div>

        </main>

    </div>

    <!-- js -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.2.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.2.1/firebase-database.js"></script>
    <script src="./assets/js/main.js"></script>

</body>

</html>
```

</div></details>

## CSSファイルの作成
CSSファイルの作成をしてきます。

```css:style.css
/* 全体設定 */
#settings {
    margin: 10px 0;
}
#settings label {
    margin-left: 20px;
}
#settings input {
    width: 60px;
    padding: 10px 10px;
    font-size: 16px;
    border-radius: 3px;
    border: 2px solid rgb(195, 195, 195);
}
#settings select {
    width: 180px;
    padding: 10px 10px;
    font-size: 16px;
    border-radius: 3px;
    border: 2px solid rgb(195, 195, 195);
}
```

全体設定のCSSでは、HTMLの全体設定divについて装飾を行います。

```css:style.css
/* コントロールパネル */
#control_all {
    margin: 10px 0;
}
#control_part {
    display: flex;
    flex-wrap : wrap;
}
#control_part div[id*="Unit"] {
    padding: 5px;
    width: calc((100% / 6) - 10px);
    color: white;
    background: #000;
}
#control_part h3 {
    padding: 5px;
    text-shadow: 2px 2px 2px #222;
}
#control_part span {
    width: 15px;
    display: inline-block;
    text-shadow: 2px 2px 2px #000;
}
#control_part input[type="number"] {
    width: 40px;
}
#control_part div[id="Unit0"] input[id*="mag"] {
    display: none;
}
```

コントロールパネルのCSSでは、HTMLのコントロールパネルdivについて装飾を行います。
特に、Unit0の一部を**非表示にする**など設定しています。（Unit0は全てのユニットをコントロールします。）

コード全体を表示したい場合は、下のサンプルコードをクリックしてください。

<details><summary>**サンプルコード (style.css)**</summary><div>

```css:style.css
/*------------------------------------
  Default Style
------------------------------------*/
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@700&family=Noto+Sans+JP:wght@500&family=Roboto&family=Kanit:wght@300&family=Noto+Sans+JP&display=swap');

* { margin: 0; padding: 0; }
html {
    font-family: 'Roboto', 'Noto Sans JP', sans-serif;
    font-size: 62.5%;
    animation: fadeIn 2s ease 0s 1 normal;
}
body {
    font-size: 1.6em;
}
h2 {
    font-size: 2rem;
    margin: 20px 0 10px;
}
button {
    margin: 0 5px;
    padding: 10px 20px;
    font-weight: bold;
    color: #fff;
    border: 2px solid rgb(0, 81, 255);
    border-radius: 3px;
    background: linear-gradient(#256aff 0%, #67a9ff 100%);
    outline: none;
}
button:active {
    -webkit-transform: translateY(2px);
    transform: translateY(2px);
    background: linear-gradient(#4a83ff 0%, #99c4fb 100%);
}

@keyframes fadeIn {
    0% {opacity: 0}
    100% {opacity: 1}
}

/*------------------------------------
  Header
------------------------------------*/
header {
    width: 100%;
}
.logo h1 {
    margin: 20px;
    font-family: 'Orbitron', sans-serif;
}

/*------------------------------------
  main
------------------------------------*/
main {
    margin: 10px;
}

/* 全体設定 */
#settings {
    margin: 10px 0;
}
#settings label {
    margin-left: 20px;
}
#settings input {
    width: 60px;
    padding: 10px 10px;
    font-size: 16px;
    border-radius: 3px;
    border: 2px solid rgb(195, 195, 195);
}
#settings select {
    width: 180px;
    padding: 10px 10px;
    font-size: 16px;
    border-radius: 3px;
    border: 2px solid rgb(195, 195, 195);
}

/* コントロールパネル */
#control_all {
    margin: 10px 0;
}
#control_part {
    display: flex;
    flex-wrap : wrap;
}
#control_part div[id*="Unit"] {
    padding: 5px;
    width: calc((100% / 6) - 10px);
    color: white;
    background: #000;
}
#control_part h3 {
    padding: 5px;
    text-shadow: 2px 2px 2px #222;
}
#control_part span {
    width: 15px;
    display: inline-block;
    text-shadow: 2px 2px 2px #000;
}
#control_part input[type="number"] {
    width: 40px;
}
#control_part div[id="Unit0"] input[id*="mag"] {
    display: none;
}

@media screen and (max-width: 1600px) {
    #control_part div[id*="Unit"] {
        width: calc((100% / 5) - 10px);
    }
}
@media screen and (max-width: 1300px) {
    #control_part div[id*="Unit"] {
        width: calc((100% / 4) - 10px);
    }
}
@media screen and (max-width: 1100px) {
    #control_part div[id*="Unit"] {
        width: calc((100% / 3) - 10px);
    }
}
@media screen and (max-width: 800px) {
    #control_part div[id*="Unit"] {
        width: calc((100% / 2) - 10px);
    }
}
@media screen and (max-width: 600px) {
    #control_part div[id*="Unit"] {
        width: calc(100% - 10px);
    }
}
```

</div></details>

## Javascriptファイルの作成
Javascriptファイルの作成をしてきます。
ここで、ユニットの追加、削除、Firebaseの更新などを行います。

はじめに、Firebaseの設定を書いていきます。

```javascript:main.js
/* Firebase */
const firebaseConfig = {
    apiKey: '********************',
    authDomain: '********************',
    databaseURL: '********************',
    projectId: '********************',
    storageBucket: '********************',
    messagingSenderId: '********************',
    appId: '********************',
    measurementId: '********************'
};
firebase.initializeApp(firebaseConfig);
const db = firebase.database();
```

`***`のところは、自分の環境に合わせて変更してください。

次に、基本設定と初期化関数を書いていきます。

```javascript:main.js
/* 基本設定 */
const maxUnits = 50;
let methodId = 200;  // ブロードキャスト
let delay = 0;
let units = 1;
let colorData = new Array(6);

init();

/* 初期化 */
function init() {
    let item;

    colorData.fill(0);
    $('#control_part').html('');

    for (let i = 0; i <= units; i++) {
        item =
        `<div id="Unit${i}"><h3>Unit ${i}</h3>
            <div><span>R</span><input id="${i}_R_out1" type="number" value="0" min="0" max="255" onClick="setColor(${i}, 'R', this.value)"><input id="${i}_R_out2" type="range" value="0" min="0" max="255" step="1" onClick="setColor(${i}, 'R', this.value)"><input id="${i}_R_mag" type="number" value="1" min="0" max="1" step="0.1" onclick="magnification(${i})"></div>
            <div><span>G</span><input id="${i}_G_out1" type="number" value="0" min="0" max="255" onClick="setColor(${i}, 'G', this.value)"><input id="${i}_G_out2" type="range" value="0" min="0" max="255" step="1" onClick="setColor(${i}, 'G', this.value)"><input id="${i}_G_mag" type="number" value="1" min="0" max="1" step="0.1" onclick="magnification(${i})"></div>
            <div><span>B</span><input id="${i}_B_out1" type="number" value="0" min="0" max="255" onClick="setColor(${i}, 'B', this.value)"><input id="${i}_B_out2" type="range" value="0" min="0" max="255" step="1" onClick="setColor(${i}, 'B', this.value)"><input id="${i}_B_mag" type="number" value="1" min="0" max="1" step="0.1" onclick="magnification(${i})"></div>
        </div>`;
        document.getElementById('control_part').insertAdjacentHTML('beforeend', item);
    }

    initMag();
}


/* 倍率初期化 */
function initMag() {
    let ary;

    db.ref('/data/LED/magnification/').once('value', snapshot => {                          // Firebase取得(更新時)

        for (let i = 1; i <= units; i++) {
            ary = snapshot.val()[i].replace('[', '').replace(']', '').split(',');           // 文字列 -> 配列
            $(`input[id='${i}_R_mag']`).val(ary[2] / 10);                                   // 値設定(R)
            $(`input[id='${i}_G_mag']`).val(ary[3] / 10);                                   // 値設定(G)
            $(`input[id='${i}_B_mag']`).val(ary[4] / 10);                                   // 値設定(B)
            db.ref('/data/LED/magnification/').update({ [i]: `[${[0, 0, 0, 0, 0, 0]}]` });  // Firebase更新
            db.ref('/data/LED/magnification/').update({ [i]: `[${ary}]` });                 // Firebase更新
        }

    });

}
```

2行目の`maxUnits`は最大のユニット数であり、ここでは`50`としておきます。
3行目の`methodId`はユニットにデータを送信する際の通信方法で`100`が一つずつ数珠繋ぎのように通信する**デイジーチェーン**、`200`が親機が全ての子機にデータを飛ばす**ブロードキャスト**となります。
4行目の`delay`は通信するデータの定義で説明した遅延時間で、5行目の`units`は現在のユニット数、6行目の`colorData`は送信するデータとなります。

関数`init()`は、データを初期化し、ユニットを生成する関数です。
関数`initMag()`は、Firebaseから各ユニットのLED倍率を取得し、適用させる関数です。

次に、ユニット数の設定について書いていきます。

```javascript:main.js
/* ユニット数設定 */
$('#units').keypress(e => { if (e.which == 13) updateUnits() });   // Enterキーで動作
function updateUnits() {
    const currentUnits = $('#units').val();
    delay = Math.floor($('#delay').val() / 10);
    methodId = $('#method').val();

    if (0 < currentUnits && currentUnits <= maxUnits) {
        units = currentUnits;
        init();
    } else {
        alert('エラー | 有効な数値ではありません');
    }
};
```

2行目では、Enterキーを押すことで、`updateUnits()`を実行できるようにしています。

関数`updateUnits()`は、設定したユニットの個数、通信方式、遅延などを更新する関数です。

次に、各ユニットのLEDの色設定について書いていきます。

```javascript:main.js
/* 色設定 */
function setColor(units_number, color, value) {
    colorData.fill(0);
    $(`input[id^=${units_number}_${color}_out]`).val(Number(value));                                            // 変更した値を適用
    colorData[1] = units_number ? units_number : methodId;                                                      // 個体番号(0 -> methodId)
    colorData[2] = Number($(`input[id='${units_number}_R_out1']`).val());                                       // 値を取得,代入(R)
    colorData[3] = Number($(`input[id='${units_number}_G_out1']`).val());                                       // 値を取得,代入(G)
    colorData[4] = Number($(`input[id='${units_number}_B_out1']`).val());                                       // 値を取得,代入(B)
    colorData[5] = units_number || methodId == 200 ? 0 : delay;                                                 // 遅延
    db.ref('/data/LED/').update({ value: `[${colorData}]`});                                                    // Firebase更新

    if (units_number) {
        $(`#Unit${units_number}`).css('background', `rgb(${colorData[2]},${colorData[3]},${colorData[4]})`);    // 背景色
    } else {
        $('input[id*="R_out"]').val(colorData[2]);                                                              // 値設定(R)
        $('input[id*="G_out"]').val(colorData[3]);                                                              // 値設定(G)
        $('input[id*="B_out"]').val(colorData[4]);                                                              // 値設定(B)
        $('div[id^="Unit"]').css('background', `rgb(${colorData[2]},${colorData[3]},${colorData[4]})`);         // 背景色(全て)
    }
}
```

関数`setColor()`は、ユニットのLEDの色を設定したら、Firebaseを更新しWeb上でユニットの背景色を設定した色に変化させる関数です。

次に、各ユニットのLEDの倍率設定について書いていきます。

```javascript:main.js
/* 倍率設定 */
function magnification(units_number) {
    let data = [1, 0, 0, 0, 0, 0];

    data[1] = units_number;
    data[2] = Number($(`input[id='${units_number}_R_mag']`).val()) * 10;         // 値を取得,代入(R)
    data[3] = Number($(`input[id='${units_number}_G_mag']`).val()) * 10;         // 値を取得,代入(G)
    data[4] = Number($(`input[id='${units_number}_B_mag']`).val()) * 10;         // 値を取得,代入(B)
    db.ref('/data/LED/magnification/').update({ [units_number]: `[${data}]`});   // Firebase更新
}
```

関数`magnification()`は、ユニットのLEDの倍率を設定したら、Firebaseを更新する関数です。

次に、マクロ設定について書いていきます。

```javascript:main.js
/*　マクロ設定 */
// グラデーション(Rainbow)
$('#macro_RB1').click(() => { macro(100, 0); });

// ランダム
$('#macro_random').click(() => { macro(2, 0); });

function macro(mode, delay) {
    colorData = [mode, methodId, 0, 0, 0, delay];
    db.ref('/data/LED/').update({ value: `[${colorData}]` });     // Firebase更新
    colorData.fill(0);
    setTimeout(() => {
        db.ref('/data/LED/').update({ value: `[${colorData}]`});  // Firebase更新
    }, 1000);
}
```

4行目と7行目では、全体設定の中にあるボタンを押すと`macro()`を実行できるようにしています。

関数`macro()`は、ボタンを押したら、Firebaseを更新し、事前に登録してある動作をマイコンにさせる関数です。

コード全体を表示したい場合は、下のサンプルコードをクリックしてください。

<details><summary>**サンプルコード (main.js)**</summary><div>

```javascript:main.js
/* Firebase */
const firebaseConfig = {
    apiKey: '********************',
    authDomain: '********************',
    databaseURL: '********************',
    projectId: '********************',
    storageBucket: '********************',
    messagingSenderId: '********************',
    appId: '********************',
    measurementId: '********************'
};
firebase.initializeApp(firebaseConfig);
const db = firebase.database();


/* 基本設定 */
const maxUnits = 50;
let methodId = 200;  // ブロードキャスト
let delay = 0;
let units = 1;
let colorData = new Array(6);

init();


/* 初期化 */
function init() {
    let item;

    colorData.fill(0);
    $('#control_part').html('');

    for (let i = 0; i <= units; i++) {
        item =
        `<div id="Unit${i}"><h3>Unit ${i}</h3>
            <div><span>R</span><input id="${i}_R_out1" type="number" value="0" min="0" max="255" onClick="setColor(${i}, 'R', this.value)"><input id="${i}_R_out2" type="range" value="0" min="0" max="255" step="1" onClick="setColor(${i}, 'R', this.value)"><input id="${i}_R_mag" type="number" value="1" min="0" max="1" step="0.1" onclick="magnification(${i})"></div>
            <div><span>G</span><input id="${i}_G_out1" type="number" value="0" min="0" max="255" onClick="setColor(${i}, 'G', this.value)"><input id="${i}_G_out2" type="range" value="0" min="0" max="255" step="1" onClick="setColor(${i}, 'G', this.value)"><input id="${i}_G_mag" type="number" value="1" min="0" max="1" step="0.1" onclick="magnification(${i})"></div>
            <div><span>B</span><input id="${i}_B_out1" type="number" value="0" min="0" max="255" onClick="setColor(${i}, 'B', this.value)"><input id="${i}_B_out2" type="range" value="0" min="0" max="255" step="1" onClick="setColor(${i}, 'B', this.value)"><input id="${i}_B_mag" type="number" value="1" min="0" max="1" step="0.1" onclick="magnification(${i})"></div>
        </div>`;
        document.getElementById('control_part').insertAdjacentHTML('beforeend', item);
    }

    initMag();
}


/* 倍率初期化 */
function initMag() {
    let ary;

    db.ref('/data/LED/magnification/').once('value', snapshot => {                          // Firebase取得(更新時)

        for (let i = 1; i <= units; i++) {
            ary = snapshot.val()[i].replace('[', '').replace(']', '').split(',');           // 文字列 -> 配列
            $(`input[id='${i}_R_mag']`).val(ary[2] / 10);                                   // 値設定(R)
            $(`input[id='${i}_G_mag']`).val(ary[3] / 10);                                   // 値設定(G)
            $(`input[id='${i}_B_mag']`).val(ary[4] / 10);                                   // 値設定(B)
            db.ref('/data/LED/magnification/').update({ [i]: `[${[0, 0, 0, 0, 0, 0]}]` });  // Firebase更新
            db.ref('/data/LED/magnification/').update({ [i]: `[${ary}]` });                 // Firebase更新
        }

    });

}


/* ユニット数設定 */
$('#units').keypress(e => { if (e.which == 13) updateUnits() });   // Enterキーで動作
function updateUnits() {
    const currentUnits = $('#units').val();
    delay = Math.floor($('#delay').val() / 10);
    methodId = $('#method').val();

    if (0 < currentUnits && currentUnits <= maxUnits) {
        units = currentUnits;
        init();
    } else {
        alert('エラー | 有効な数値ではありません');
    }
};


/* 色設定 */
function setColor(units_number, color, value) {
    colorData.fill(0);
    $(`input[id^=${units_number}_${color}_out]`).val(Number(value));                                            // 変更した値を適用
    colorData[1] = units_number ? units_number : methodId;                                                      // 個体番号(0 -> methodId)
    colorData[2] = Number($(`input[id='${units_number}_R_out1']`).val());                                       // 値を取得,代入(R)
    colorData[3] = Number($(`input[id='${units_number}_G_out1']`).val());                                       // 値を取得,代入(G)
    colorData[4] = Number($(`input[id='${units_number}_B_out1']`).val());                                       // 値を取得,代入(B)
    colorData[5] = units_number || methodId == 200 ? 0 : delay;                                                 // 遅延
    db.ref('/data/LED/').update({ value: `[${colorData}]`});                                                    // Firebase更新

    if (units_number) {
        $(`#Unit${units_number}`).css('background', `rgb(${colorData[2]},${colorData[3]},${colorData[4]})`);    // 背景色
    } else {
        $('input[id*="R_out"]').val(colorData[2]);                                                              // 値設定(R)
        $('input[id*="G_out"]').val(colorData[3]);                                                              // 値設定(G)
        $('input[id*="B_out"]').val(colorData[4]);                                                              // 値設定(B)
        $('div[id^="Unit"]').css('background', `rgb(${colorData[2]},${colorData[3]},${colorData[4]})`);         // 背景色(全て)
    }
}


/* 倍率設定 */
function magnification(units_number) {
    let data = [1, 0, 0, 0, 0, 0];

    data[1] = units_number;
    data[2] = Number($(`input[id='${units_number}_R_mag']`).val()) * 10;         // 値を取得,代入(R)
    data[3] = Number($(`input[id='${units_number}_G_mag']`).val()) * 10;         // 値を取得,代入(G)
    data[4] = Number($(`input[id='${units_number}_B_mag']`).val()) * 10;         // 値を取得,代入(B)
    db.ref('/data/LED/magnification/').update({ [units_number]: `[${data}]`});   // Firebase更新
}


/*　マクロ設定 */

// グラデーション(Rainbow)
$('#macro_RB1').click(() => { macro(100, 0); });

// ランダム
$('#macro_random').click(() => { macro(2, 0); });

function macro(mode, delay) {
    colorData = [mode, methodId, 0, 0, 0, delay];
    db.ref('/data/LED/').update({ value: `[${colorData}]` });     // Firebase更新
    colorData.fill(0);
    setTimeout(() => {
        db.ref('/data/LED/').update({ value: `[${colorData}]`});  // Firebase更新
    }, 1000);
}

```

</div></details>

## 制御パネルのデモ
完成した制御パネルは下のようになります。
「Unit0」は全てのユニットを操作するものです。各ユニットのR、G、Bを操作することで、個別に色を変えられます。
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/857712/d130c98b-96a3-59c5-d684-bbfa66f8656e.gif)

# 7. 完成したアート
最後に、**完成したアート**をもう一度見せます。
今回は動画です！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">もうすぐクリスマスなので<br>イルミネーション✨<a href="https://twitter.com/hashtag/%E3%83%87%E3%82%B8%E3%82%BF%E3%83%AB%E3%82%A2%E3%83%BC%E3%83%88?src=hash&amp;ref_src=twsrc%5Etfw">#デジタルアート</a> <a href="https://t.co/K1Jd5RVXKn">pic.twitter.com/K1Jd5RVXKn</a></p>&mdash; あつし@福島高専AdventCalendar2020 (@atsushi_nit) <a href="https://twitter.com/atsushi_nit/status/1339543531321364480?ref_src=twsrc%5Etfw">December 17, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 8. 開発環境
- Windows10
- Arduino IDE 1.8.12
- esp8266 by ESP8266 Community 2.6.3
- Firebase JavaScript SDK Version 8.2.1

# 9. おわりに
今回は、LEDを使用したアートを制作しました。
制作を手伝っていただいた皆様、本当にありがとうございました。

今の段階では、ただLEDを制御するだけなので、センサーで音に反応したり、人に反応したりするなど、もっとインタラクティブなアートにしていきたいですね。（その時は、記事を書くのでぜひお楽しみに！）

最後まで読んでいただき、ありがとうございました！

# 10. 参考
- [ESP-WROOM-02の使い方/まとめ](https://www.petitmonte.com/robot/howto_esp_wroom_02.html)
- [グラスをスマートにする技術～その３：ESP-Now～](https://qiita.com/nnn112358/items/da9762c4651c521af2c8)
- [ESP8266とESP32 温度測定にESP-NOWを使ってみる](https://okiraku-camera.tokyo/blog/?p=7167)
- [Firebase Realtime Database のデータ保存、取得、ストリーミング受信実験](https://www.mgo-tec.com/blog-entry-firebase-realtime-database-sever-sent-events-esp32-m5stack.html/4)