M5Stack Core2でWS2812BのテープLEDをFastLEDで制御

# 1. この記事でわかること

- M5Stack Core2 を使った次の制御
  - WS2812B　フルカラーシリアルLED
  - ディスプレイ機能
    - デバッグ用に情報を表示
    - ディスプレイのタッチイベントから処理をする

# 2. はじめに

## 2.1. 作業環境

- PC
  - OS : Windows 11
  - 開発環境：VSCode + PlatformIO
- [M5Stack Core2 v1.1](https://www.switch-science.com/products/9349?srsltid=AfmBOopTd-uv53jPmaTqCY02Y13JIG7Pon3-G23lh6FZ-RxJqSB0QmV0)
    -  M5Stack Core2は、ESP32を搭載した多機能なマイコンボードであり、タッチパネル液晶や各種センサーが内蔵されている。
 -  フルカラーシリアルLEDテープ
    -  [SK6812搭載 防水RGB LEDテープ](https://www.switch-science.com/products/6917?srsltid=AfmBOopTfiOKDuJ9pg7Lg0JeKxW_4xL5RUj5iu3jhzEoulu3AEiar-Mn)（今回は 200cm 120LEDs）

## 2.2. 前提

以下の記事の続きなので、基本的な部分が知りたければそちらを参照ください。

- [前回の記事：M5Stack Core2 の開発環境を作成する( VSCode + PlatformIO )](https://qiita.com/ten_chTech/items/55b3b1da9298b6818af6)

# 3. やりたいことの説明

## 3.1. システム構成概要

### 3.1.1. 構成図

出力としてフルカラーシリアルLEDとディスプレイ（デバッグ用にシリアル通信）
入力としてディスプレイのタッチ機能を使う。

![SpritLED001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3989640/d3703df0-5561-ae7a-9fd6-b04602645642.png)


### 3.1.2. 主要機能

*   **テープLED制御**:
    *   フルカラーシリアルLEDテープの色と明るさを制御します。
    *   色味は全体で統一し、３色を順にゆっくり変化させます。
    *   明るさは波状に変化させて動きのある表現にします。
    *   波のアニメーション速度はインタラクティブに変更可能です。

*   **インタラクティブ操作**:
    *   ディスプレイのタッチイベントを利用して、テープLEDの明るさに関する制御を一部変更します。

*   **デバッグ**:
    *   シリアル通信とディスプレイ表示を用いて、デバッグ用の変数を表示します。

### 3.1.3. ハードウェア構成

*   **M5Stack Core2**: メイン制御デバイス
*   **ディスプレイ**: タッチイベント入力とデバッグ情報の表示
*   **フルカラーシリアルLEDテープ**: 照明出力
*   **PC**: シリアルモニターでのデバッグ表示

### 3.1.4. 制御の概要

*   **色変化**: 全体で同じ色を使用し、３つの色を順繰りにゆっくり変化させます。
*   **明るさ変化**: 波状のパターンで明るさを変化させ、動きを表現します。
*   **インタラクティブ制御**: タッチイベントで波のアニメーション速度を変更可能にします。


# 4. コーディング作業

## 4.1. 必要なライブラリの追加

前回の記事と同じ要領でライブラリの追加作業を行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3989640/d81edc85-d6ca-2eb1-a7cd-8aefc8b23ee0.png)


### 4.1.1. ライブラリ：FastLED

そもそも今回制御しようとしているフルカラーシリアルLEDテープは`WS2812B`で制御されている。（画像は以下記事よりばっすい）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3989640/194a3c2c-e531-f1f4-682b-260bb37aecb8.png)


https://www.marutsu.co.jp/pc/static/large_order/WS2812B_0124?srsltid=AfmBOooynlRi1OIcyIOxRZcEySPD6QAvXW1rpdubPQWYtYd8YqCuGdiS

そして`WS2812B`がFastLEDで対応されているのか以下のドキュメントで検索することで確認することができる。

https://fastled.io/docs/group___chipsets.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3989640/34b26674-111b-28ae-dc33-6f1065519162.png)

このことから、`WS2812B`を採用しているM5Stack Core2のLED制御に`FastLED`をライブラリとして採用しても大丈夫なことが確認できる。

## 4.2. ファイル構造

今回作成と編集を行ったソースコード部分を以下に示す。`lib/myfunc/`のディレクトリは今回追加した関数になる。`src/`直下の`main.cpp`と同じ場所に自作関数を置くとコンパイラ時にエラーが発生するので注意。


```
(プロジェクトのホームディレクトリ)
├── src/
│   └── main.cpp
└── lib/
    └── myfunc/
        ├── waveAnimation.h
        ├── waveAnimation.cpp
        ├── displayValues.h
        ├── displayValues.cpp
        ├── handleTouch.h
        ├── handleTouch.cpp
        ├── displayCurrentColor.h
        └── displayCurrentColor.cpp
```

## 4.3. main.cpp の内容

コードのコメントに説明が書いてあるので、そちらを参照

```cpp
#include <M5Core2.h>
#include <FastLED.h>
#include <cmath>
#include "../lib/myfunc/waveAnimation.h"
#include "../lib/myfunc/displayValues.h"
#include "../lib/myfunc/handleTouch.h"
#include "../lib/myfunc/displayCurrentColor.h" // 新しいヘッダーファイルをインクルード

// LEDストリップの設定（購入したものに合わせて設定）
#define LED_PIN     32        // LEDストリップのデータピンM5Stack Core2のG32に接続してる
#define NUM_LEDS    120       // LEDストリップのLED数
#define LED_TYPE    WS2812B   // LEDストリップのタイプ
#define COLOR_ORDER GRB

CRGB leds[NUM_LEDS];

// 色変化のための変数
float hue = 0;
float hue_change_speed = 0.1;
float brightness_scale_max = 1.0;
float brightness_scale_min = 0.2; // 最小の明るさ

// 色の定義
const CRGB color1 = CRGB(255, 105, 180); // ホットピンク
const CRGB color2 = CRGB(148, 0, 211);  // ダークバイオレット
const CRGB color_white = CRGB::White;

// 色変化周期
const int color_change_period = 15000; // 15秒(ミリ秒単位)

unsigned long lastTouchTime = 0;
unsigned long prevTouchTime = 0;
int cycle_time = 5000; // 周期

void setup() {
  Serial.begin(115200);
  delay(1000);
  M5.begin(); // M5 Stack Core2 の初期化
  M5.Lcd.clear();
  displayValues(brightness_scale_max, brightness_scale_min, color_change_period, cycle_time);

  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.setBrightness(80);
  Serial.println("Setup done.");
}

void loop() {
  static float last_brightness_max = brightness_scale_max;    // 前回の最大明るさ
  static float last_brightness_min = brightness_scale_min;    // 前回の最小明るさ
  static int last_color_change_period = color_change_period;  // 前回の色変化周期
  static int last_cycle_time = cycle_time;                    // 前回の周期

  // 現在の色を計算
  CRGB current_color = calculateCurrentColor(color1, color2, color_white, color_change_period);

  // ウェーブアニメーションの実行
  waveAnimation(leds, NUM_LEDS, brightness_scale_max, brightness_scale_min, current_color, cycle_time);

  // タッチイベントの処理
  handleTouch(prevTouchTime, lastTouchTime, cycle_time);

  // デバッグ用にディスプレイに値を表示
  if (brightness_scale_max != last_brightness_max || brightness_scale_min != last_brightness_min || color_change_period != last_color_change_period || cycle_time != last_cycle_time) {
    displayValues(brightness_scale_max, brightness_scale_min, color_change_period, cycle_time);
    last_brightness_max = brightness_scale_max;
    last_brightness_min = brightness_scale_min;
    last_color_change_period = color_change_period;
    last_cycle_time = cycle_time;
  }

  // 現在のLEDの色を取得してディスプレイに表示
  displayCurrentColor(current_color);

  delay(20); // アニメーションのスムーズさを調整
}

```

## 4.4. 関数の説明

`main.cpp`で呼ばれている自作関数を以下で説明する。ソースコードもつけているので必要であればそちらをコピーでもして使ってください。

### 4.4.1. waveAnimation

```cpp
void waveAnimation(CRGB* leds, int numLeds, float brightness_max, float brightness_min, CRGB current_color, int cycle_time);
```

- **引数**
  - `CRGB* leds`: LED配列のポインタ
  - `int numLeds`: LEDの数
  - `float brightness_max`: 最大明るさ
  - `float brightness_min`: 最小明るさ
  - `CRGB current_color`: 現在の色
  - `int cycle_time`: サイクル時間（ミリ秒）
- **処理内容**
  - 各LEDの明るさと色を計算し、波のようなアニメーションを表示します。

### 4.4.2. calculateCurrentColor

```cpp
CRGB calculateCurrentColor(CRGB color1, CRGB color2, CRGB color_white, int color_change_period);
```

- **引数**
  - `CRGB color1`: 最初の色
  - `CRGB color2`: 二番目の色
  - `CRGB color_white`: 白色
  - `int color_change_period`: 色の変化周期（ミリ秒）
- **処理内容**
  - 色の変化周期に基づいて現在の色を計算します。

### 4.4.3. handleTouch

```cpp
void handleTouch(unsigned long &prevTouchTime, unsigned long &lastTouchTime, int &cycle_time);
```

- **引数**
  - `unsigned long &prevTouchTime`: 前回のタッチ時間
  - `unsigned long &lastTouchTime`: 最後のタッチ時間
  - `int &cycle_time`: サイクル時間
- **処理内容**
  - タッチ入力を処理し、タッチ間の時間を計算します。

### 4.4.4. displayCurrentColor

```cpp
void displayCurrentColor(CRGB color);
```

- **引数**
  - `CRGB color`: 表示する色
- **処理内容**
  - 現在の色をディスプレイに表示します。

### 4.4.5. displayValues

```cpp
void displayValues(float brightness_max, float brightness_min, int color_change_period, int cycle_time);
```

- **引数**
  - `float brightness_max`: 最大明るさ
  - `float brightness_min`: 最小明るさ
  - `int color_change_period`: 色の変化周期
  - `int cycle_time`: サイクル時間
- **処理内容**
  - 各種パラメータの値をディスプレイに表示します。



## 4.5. 各ファイルのソースコード

左の▼をクリックすると内容が開かれます。

<details>
<summary>waveAnimation.h</summary>

```cpp
#ifndef WAVE_ANIMATION_H
#define WAVE_ANIMATION_H

#include <FastLED.h>

void waveAnimation(CRGB* leds, int numLeds, float brightness_max, float brightness_min, CRGB current_color, int cycle_time);
CRGB calculateCurrentColor(CRGB color1, CRGB color2, CRGB color_white, int color_change_period);

#endif // WAVE_ANIMATION_H
```
</details>

<details>
<summary>waveAnimation.cpp</summary>

```cpp
#include "waveAnimation.h"
#include <FastLED.h>
#include <cmath>

void waveAnimation(CRGB* leds, int numLeds, float brightness_max, float brightness_min, CRGB current_color, int cycle_time) {
    // 各LEDに対して処理を行う
    for (int i = 0; i < numLeds; i++) {
        // 各LEDの遅延時間に基づいて光の強さを計算
        float delay_time    = (i * 10) % cycle_time; // 遅延時間を計算（0-cycle_timeの範囲）
        float time_now      = millis() % cycle_time; // 現在時間を計算（0-cycle_timeの範囲）
        float relative_time = time_now - delay_time; // 遅延時間を考慮した相対時間を計算
        if(relative_time < 0)
        {
            relative_time += cycle_time;
        }
        
        float brightness_scale;
        
        // 明るさのカーブ計算
        if(relative_time < cycle_time / 5){
            brightness_scale = 1.0; // 最大で1/5周期点灯
        } else if(relative_time < cycle_time){
            float progress = 1.0 - (relative_time - cycle_time / 5) / (cycle_time * 4 / 5); // 残りの4/5周期で減衰
            brightness_scale = (brightness_max - brightness_min) * progress + brightness_min;
        } else{
            brightness_scale = brightness_min;
        }
        
        // 色の計算
        CRGB color;
        color.r = (float)current_color.r * brightness_scale;
        color.g = (float)current_color.g * brightness_scale;
        color.b = (float)current_color.b * brightness_scale;
        leds[i] = color;

        // debugのためにLED0の情報をシリアルモニター表示
        if (i == 0) {
            Serial.print("LED ");
            Serial.print(i);
            Serial.print(": Brightness Scale = ");
            Serial.println(brightness_scale);
            Serial.print("Color = ");
            Serial.print(color.r);
            Serial.print(", ");
            Serial.print(color.g);
            Serial.print(", ");
            Serial.println(color.b);
        }
    }
    FastLED.show();
}

CRGB calculateCurrentColor(CRGB color1, CRGB color2, CRGB color_white, int color_change_period) {
    float time_progress = (millis() % color_change_period) / (float)color_change_period;
    CRGB current_color;
    if (time_progress < 0.333) {
        // ホットピンクから白へ
        current_color = blend(color1, color_white, time_progress * 3 * 255);
    } else if (time_progress < 0.666) {
        // 白からダークバイオレットへ
        current_color = blend(color_white, color2, (time_progress - 0.333) * 3 * 255);
    } else {
        // ダークバイオレットからホットピンクへ
        current_color = blend(color2, color1, (time_progress - 0.666) * 3 * 255);
    }
    return current_color;
}
```
</details>

<details>
<summary>handleTouch.h</summary>

```cpp
#ifndef HANDLE_TOUCH_H
#define HANDLE_TOUCH_H

void handleTouch(unsigned long &prevTouchTime, unsigned long &lastTouchTime, int &cycle_time);

#endif // HANDLE_TOUCH_H
```
</details>

<details>
<summary>handleTouch.cpp</summary>

```cpp
#include <M5Core2.h>
#include "handleTouch.h"

/**
 * @brief タッチ入力を処理し、タッチ間の時間を計算します。
 *
 * この関数はタッチスクリーンが押されているかを確認します。押されている場合、
 * 前回と最後のタッチ時間を更新します。その後、現在のタッチと前回のタッチの間の
 * サイクル時間を計算します。サイクル時間が10秒を超える場合、サイクル時間を5秒に設定します。
 *
 * @param prevTouchTime 前回のタッチ時間を格納する変数への参照。
 * @param lastTouchTime 最後のタッチ時間を格納する変数への参照。
 * @param cycle_time タッチ間のサイクル時間を格納する変数への参照。
 */

void handleTouch(unsigned long &prevTouchTime, unsigned long &lastTouchTime, int &cycle_time) {
  static bool wasPressed = false;
  if (M5.Touch.ispressed()) {
    if (!wasPressed) {
      unsigned long currentTouchTime = millis();
      if (lastTouchTime != 0) {
        cycle_time = currentTouchTime - lastTouchTime;
        if (cycle_time > 10000) {
          cycle_time = 5000;
        }
      }
      prevTouchTime = lastTouchTime;
      lastTouchTime = currentTouchTime;
      wasPressed = true;
    }
  } else {
    wasPressed = false;
  }
}
```
</details>

<details>
<summary>displayCurrentColor.h</summary>

```cpp
#ifndef DISPLAY_CURRENT_COLOR_H
#define DISPLAY_CURRENT_COLOR_H

#include <M5Core2.h>
#include <FastLED.h>

void displayCurrentColor(CRGB color);

#endif // DISPLAY_CURRENT_COLOR_H
```
</details>

<details>
<summary>displayCurrentColor.cpp</summary>

```cpp
#include "displayCurrentColor.h"

void displayCurrentColor(CRGB color) {
  int height = M5.Lcd.height() / 2;
  M5.Lcd.fillRect(0, height, M5.Lcd.width(), height, M5.Lcd.color565(color.r, color.g, color.b));
  M5.Lcd.setBrightness(255); // 明るさを最大に設定
}
```
</details>

<details>
<summary>displayValues.h</summary>

```cpp
#ifndef DISPLAY_VALUES_H
#define DISPLAY_VALUES_H

void displayValues(float brightness_max, float brightness_min, int color_change_period, int cycle_time);

#endif // DISPLAY_VALUES_H
```
</details>

<details>
<summary>displayValues.cpp</summary>

```cpp
#include <M5Core2.h>
#include "displayValues.h"

void displayValues(float brightness_max, float brightness_min, int color_change_period, int cycle_time) {
  M5.Lcd.clear();
  M5.Lcd.setTextSize(2); // 文字サイズを2倍に設定
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.printf("Brightness Max: %.2f\n", brightness_max);
  M5.Lcd.printf("Brightness Min: %.2f\n", brightness_min);
  M5.Lcd.printf("Color Change Period: %d\n", color_change_period);
  M5.Lcd.printf("Cycle Time: %d\n", cycle_time);
}
```
</details>




# 5. 動作確認

## 5.1. 出力

![SpritLED.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3989640/e36f6b3d-9845-c217-6c59-41dc5329ebb1.gif)

確認できること

- フルカラーシリアルLEDテープが以下の制御がされている
    - スムーズに全体の色味が変化している
    - 明かりの強さでウェーブが表現されている
- ディスプレイ下部
    - 設定されている色が確認できる


## 5.2. 入力

![SpritLEDTouch.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3989640/19e77fcf-14ec-f025-cec6-2b16b821a958.gif)

確認できること

- ディスプレイタッチ
    - タッチとタッチの時間間隔を`Cycle Time`でとっている
    - `Cycle Time`に応じて、フルカラーシリアルLEDテープのウェーブのスピードが変化している

# 6. 雑感

おそらく記事にソースコードべた書きなのを見て思うことがある人がいる気がするので、思ったことを少し書き記す。

### ソースコード共有の難しさ

*   **複数ファイル**: 作品の例としてソースコードを紹介する際、複数のファイルに分割されているため、記事内での表現が難しい。
*   **GitHub利用**:
    *   GitHubでの公開も検討したが、組み込みソフトのソースコードは複数のライブラリに依存するため、単純な公開では理解が難しい。
    *   GitHubの機能を使うことで部分的な共有は可能だが、GitHubやGitに関する知識が必要となるため、読者の理解のハードルが上がる。
*   **結論**: 上記の理由から、今回はソースコード全体を記事内に掲載することにした。