聞きかじりめも

主にC++やメディア処理技術などに関して気付いたことを書いていきます.ここが俺のメモ帳だ!

虹色ペンライトを改造

今年の初めに作ってからというものずっと放置していましたが,ようやく記事にする気になりました. 一応身内には制作過程の資料を公開してたのですが...

組み込みプログラミングの部分はかなり端折っているので,この記事を読めば誰でもすぐに作れるって訳ではないです. 興味ある人は参考程度に見ておいてください.

(20181116追記) 4thライブ仕様の新しいAqoursラブライブレードがこれとほぼ同じ実装らしいです。なんか報われた気がします。 (20211021追記) 諸事情により動画のアップロード先を書き換えました.また,Amazon広告を変えました.

問題提起

近年のライブシーンにおいて,多色LEDペンライトを使用する機会が増えている.これは演者を応援するグッズである以上に,観客が色によって能動的に情報発信するツールとしても使用される.観客自体が訓練し一種の舞台装置となる例[1]もあるが,特定のメンバーを応援するためにメンバーのイメージ色を表示する場合が多い.しかしキングブレードに代表される現在市販の「虹色」ペンライトの殆どは光源が一つであるため,光の色が変化していく時間的な虹色発光は可能であるものの,空間的な虹色発光が原理上不可能である.このため,同時に2人以上のメンバー色を表示し応援することが不可能という問題点があった.筆者は,μ's全員を同時に応援することを目的として,最大9色まで同時発光が可能なペンライト「レイン棒」を新規開発し,これらの問題への解決を試みた.本稿ではその製作過程を報告する.(論文のAbstract風に)

[1] 水樹奈々田村ゆかりのライブ会場が好例.彼らは高度に統率されており,群体として一つの大きな光のパフォーマンスを見せる時がある.最近ではサザンオールスターズが,WiFi技術を駆使して訓練なしに観客の光を制御する仕掛けをした.

つまりどういうことだってばよ

f:id:Mzawa2:20151010105742j:plain:w320

こんなのを作った.以上.

材料

外側

  • ボタン付きペンライト

ガワだけ使うのでボタンがついてりゃなんでもいいです.なんなら3Dプリンタで作ればもっと自由なデザインになります.私は安さ重視で「キングサンダーIK1」というキンブレの中華コピー品を使用しました(2015/01現在在庫・再入荷予定なし).ボタンスイッチは1個だけですが充分です.

f:id:Mzawa2:20151010114157j:plain:w320

電子パーツ

f:id:Mzawa2:20151010115513j:plain

  • 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.ペンライトを分解する

f:id:Mzawa2:20151010153245j:plain:w300

分解してみると,発光部根元の小さいLEDを透明アクリル板とビニール製の筒で拡散させて全体を光らせるという安価な実装でした.この筒にLEDのタワーを入れていきます.案外内部スペースが小さいので,基板実装は工夫が必要です.

2.LEDモジュールをはんだ付けする

f:id:Mzawa2:20151010153456j:plain:h300 f:id:Mzawa2:20151011150645j:plain:h300

まず1個目にピンヘッダをつけた後,LEDモジュールの6個の足のうち外側の4個(GND, VDD)を,先程の1個目の上にモジュールの向きを揃えてタワー状に半田づけします.タワーの高さと間隔はペンライトの大きさから考えましょう.次に,1個目のDOピンから2個目のDIピン,2個目のDOから3個目のDI…と9個目のDIまで繋げます. ここの半田付けはクソ面倒ですが根気よく行きましょう.

3.周辺回路を組み立てる

f:id:Mzawa2:20151010153652p:plain:h300

制御マイコンの2階を作る感じで,電源・スイッチ・LED信号線(根元の方のDI)・GNDをそれぞれ,マイコン電源(LED電源)・マイコンIOピン2箇所・電池の−極に繋げます.抵抗が手元になければ最悪省略してしまっても動きます.頑張って半田付けしましょう.

4.基板をペンライト内に納める

f:id:Mzawa2:20151010153943j:plain:h300 f:id:Mzawa2:20151010154010j:plain:h300

試しに作った制御回路を嵌め込んでみると,狭すぎて入らないことがあります.こういう時は思い切って本体を削ったり穴を開けたりしてみましょう.入るようになるかもしれません.

5.LEDタワーをセットする

f:id:Mzawa2:20151010154458j:plain:h300

先ほど製作したLEDタワーをビニール筒の中に納めます.拡散用アクリル板はいらないのでポイしてください.

6.サンプルプログラムを書き込む

LEDを装着できることを確認したら,まずはサンプルプログラムをネットから拾って動かしてみましょう.開発環境は「Digispark tutorial」で,プログラムは「Adafruit Neopixel library」でググれば出てきます(両方英語).書き込み方などの詳細は日本語で解説している個人ブログを参照してください. ただし,サンプルプログラムはそのままでは動きませんので手を加えます.例えば「buttoncycler」なら,次のようにマクロ定数を変えます.

#define BUTTON_PIN 21
#define PIXEL_PIN 60
#define PIXEL_COUNT 169

うまくいけば色々なパターンで光るLEDタワーが確認できるはずです.

www.youtube.com

7.プログラムを改造する

ここまで来たらあとは皆さんの思うがままにサンプルを改変しちゃいましょう.参考までに私はµ's仕様にしており,かよちん(Lime)→凛ちゃん(Yellow)→(・8・)(White)→ハノケチュン(Dark Orange)→まきちゃん(Red)→にこにー(Deep Pink)→のんちゃん(Dark Violet)→ンミチュン(Blue)→エリチカ(Cyan)→この順でレインボー,という感じにしました.()内の色名はweb用カラーコードを参考にしています.長押しでオフになり最初に戻ります.レインボーのみ長押し中は光が動きます.ソースコードは後で公開するかも.

www.youtube.com

終わりに

開発当初に思い描いていた,µ'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追記) 要望があったので基板の写真を追加します。基本的に回路を銅線で繋いであるだけです。抵抗も特に入れていません。