プログラミング

【Arduino】WS2812C搭載ネコユニットをグラデーションで光らせるコードの紹介

今回はスイッチサイエンスさんの「WS2812C搭載ネコユニット」を好きな2色でグラデーションを作り光らせるコードを作成したので紹介します。

👆実際にこのコードを使用してWS2812C搭載ネコユニットを光らせている様子。字幕にもある通り、色と点灯速度、色が切り替わる速度、LEDを消灯させる個数を変更することができます。また、今回はボタンによるモード切り替えについても少し解説します。

使用するライブラリについて

今回は Adafruit_NeoPixel.h というライブラリを使用します。WS2812C搭載ネコユニットを制御するのに使用し、Arduino IDEのライブラリマネージャーからインストール可能です。また、NeoPixelに互換のあるLEDでも今回のコードは使用できます。
M5AtomS3.hというライブラリも使用していますが、AtomS3モジュールを使用しない場合は、このライブラリは不要です。

コード全体の紹介

まずは、コード全体の大まかな説明をします。詳細な解説は「コードのゆる解説」でするため、まずはこちらで全体の処理をふんわりと理解してください。

こちらが今回紹介するコードの全体像です。

  1. #include <M5AtomS3.h>
  2. #include <Adafruit_NeoPixel.h>
  3. // 変数の宣言
  4. volatile int mode_now = 0; // モードの切り替え
  5. // mode 0:動作A RoyalBlue & Snow
  6. // mode 1:動作B DarkGrean & PureGrean
  7. // mode 2:動作C Red & Gold
  8. // モードの設定
  9. const int LED_colorsA[3][3] = {{55,90,225},{0,128,0},{255,0,0}};
  10. const int LED_colorsB[3][3] = {{230,230,250},{152,251,152},{255,215,0}};
  11. // LED制御用の変数
  12. #define DELAYVAL 5 // ループに使用するディレイ
  13. float LED_lv = 0; // 点灯させる場所
  14. float LED_color_lv = 0; // 色変化率
  15. const float d_LED_lv[3] = {0.5, 0.7, 1}; // 点灯速度, 1を超えると飛び飛びに
  16. const float d_LED_color_lv[3] = {0.03, 0.09, 0.22}; // 色の切り替え速度
  17. const int off_LED_nums[] = {10, 5, 0}; // 消灯させるLEDの数
  18. // ネコミミPIN, インスタンス作成
  19. #define PIN 2 // ネコ耳LEDに接続するピン
  20. #define NUMPIXELS 70 // ネコ耳モジュールは左右合わせて35 * 2 の 70LED
  21. Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
  22. void setup() {
  23.   // AtomS3モジュールを初期化
  24.   AtomS3.begin();
  25.   // ネコ耳用モジュールの初期化
  26.   pixels.setBrightness(20);
  27.   pixels.begin();
  28. }
  29. void loop(void) {
  30.   // ボタンでのモード切り替え
  31.   AtomS3.update();
  32.   if (AtomS3.BtnA.wasPressed()) {
  33.     // 押したらモード切り替え : 0, 1, 2, 0, 1, 2...
  34.     if (mode_now < 2){
  35.       mode_now++;
  36.     } else if (mode_now == 2){
  37.       mode_now = 0;
  38.     }
  39.   }
  40.   
  41.   delayWithLED(50);
  42. }
  43. void delayWithLED (int delay_time){ // delay_timeは5msの倍数で指定
  44.   for (int i = 0; i < delay_time; i += DELAYVAL){
  45.     // LEDの処理
  46.     // 点灯箇所, 色の切り替えのための変数の処理
  47.     LED_lv += d_LED_lv[mode_now];
  48.     LED_color_lv += d_LED_color_lv[mode_now];
  49.     if ((int)LED_lv >= NUMPIXELS) { // 一番端まで行ったらスタートへ
  50.       LED_lv = 0;
  51.     }
  52.     if (LED_color_lv > 2.0 * PI) { // LED_color_lvがオーバーフローしないよう調節
  53.       LED_color_lv -= 2.0 * PI;
  54.     }
  55.     // 点灯する箇所のRGBを計算
  56.     int color[3];
  57.     for(int i = 0; i < 3; i++){
  58.       color[i] = (int)( (float)LED_colorsA[mode_now][i] * (sin(LED_color_lv) + 1) / 2
  59.                       + (float)LED_colorsB[mode_now][i] * (1.0 - (sin(LED_color_lv) + 1) / 2));
  60.     }
  61.     // 少し先を消すことで点灯を強調
  62.     if ((int)LED_lv + off_LED_nums[mode_now] < NUMPIXELS){
  63.       pixels.setPixelColor((int)LED_lv + off_LED_nums[mode_now], pixels.Color(0, 0, 0));
  64.     } else {
  65.       pixels.setPixelColor((int)LED_lv + off_LED_nums[mode_now] - NUMPIXELS, pixels.Color(0, 0, 0));
  66.     }
  67.     // 現在光らせたい箇所を点灯
  68.     pixels.setPixelColor((int)LED_lv, pixels.Color(color[0], color[1], color[2]));
  69.     pixels.show();
  70.     delay(DELAYVAL);
  71.   }
  72. }

