虹色ペンライトを改造
今年の初めに作ってからというものずっと放置していましたが,ようやく記事にする気になりました. 一応身内には制作過程の資料を公開してたのですが...
組み込みプログラミングの部分はかなり端折っているので,この記事を読めば誰でもすぐに作れるって訳ではないです. 興味ある人は参考程度に見ておいてください.
(20181116追記) 4thライブ仕様の新しいAqoursラブライブレードがこれとほぼ同じ実装らしいです。なんか報われた気がします。 (20211021追記) 諸事情により動画のアップロード先を書き換えました.また,Amazon広告を変えました.
問題提起
近年のライブシーンにおいて,多色LEDペンライトを使用する機会が増えている.これは演者を応援するグッズである以上に,観客が色によって能動的に情報発信するツールとしても使用される.観客自体が訓練し一種の舞台装置となる例[1]もあるが,特定のメンバーを応援するためにメンバーのイメージ色を表示する場合が多い.しかしキングブレードに代表される現在市販の「虹色」ペンライトの殆どは光源が一つであるため,光の色が変化していく時間的な虹色発光は可能であるものの,空間的な虹色発光が原理上不可能である.このため,同時に2人以上のメンバー色を表示し応援することが不可能という問題点があった.筆者は,μ's全員を同時に応援することを目的として,最大9色まで同時発光が可能なペンライト「レイン棒」を新規開発し,これらの問題への解決を試みた.本稿ではその製作過程を報告する.(論文のAbstract風に)
[1] 水樹奈々や田村ゆかりのライブ会場が好例.彼らは高度に統率されており,群体として一つの大きな光のパフォーマンスを見せる時がある.最近ではサザンオールスターズが,WiFi技術を駆使して訓練なしに観客の光を制御する仕掛けをした.
つまりどういうことだってばよ
こんなのを作った.以上.
材料
外側
- ボタン付きペンライト
ガワだけ使うのでボタンがついてりゃなんでもいいです.なんなら3Dプリンタで作ればもっと自由なデザインになります.私は安さ重視で「キングサンダーIK1」というキンブレの中華コピー品を使用しました(2015/01現在在庫・再入荷予定なし).ボタンスイッチは1個だけですが充分です.
電子パーツ
- LEDモジュール
秋月電子通商のAE-WS2812Bを使用しました.モジュール自体は最近流行りのAdafruit NeoPixelです.普通の3色LEDではなく,信号線を直列に繋ぐだけで色制御が可能な特殊モジュールです.
(参考: フルカラーシリアルLED NeoPixelを試してみた - The jonki )
- 制御マイコン
DigiSparkという超小型Arduino互換ボードを使用しました.海外からの輸入品ですが,1個895円とお買い得です.初心者にも扱いやすいArduinoベースのマイコンなので,開発環境構築が非常に楽です.これくらいの大きさならペンライト内にすっぽりと収まるでしょう.私は収まりませんでした.
(参考: 橋本商会 » 895円の超小型Ardunoクローン DigiSparkを買った )
最近は輸入代行業者でも買えるみたいです.
http://www.elefine.jp/SHOP/96285/103406/list.html
- その他パーツ
DigiSparkと同じ大きさのユニバーサル基板(DigiSparkと一緒に買える).導線,5kΩくらいの抵抗(あれば),半田,ピンソケット,ピンヘッダ,その他工具.プッシュスイッチは元々ペンライトにあったものを流用.
製作方法
1.ペンライトを分解する
分解してみると,発光部根元の小さいLEDを透明アクリル板とビニール製の筒で拡散させて全体を光らせるという安価な実装でした.この筒にLEDのタワーを入れていきます.案外内部スペースが小さいので,基板実装は工夫が必要です.
2.LEDモジュールをはんだ付けする
まず1個目にピンヘッダをつけた後,LEDモジュールの6個の足のうち外側の4個(GND, VDD)を,先程の1個目の上にモジュールの向きを揃えてタワー状に半田づけします.タワーの高さと間隔はペンライトの大きさから考えましょう.次に,1個目のDOピンから2個目のDIピン,2個目のDOから3個目のDI…と9個目のDIまで繋げます. ここの半田付けはクソ面倒ですが根気よく行きましょう.
3.周辺回路を組み立てる
制御マイコンの2階を作る感じで,電源・スイッチ・LED信号線(根元の方のDI)・GNDをそれぞれ,マイコン電源(LED電源)・マイコンIOピン2箇所・電池の−極に繋げます.抵抗が手元になければ最悪省略してしまっても動きます.頑張って半田付けしましょう.
4.基板をペンライト内に納める
試しに作った制御回路を嵌め込んでみると,狭すぎて入らないことがあります.こういう時は思い切って本体を削ったり穴を開けたりしてみましょう.入るようになるかもしれません.
5.LEDタワーをセットする
先ほど製作したLEDタワーをビニール筒の中に納めます.拡散用アクリル板はいらないのでポイしてください.
6.サンプルプログラムを書き込む
LEDを装着できることを確認したら,まずはサンプルプログラムをネットから拾って動かしてみましょう.開発環境は「Digispark tutorial」で,プログラムは「Adafruit Neopixel library」でググれば出てきます(両方英語).書き込み方などの詳細は日本語で解説している個人ブログを参照してください. ただし,サンプルプログラムはそのままでは動きませんので手を加えます.例えば「buttoncycler」なら,次のようにマクロ定数を変えます.
#define BUTTON_PIN 2 → 1 #define PIXEL_PIN 6 → 0 #define PIXEL_COUNT 16 → 9
うまくいけば色々なパターンで光るLEDタワーが確認できるはずです.
7.プログラムを改造する
ここまで来たらあとは皆さんの思うがままにサンプルを改変しちゃいましょう.参考までに私はµ's仕様にしており,かよちん(Lime)→凛ちゃん(Yellow)→(・8・)(White)→ハノケチュン(Dark Orange)→まきちゃん(Red)→にこにー(Deep Pink)→のんちゃん(Dark Violet)→ンミチュン(Blue)→エリチカ(Cyan)→この順でレインボー,という感じにしました.()内の色名はweb用カラーコードを参考にしています.長押しでオフになり最初に戻ります.レインボーのみ長押し中は光が動きます.ソースコードは後で公開するかも.
終わりに
開発当初に思い描いていた,µ's9人を同時に応援できるペンライトを実現できて大変満足です.今後のオフライブも捗ると思います.今後の発展として,レイン棒を降った時に丁度光のメッセージが読めるような点滅パターンを仕込むなどすると更に表現の幅が広がるので,挑戦したいところです.
しかし残念なことに,多くのライブ(µ'sのライブも含む)では改造・自作ペンライトの使用が禁止されています.自作し終えてから気づいた私は今,ショックで軽く落ち込んでいます.レイン棒が公式に陽の目を見ることがあるのか非常に疑問ですが……
まぁ楽しいからいっか!
(追記:2017/02/04)要望があったのでソースコードを公開します.7.で作成したプログラムは以下の通りですが,その後ちょっと改変したので動画そのものの挙動ではありません.具体的には「終わりに」で述べた「レイン棒を降った時に丁度光のメッセージが読めるような点滅パターン」の実装を試みている部分があります(電源を入れた直後にピンクの点滅が起こりますが,暗い場所でいい感じに横に振るとNICOの文字が見えるかも?).あと,元のbuttoncyclerで使われていた関数は勉強のためそのまま残してあります.
// This is a demonstration on how to use an input device to trigger changes on your neo pixels. // You should wire a momentary push button to connect from ground to a digital IO pin. When you // press the button it will change to a new pixel animation. Note that you need to press the // button once to start the first animation! #include <Adafruit_NeoPixel.h> #define BUTTON_PIN 1 // Digital IO pin connected to the button. #define PIXEL_PIN 0 // Digital IO pin connected to the NeoPixels. #define PIXEL_COUNT 9 // Number of NeoPixels #define SHOWTYPE_NUM 11 // Number of color patterns //-------------------------------- // μ's Color Code (R, G, B) //-------------------------------- #define COLOR_NICO 0xff, 0x14, 0x93 // Deep Pink #define COLOR_MAKI 0xff, 0x00, 0x00 // Red #define COLOR_ELI 0x00, 0xff, 0xff // Cyan #define COLOR_KOTORI 0xff, 0xff, 0xff // White Smoke #define COLOR_HONOKA 0xff, 0x8c, 0x00 // Dark Orange #define COLOR_HANAYO 0x00, 0xff, 0x00 // Lime #define COLOR_RIN 0xff, 0xff, 0x00 // Yellow #define COLOR_UMI 0x00, 0x00, 0xff // Blue #define COLOR_NOZOMI 0x94, 0x00, 0xd3 // Dark Violet #define COLOR_BLACK 0x00, 0x00, 0x00 const uint8_t colorCode[9][3] = { {COLOR_HANAYO}, {COLOR_RIN}, {COLOR_KOTORI}, {COLOR_HONOKA}, {COLOR_MAKI}, {COLOR_NICO}, {COLOR_NOZOMI}, {COLOR_UMI}, {COLOR_ELI}}; const char str[4][5][9] = { {{1,1,1,1,1,1,1,1,1}, {0,0,0,0,0,1,1,0,0}, {0,0,0,1,1,0,0,0,0}, {0,1,1,0,0,0,0,0,0}, {1,1,1,1,1,1,1,1,1}}, // N {{0,0,0,0,0,0,0,0,0}, {1,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,1,1,1}, {1,0,0,0,0,0,0,0,1}, {0,0,0,0,0,0,0,0,0}}, // I {{0,0,1,1,1,1,1,0,0}, {0,1,0,0,0,0,0,1,0}, {1,0,0,0,0,0,0,0,1}, {0,1,0,0,0,0,0,1,0}, {0,0,0,0,0,0,0,0,0}}, // C {{0,0,1,1,1,1,1,0,0}, {0,1,0,0,0,0,0,1,0}, {1,0,0,0,0,0,0,0,1}, {0,1,0,0,0,0,0,1,0}, {0,0,1,1,1,1,1,0,0}}}; // O // Parameter 1 = number of pixels in strip, neopixel stick has 8 // Parameter 2 = pin number (most are valid) // Parameter 3 = pixel type flags, add together as needed: // NEO_RGB Pixels are wired for RGB bitstream // NEO_GRB Pixels are wired for GRB bitstream, correct for neopixel stick // NEO_KHZ400 400 KHz bitstream (e.g. FLORA pixels) // NEO_KHZ800 800 KHz bitstream (e.g. High Density LED strip), correct for neopixel stick Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800); bool oldState = HIGH; int showType = 0; // The Number of Color Patterns unsigned long timeStamp = 0; void setup() { pinMode(BUTTON_PIN, INPUT); strip.begin(); colorWipe(strip.Color(COLOR_KOTORI), 30); delay(50); colorWipe(strip.Color(0, 0, 0), 30); delay(20); strip.show(); // Initialize all pixels to 'off' } void loop() { // Get current button state. bool newState = digitalRead(BUTTON_PIN); // Check if state changed from high to low (button press). if (newState == HIGH && oldState == LOW) { // Short delay to debounce button. delay(20); // Check if button is still low after debounce. newState = digitalRead(BUTTON_PIN); if (newState == HIGH) { showType++; if (showType > SHOWTYPE_NUM) showType = 0; startShow(showType); // Set time stamp on pushing the button. timeStamp = millis(); } } // You can reset color pattern by pushing the button for 1 sec. else if(newState == HIGH && oldState == HIGH) { if((millis() - timeStamp) >= 1000) { showType = 0; startShow(showType); } } // Set the last button state to the old state. oldState = newState; } void startShow(int i) { switch(i){ case 0: colorWipe(strip.Color(COLOR_BLACK), 40); // Black/off break; case 1: colorCharactor(strip.Color(COLOR_NICO), str, 12); break; case 2: colorWipe(strip.Color(COLOR_HANAYO), 40); break; case 3: colorWipe(strip.Color(COLOR_RIN), 40); break; case 4: colorWipe(strip.Color(COLOR_KOTORI), 40); break; case 5: colorWipe(strip.Color(COLOR_HONOKA), 40); break; case 6: colorWipe(strip.Color(COLOR_MAKI), 40); break; case 7: colorWipe(strip.Color(COLOR_NICO), 40); break; case 8: colorWipe(strip.Color(COLOR_NOZOMI), 40); break; case 9: colorWipe(strip.Color(COLOR_UMI), 40); break; case 10: colorWipe(strip.Color(COLOR_ELI), 40); break; case 11: rainbowCycleForLoveLive(80); break; } } // Fill the dots one after the other with a color void colorWipe(uint32_t c, uint8_t wait) { for(uint16_t i=0; i<strip.numPixels(); i++) { strip.setPixelColor(i, c); strip.show(); delay(wait); } } void rainbowForLoveLive(uint8_t wait) { for(int i=0; i<9; i++) { strip.setPixelColor(i, strip.Color(colorCode[i][0],colorCode[i][1],colorCode[i][2])); strip.show(); delay(wait); } } void colorCharactor(uint32_t c, const char str[][5][9], uint8_t wait) { do{ for(int k=0; k<4; k++) { // num of charactors for(int j=0; j<5; j++) { // width of charactor for(int i=0; i<9; i++) { // height of charactor if(str[k][j][i]) strip.setPixelColor(i, c); else strip.setPixelColor(i, strip.Color(COLOR_BLACK)); } strip.show(); delay(wait); } } for(int i=0; i<9; i++) { // height of charactor strip.setPixelColor(i, strip.Color(COLOR_BLACK)); } strip.show(); delay(wait*10); }while(digitalRead(BUTTON_PIN) == LOW); } void rainbowCycleForLoveLive(uint8_t wait){ static int8_t j = 0; while(digitalRead(BUTTON_PIN) == HIGH) { for(int i=0; i<strip.numPixels(); i++) { strip.setPixelColor(i, strip.Color(colorCode[j][0], colorCode[j][1], colorCode[j][2])); j--; if(j<0) j=8; } strip.show(); delay(wait); j++; if(j>8) j=0; } } void rainbow(uint8_t wait) { uint16_t i, j; for(j=0; j<256; j++) { for(i=0; i<strip.numPixels(); i++) { strip.setPixelColor(i, Wheel((i+j) & 255)); } strip.show(); delay(wait); } } // Slightly different, this makes the rainbow equally distributed throughout void rainbowCycle(uint8_t wait) { uint16_t i, j; for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel for(i=0; i< strip.numPixels(); i++) { strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255)); } strip.show(); delay(wait); } } //Theatre-style crawling lights. void theaterChase(uint32_t c, uint8_t wait) { for (int j=0; j<10; j++) { //do 10 cycles of chasing for (int q=0; q < 3; q++) { for (int i=0; i < strip.numPixels(); i=i+3) { strip.setPixelColor(i+q, c); //turn every third pixel on } strip.show(); delay(wait); for (int i=0; i < strip.numPixels(); i=i+3) { strip.setPixelColor(i+q, 0); //turn every third pixel off } } } } //Theatre-style crawling lights with rainbow effect void theaterChaseRainbow(uint8_t wait) { for (int j=0; j < 256; j++) { // cycle all 256 colors in the wheel for (int q=0; q < 3; q++) { for (int i=0; i < strip.numPixels(); i=i+3) { strip.setPixelColor(i+q, Wheel( (i+j) % 255)); //turn every third pixel on } strip.show(); delay(wait); for (int i=0; i < strip.numPixels(); i=i+3) { strip.setPixelColor(i+q, 0); //turn every third pixel off } } } } // Input a value 0 to 255 to get a color value. // The colours are a transition r - g - b - back to r. uint32_t Wheel(byte WheelPos) { WheelPos = 255 - WheelPos; if(WheelPos < 85) { return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3); } else if(WheelPos < 170) { WheelPos -= 85; return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3); } else { WheelPos -= 170; return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0); } }
(2017/06/09追記) 要望があったので基板の写真を追加します。基本的に回路を銅線で繋いであるだけです。抵抗も特に入れていません。