8
2

More than 3 years have passed since last update.

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

Posted at

この記事は 福島高専 Advent Calendar 2020 21日目の記事です。

1. はじめに

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

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

目次

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

2. インタラクティブアートとは

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

このようなアートを制作している方々はたくさんいらっしゃいますが、その中でも有名なのがチームラボ株式会社だと思います。チームラボは様々な分野のスペシャリストから構成され、デジタルアート制作からWebサイト制作、アプリ開発、製品開発などに至るまで様々なことを行っています。下の写真はお台場にある「teamLab Borderless」に行った時のものです。
IMG_6511.jpg
IMG_6506.JPG
IMG_6509.JPG

3. 制作するアート

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

もちろん、色を同期させたり個別に設定したりすることができます。
下の写真が、完成したアートです。
led1.jpg
led2.png
led3.png

システム構成

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

①PCやスマホからWebサイトにアクセスし、Firebaseを操作します。
②Firebaseから変更したデータをマイコンに送信します。
③マイコンでLEDを制御します。
p1.png

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

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

④マイコン(親機)からマイコン(子機)にデータを送信します。
p2.png

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

⑤マイコンから次のマイコンにデータを送信します。
p3.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個を制作に必要な部品で、ほとんどは秋月電子で購入しました。

部品 用途
1WフルカラーRGBLED 光らせる 1
トランジスタ 2SD468L LED電流増幅 3
カーボン抵抗 1/4W 10kΩ マイコン電流制限抵抗 3
カーボン抵抗 1/4W 3.9kΩ マイコン電流制限抵抗 3
カーボン抵抗 1W 5.1Ω LED電流制限抵抗(G、B用) 2
カーボン抵抗 1W 10Ω LED電流制限抵抗(R用) 1
ESP-WROOM-02 マイコン(LED制御、通信用) 1
分割ロングピンソケット マイコンを挿す 16
ユニバーサル基板 Cタイプ 回路作成 1
低損失CMOS三端子レギュレータ 電源電圧降圧 1
電解コンデンサ 100μF レギュレータ用コンデンサ 1
セラミックコンデンサ 0.1μF レギュレータ用コンデンサ 1
電池ボックス 単3×3本 電源用ボックス 1
プラスチックケース SW-85B ユニットを入れるケース 1

何を思ったのか当時の僕は一気にユニットを50個作ろうとか考えていたので、50個分の部品を購入しました。
EHKRyR-U0AEPF6E.jpg

回路作成

回路を作成していきます。
以前にESP-WROOM-02の使い方及び回路作成方法についての記事を書いたので、そちらを参考にしてください。
とりあえず使ってみる ESP-WROOM-02 (ESP8266)

書き込み

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

親機用プログラムの書き込み

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

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

サンプルコード (mother.ino)
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("}");

}

子機用プログラムの書き込み

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

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

サンプルコード (child.ino)
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("}");
}

5. Firebase設定

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

Firebase登録

ここでは、Firebaseの登録、使い方については省略します。
登録、使い方を知りたい方は、とても分かりやすい記事があるのでそちらをご覧ください。
Firebaseの始め方

データの追加

Realtime Databaseにデータを追加してきます。
はじめに、下のように「data」、「LED」、「magnification」という階層を追加し、「LED」内に「value」というオブジェクトを追加します。
この時のvalueの値は何でも良いのですが、ここでは"[0,0,0,0,0,0]"としておきます。
1.png

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

2.png

6. 制御パネル制作

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

HTMLファイルの作成

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

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関数を実行しています。

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を追加します。

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

サンプルコード (index.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>

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について装飾を行います。

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は全てのユニットをコントロールします。)

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

サンプルコード (style.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);
    }
}

Javascriptファイルの作成

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

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

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

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

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

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倍率を取得し、適用させる関数です。

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

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の色設定について書いていきます。

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の倍率設定について書いていきます。

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を更新する関数です。

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

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を更新し、事前に登録してある動作をマイコンにさせる関数です。

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

サンプルコード (main.js)
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);
}

制御パネルのデモ

完成した制御パネルは下のようになります。
「Unit0」は全てのユニットを操作するものです。各ユニットのR、G、Bを操作することで、個別に色を変えられます。
demo.gif

7. 完成したアート

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

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. 参考

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2