このコードでは、初めに宣言した変数を元に、delayWithLED関数によって、点灯箇所、色を計算しそれを反映させます。delayWithLED()関数はLED_lv, LED_color_lvというfloat型の変数を計算、それを元にLEDを光らせる場所、色の配合割合を計算します。

LED_lvは0〜69の値を取り、d_LED_lvずつ増加し、点灯させる位置を少しずつ変えます。
一方、LED_color_lvは0〜2πの値を取り、d_LED_color_lvずつ増加し、色の配合割合を変えます。
色の配合は色Aに (sin(LED_color_lv) + 1) / 2 、色Bに 1 - (sin(LED_color_lv) + 1) / 2 を掛けたものを合計することで行います。

また、int型の変数mode_nowの値によって、配列から取り出す変数を変えることで使用する色や速度のモードを変更しています。今回はM5AtomS3のボタンを使用したため、loop関数内ではボタンを押したらモードを切り替える処理があります。この処理は、時間で切り替え、もしくは他のボタンやセンサーで切り替えてもいいと思います。WS2812C搭載ネコユニットにもボタンがついているためそちらで処理をしてもいいと思います。

コードのゆる解説

ここからはゆるくコード全体の解説をしていきます。コード全体の紹介でわからない箇所があればこちらをご確認ください。

ライブラリの呼び出し部分です。M5AtomS3.h はM5STACKさんのAtomS3のボタンを使用するために呼び出していますが、他のマイコンを使う方は不要です。
Adafruit_NeoPixel.h はWS2812C搭載ネコユニットを動かすのに使用するために使用します。Arduino IDEのライブラリマネージャーでインストール可能です。

  1. #include <M5AtomS3.h>
  2. #include <Adafruit_NeoPixel.h>

ここでは、delayWithLED()関数で使用する変数を宣言します。今回は3つのモードを切り替えられるよう、長さ3の配列を色々準備していますが、用意したいモードの数に応じて配列の長さを変えてください。

  1. // 変数の宣言
  2. volatile int mode_now = 0; // モードの切り替え
  3. // mode 0:動作A RoyalBlue & Snow
  4. // mode 1:動作B DarkGrean & PureGrean
  5. // mode 2:動作C Red & Gold
  6. // モードの設定
  7. const int LED_colorsA[3][3] = {{55,90,225},{0,128,0},{255,0,0}};
  8. const int LED_colorsB[3][3] = {{230,230,250},{152,251,152},{255,215,0}};
  9. // LED制御用の変数
  10. #define DELAYVAL 5 // ループに使用するディレイ
  11. float LED_lv = 0; // 点灯させる場所
  12. float LED_color_lv = 0; // 色変化率
  13. const float d_LED_lv[3] = {0.5, 0.7, 1}; // 点灯速度, 1を超えると飛び飛びに
  14. const float d_LED_color_lv[3] = {0.03, 0.09, 0.22}; // 色の切り替え速度
  15. const int off_LED_nums[] = {10, 5, 0}; // 消灯させるLEDの数

LED_colorALED_colorBモードの数 ✖️ RGB の長さのint型の配列です。RGBの値は各モードで表示させたい色をAとBの同じ配列番号に入れてください。RGBの値は「RGB カラーコード 一覧」などでお好みの色を見つけてください。

d_LED_lvd_LED_color_lvoff_LED_nums はコメントアウトにあるように、それぞれ点灯速度、色の切り替え速度、消灯させるLEDの数になります。こちらも配列番号がモードの番号と一致するように入力してください。

♦︎ 小話 Arduinoでは配列の長さを書かなくても良い? ♦︎
今回、記事を書いていて気づいたのですが、Arduinoではoff_LED_nums[] のように配列の長さを指定せずに記述することでできるようです。コンパイル時に配列の長さが自動的に決められるとのことですが...私が、最初に勉強したプログラム言語と異なる仕様なので少し恐ろしいです。
予期しないミスや動作に繋がると思うので、なるべく配列の長さは記述することを推奨します。

ここでは、Adafruit_NeoPixelのインスタンスをpixelsという名前で宣言しています。
PIN番号は適宜変更する必要があります。また、ネコユニットを半分しか使わない場合やよりたくさんのネコユニットを繋げる場合はNUMPIXELの数を変更する必要があります。
(今回のコードはユニットを連結させることを前提としたコードのため、別々のピンに接続する場合などはインスタンスを複数宣言し、delayWithLED()関数でインスタンスの切り替え処理などを追加する必要があります。)

  1. // ネコミミPIN, インスタンス作成
  2. #define PIN 2 // ネコ耳LEDに接続するピン
  3. #define NUMPIXELS 70 // ネコ耳モジュールは左右合わせて35 * 2 の 70LED
  4. Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

setup()関数ではAtomS3のモジュールの初期化とネコ耳用のモジュールの初期を行います。AtomS3でないデバイスの場合やAtomS3のボタンを使わない場合は27, 28行目は不要です。
また、pixels.Brightness(20); ではネコユニットの明るさを指定しています。スイッチサイエンスさんのネコユニットの販売ページでは"長時間輝度を最大にするとLEDが焼き切れる恐れがあるので、輝度は256段階中20程度に設定することをお勧めします"とあるので20、もしくはそれ以下にした方がいいでしょう。

  1. void setup() {
  2.   // AtomS3モジュールを初期化
  3.   AtomS3.begin();
  4.   // ネコ耳用モジュールの初期化
  5.   pixels.setBrightness(20);
  6.   pixels.begin();
  7. }

loop()関数ではボタンでのモード切り替えとdelayWithLED関数の呼びだしをしています。今回はAtomS3ボタンを押すことでモードの切り替えを行なっていますが、時間や他のセンサーなどで切り替えてもいいと思います(下に10秒ごとにモードを切り替えるVerを載せています)。ネコユニット自体にもボタンがあるので少し押しづらいですがそちらもご検討ください。

  1. void loop(void) {
  2.   // ボタンでのモード切り替え
  3.   AtomS3.update();
  4.   if (AtomS3.BtnA.wasPressed()) {
  5.     // 押したらモード切り替え : 0, 1, 2, 0, 1, 2...
  6.     if (mode_now < 2){
  7.       mode_now++;
  8.     } else if (mode_now == 2){
  9.       mode_now = 0;
  10.     }
  11.   }
  12.   
  13.   delayWithLED(50);
  14. }

10秒(10,000ミリ秒)でコードを切り替える場合、loop()関数はこのようなコードになります。上記のボタンで切り替えるコードのボタンの更新, ボタンが押されているかの条件をなくし、delayWithLED()関数に渡す時間を変更しました。こちらもご活用ください。

  1. void loop(void) {
  2.   delayWithLED(10000);
  3.   // 10秒ごとにモード切り替え : 0, 1, 2, 0, 1, 2...
  4.   if (mode_now < 2){
  5.     mode_now++;
  6.   } else if (mode_now == 2){
  7.     mode_now = 0;
  8.   }
  9. }

delayWithLED()関数では与えられた時間 delay_time を短い時間 DELAYVAL に区切り、光らせる色・場所の計算、LEDの消灯、LEDの点灯、待機を繰り返します。少し長いコードですが、してること一つ一つは非常にシンプルです。

  1. void delayWithLED (int delay_time){ // delay_timeは5msの倍数で指定
  2.   for (int i = 0; i < delay_time; i += DELAYVAL){
  3.     // LEDの処理
  4.     // 点灯箇所, 色の切り替えのための変数の処理
  5.     LED_lv += d_LED_lv[mode_now];
  6.     LED_color_lv += d_LED_color_lv[mode_now];
  7.     if ((int)LED_lv >= NUMPIXELS) { // 一番端まで行ったらスタートへ
  8.       LED_lv = 0;
  9.     }
  10.     if (LED_color_lv > 2.0 * PI) { // LED_color_lvがオーバーフローしないよう調節
  11.       LED_color_lv -= 2.0 * PI;
  12.     }
  13.     // 点灯する箇所のRGBを計算
  14.     int color[3];
  15.     for(int i = 0; i < 3; i++){
  16.       color[i] = (int)( (float)LED_colorsA[mode_now][i] * (sin(LED_color_lv) + 1) / 2
  17.                       + (float)LED_colorsB[mode_now][i] * (1.0 - (sin(LED_color_lv) + 1) / 2));
  18.     }
  19.     // 少し先を消すことで点灯を強調
  20.     if ((int)LED_lv + off_LED_nums[mode_now] < NUMPIXELS){
  21.       pixels.setPixelColor((int)LED_lv + off_LED_nums[mode_now], pixels.Color(0, 0, 0));
  22.     } else {
  23.       pixels.setPixelColor((int)LED_lv + off_LED_nums[mode_now] - NUMPIXELS, pixels.Color(0, 0, 0));
  24.     }
  25.     // 現在光らせたい箇所を点灯
  26.     pixels.setPixelColor((int)LED_lv, pixels.Color(color[0], color[1], color[2]));
  27.     pixels.show();
  28.     delay(DELAYVAL);
  29.   }
  30. }

// 点灯箇所, 色の切り替えのための変数 では、点灯箇所 LED_lv と色の切り替え度合い LED_color_lv を計算します。
点灯させる箇所である LED_lv にそのモードの点灯速度である d_LED_lv を加算し、色の切り替え度合い LED_color_lv にそのモードの色の切り替え速度 d_LED_color_lv を加算します。
また、点灯箇所 LED_lv はLEDの存在する範囲である必要があるため、NUMPIXELSを超えたら左端の0へ移動させています。色の切り替え度合い LED_color_lv はサイン関数に入れるため、範囲の制限は内容に思えますがオーバーフローといいfloat型の変数の上限を超えてしまう可能性を考えて、2πを超えたら2πを引くようにしています。

// 点灯する箇所のRGBを計算 では、色の切り替え度合い LED_color_lv と各モードの色が指定されている LED_colorsA, LED_colorsB をもとに点灯箇所の色を計算します。
片方の色Aに (sin(LED_color_lv) + 1) / 2 を もう片方の色Bに (1.0 - (sin(LED_color_lv) + 1) / 2)) を掛け足し合わせることで 色A→中間色→色B→中間色→色A→... のように滑らかに色を変化させています。
計算結果を格納するためのint型の配列 color[3] の配列番号0に赤R、配列番号1に緑G、配列番号2に青Bの計算結果を格納しています。この計算はfloat型で行いましたが、色を指定する際は0〜255の整数値で指定する必要があるため、(int)( /* float型の計算結果 */ ) のようにint型に変更しています(このような型変換をキャストと言います)。

// 少し先を消すことで点灯を強調// 現在光らせたい箇所を点灯 ではNeoPixelの setPixelColor()メソッド によって消灯と点灯を行なっています。setPixelColor()メソッド は第1引数に光らせたい位置、第2引数に点灯させたい色を入れることで好きな位置を好きな色で点灯することができます。ここで、今回使用するNeoPixelのメソッドを軽く紹介します。

計算した場所、色をもとに光らせたいため、77行目では③setPixelColor()メソッドを第一引数をint型にキャストした LED_lv、第二引数を color[3] をもとに④Color()メソッドで作成した色で呼び出しています。また、点灯を強調するために off_nums[mode_now] 分先のピクセルを消す(RGBを0, 0, 0にセット)する処理を71〜75行目で行なっています。セットするだけでは反映されないため、show()メソッドを最後に呼び出しています。

少し長くなりましたが、これで今回のコードの説明は以上です。是非、お試しください。

まとめ

今回は「WS2812C搭載ネコユニット」を好きな2色でグラデーションを作り光らせるコードを紹介しました。ネコユニットに限らず、NeoPixelを使うものなら同様に使用できます。
それでは、良きネコ耳ライフをお過ごしください!

謝辞

このコードはTechSeeker Hackathon 2024の成果物の一部です。このような学習の機会を用意してくださった方々、同じチームとして支えてくださったデジもく会のみなさんに感謝します。本当にありがとうございました。

-プログラミング
-, ,