amiiboオルゴールポケットをつくる 前編(ソフト編)

th_amiibo-music-box-pocket-4

前回ご紹介したamiiboオルゴールポケットの作り方の前編、ソフト編になります。まずはArduinoでのNFCの扱い方の復習から入りたいと思います。

NFCにはType A, B, F(Felica)の三種類があり、amiiboはそのうちのType Aのタグを使用しています。一方、手に入りやすい電子工作向けNFCリーダーとしてRC-S620Sがありますが、こちらは公式配布のライブラリだとType F(Felica)の読み書きにしか対応していません。

しかし、hirokumaさんが作成してくださったライブラリを使用することで、RC-S620Sを使ってType Aタグのデータの読込みが可能になります(※書込みもできると思いますが、未検証)。

詳しくは以前書いた記事をご参照ください。

amiiboを個別に認識するためには、まずはそれぞれを一意に特定するID情報(UID)を知る必要があります。そのために、一度RC-S620SをArduino Megaに繋いで、それぞれのUIDを確認します。

Arduino UNOではなくMegaを使う理由ですが、UNOにはハードウェアシリアル(RX/TX)が一組しかないからです(Megaは四組)。RC-S620SはArduinoのハードウェアシリアルに接続して使用するので、ハードウェアシリアルに接続してしまうと、PCのArduino IDEのシリアルモニタにデバッグ情報をprintlnで表示することができなくなってしまうのです。そのため、折角UIDを読み取っても、目視で確認できません。読み取った情報をSDか何かに保存して後で読み取ったり、シリアルモニタとは別の表示器が使える場合には、通常のArduino UNOでもOKです。RC-S620Sの接続をハードウェアシリアルの代わりにソフトウェアシリアルで代替するやり方は、少なくとも自分は成功例を見たことがありません。

UIDを確認するためのサンプルプログラムはこんな感じです。なお、以下は、RC-S620SをRX1/TX1に接続することを想定しているので、ライブラリ(ArduinoHkNfcRw)のファイル”devaccess_uart.cpp”で”Serial”になっているところは、事前に一時的に”Serial1″に変更しておいてください。

#include <HkNfcRw.h>
#include <HkNfcA.h>
#include <inttypes.h>
#include <string.h>

uint32_t last_uid    = 0;
uint32_t current_uid = 0;
// Type A MIFARE UltralightのUIDは7byte固定
// (CheckByteを入れたら9byteだが、戻り値には含まれない)
uint8_t uid[] = {0, 0, 0, 0, 0, 0, 0}; 

void setup()
{
  bool ret;
  Serial.begin(115200);
  ret = HkNfcRw::open();
  while (!ret) {}
}

void loop()
{
  HkNfcRw::Type type = HkNfcRw::detect(true, false, false);
  if(type == HkNfcRw::NFC_A) {
    // Type A タグが見つかったとき
    Serial.println(F("Type A Detect!"));

    if(HkNfcRw::getNfcId(uid)){      

      current_uid = uid[6];
      for(int i=5; i>=0; i--){
        current_uid <<= 8; current_uid |= uid[i];
      }
      Serial.print(F("UID:"));
      Serial.println(current_uid);   
    }else{
      Serial.println(F("Failed to get UID."));
    }
  } else {
    // Type A タグが見つからなかったとき
    Serial.println(F("No Tag."));
  }

  delay(500);
}

接続はこんな感じです。

th_amiibo-music-box-pocket-1

これにamiiboを乗せると、

th_amiibo-music-box-pocket-2

(なんかマリオでかいな。。。)こんな感じでシリアルモニタに表示されます。

th_amiibo-music-box-pocket-3

ここでUIDとして表示されている値をメモって置いてください。認識させたいamiiboの分だけ記録しておきます。ちなみに、ずっとamiiboを乗せっぱなしでも”No Tag.”が毎回挟まってしまうのは、ライブラリの仕様のようです。

なお、UIDは別にamiiboのみに含まれる情報ではなく、すべてのNFCタグの固定位置に含まれ、読み取ることができる情報です。そのため、amiiboの中でどのように情報が格納されているかについての仕様情報は不要です。UIDではなく、ちゃんと「マリオ」や「リンク」という識別情報を使いたい場合は、amiiboの仕様を理解してのハックが必要になります。

 

さて、これで前準備は終了です。次はゲームボーイポケットに組み込むためのソフトを書いて、ブレッドボードでプロト作成です。なお、組み込みでは小型のArduino Pro Miniを使うので、ライブラリの”devaccess_uart.cpp”で”Serial1″に変更したところは、”Serial”に戻しておきましょう。

ソースコードはこんな感じです。

#include <string.h>
#include <SoftwareSerial.h>
#include <DFPlayer_Mini_Mp3.h>
#include <Adafruit_NeoPixel.h>
#include <HkNfcRw.h>
#include <HkNfcA.h>

//------------------------------------------------------------------

SoftwareSerial mySerial(2, 3); // RX, TX
#define START_PIN     4
#define STOP_PIN      6
#define NEXT_PIN      8
#define PREV_PIN     10
#define VOL_UP_PIN   12
#define VOL_DOWN_PIN A0
#define LED_PIN       9

#define NUM_OF_LED  1
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUM_OF_LED, LED_PIN, NEO_GRB + NEO_KHZ400);

const uint32_t UID_MARIO    PROGMEM = 2063447XXX;
const uint32_t UID_LINK     PROGMEM = 3401931XXX;
const uint32_t UID_NES      PROGMEM = 721336XXX;

const uint8_t GAME_MARIO  = 0;
const uint8_t GAME_ZELDA  = 1;
const uint8_t GAME_MOTHER = 2;
const uint8_t GAME_NULL   = 255;

const uint8_t game_sounds[][4] PROGMEM = {{2,3,4,5},{6,7,8,9},{10,11,12,13}};

/*----------楽曲リスト----------
 1. GAME BOY 起動音
 2. マリオ コイン
 3. マリオ マリオ1地上BGM
 4. マリオ マリオ3アスレチックBGM
 5. マリオ マリオ64スライダーBGM
 6. ゼルダ 謎解き音
 7. ゼルダ ゼルダの子守唄
 8. ゼルダ 大海原
 9. ゼルダ ガノンドロフ
10. MOTHER 勝利BGM
11. MOTHER EightMelodies
12. MOTHER SmileAndTears
13. MOTHER 愛のテーマ
  ----------楽曲リスト----------*/

#define VOLUME_MAX 30
#define VOLUME_MIN  0

#define ON  LOW
#define OFF HIGH

uint8_t game_index  = GAME_NULL;
uint8_t music_index = 0;
uint8_t volume = 20;
uint8_t lastStartState   = OFF;
uint8_t lastStopState    = OFF;
uint8_t lastNextState    = OFF;
uint8_t lastPrevState    = OFF;
uint8_t lastVolUpState   = OFF;
uint8_t lastVolDownState = OFF;
boolean isPlaying = false;
//------------------------------------------------------------------

const uint8_t LED_RING_ALL_OFF PROGMEM = 0;
const uint8_t LED_RING_ALL_ON  PROGMEM = 1;
const uint8_t LED_RING_FADE    PROGMEM = 2;
const uint8_t LED_RING_CIRCLE  PROGMEM = 3;
const uint8_t LED_RING_BRIGHTNESS_MAX = 128; // 処理の都合上、この値は最大245までとする
const uint8_t LED_RING_BRIGHTNESS_MIN = 10;  // 処理の都合上、この値は最小10までとする
const int LED_RING_FADE_INTERVAL   = 40; //ms
const int LED_RING_CIRCLE_INTERVAL = 20; //ms

uint32_t last_uid    = 0;
uint32_t current_uid = 0;
// Type A MIFARE UltralightのUIDは7byte固定
// (CheckByteを入れたら9byteだが、戻り値には含まれない)
uint8_t uid[] = {0, 0, 0, 0, 0, 0, 0}; 

const uint8_t UID_RESET_COUNT PROGMEM = 5;
uint8_t not_detected_count = 0;
uint8_t detected_count = 0;

unsigned long led_ring_last_control_time = 0;
uint8_t current_led_ring_mode = 0;
int current_led_ring_r = 0; // 計算の都合でマイナスになることあり
int current_led_ring_g = 0; // 計算の都合でマイナスになることあり
int current_led_ring_b = 0; // 計算の都合でマイナスになることあり
uint8_t new_led_ring_mode = 0;
int new_led_ring_r = 0;
int new_led_ring_g = 0;
int new_led_ring_b = 0;
int led_ring_index = 0;
boolean led_ring_fade_in = true;
boolean led_ring_pattern_changed = false;
//------------------------------------------------------------------

void set_led_ring_pattern(uint8_t mode, uint8_t r, uint8_t g, uint8_t b){
  if(new_led_ring_mode != mode || new_led_ring_r != r || new_led_ring_g != g || new_led_ring_b != b){
    new_led_ring_mode = mode;
    new_led_ring_r = r;
    new_led_ring_g = g;
    new_led_ring_b = b;
    led_ring_pattern_changed = true;
  }
}

void exec_led_ring_pattern(){

  if(led_ring_pattern_changed){
    // 初回設定
    current_led_ring_mode = new_led_ring_mode;
    current_led_ring_r = new_led_ring_r;
    current_led_ring_g = new_led_ring_g;
    current_led_ring_b = new_led_ring_b;
    led_ring_pattern_changed = false;
  }else{
    // 現在選択中のモードで点灯を継続する
    unsigned long now = millis();

    if(current_led_ring_mode == LED_RING_ALL_OFF){//--------------------------------------------------------------
      // すべてOFF
      for(uint8_t i=0;i<NUM_OF_LED;i++){
        pixels.setPixelColor(i, pixels.Color(0,0,0)); 
      }
      pixels.show();
    }else if(current_led_ring_mode == LED_RING_ALL_ON){//--------------------------------------------------------------
      // すべてを指定のカラーでON
      for(uint8_t i=0;i<NUM_OF_LED;i++){
        pixels.setPixelColor(i, pixels.Color(current_led_ring_r,current_led_ring_g,current_led_ring_b)); 
      }
      pixels.show();    
    }else if(current_led_ring_mode == LED_RING_FADE){//--------------------------------------------------------------
      // すべてを指定の色でゆっくり明るさを変化させる  
      if(now - led_ring_last_control_time > LED_RING_FADE_INTERVAL){

        if(led_ring_fade_in){
          current_led_ring_r += 10;
          current_led_ring_g += 10;
          current_led_ring_b += 10;
        }else{
          current_led_ring_r -= 10;
          current_led_ring_g -= 10;
          current_led_ring_b -= 10;
        }

        // 現在、値が最も大きい色を確認(0:赤, 1:緑, 2: 青)
        uint8_t max_color_index = 0;
        int max_color = current_led_ring_r;

        if(max_color < current_led_ring_g){
          max_color_index = 1;
          max_color = current_led_ring_g;
        }
        if(max_color < current_led_ring_b){
          max_color_index = 2;
          max_color = current_led_ring_b;
        }

        switch(max_color_index){
        case 0:
          if(current_led_ring_r >= LED_RING_BRIGHTNESS_MAX){
            led_ring_fade_in = false; 
          }else if(current_led_ring_r <= LED_RING_BRIGHTNESS_MIN){
            led_ring_fade_in = true;
          }
          break;
        case 1:
          if(current_led_ring_g >= LED_RING_BRIGHTNESS_MAX){
            led_ring_fade_in = false; 
          }else if(current_led_ring_g <= LED_RING_BRIGHTNESS_MIN){
            led_ring_fade_in = true;
          }
          break;
        case 2:
          if(current_led_ring_b >= LED_RING_BRIGHTNESS_MAX){
            led_ring_fade_in = false; 
          }else if(current_led_ring_b <= LED_RING_BRIGHTNESS_MIN){
            led_ring_fade_in = true;
          }
          break;
        }
        // この時点で負の値になっているものについては、0を代入する
        int temp_r = (current_led_ring_r < 0) ? 0: current_led_ring_r;
        int temp_g = (current_led_ring_g < 0) ? 0: current_led_ring_g;
        int temp_b = (current_led_ring_b < 0) ? 0: current_led_ring_b;

        // すべてのLEDに(ほぼ)同時に色の設定を反映させる
        for(uint8_t i=0;i<NUM_OF_LED;i++){
          pixels.setPixelColor(i, pixels.Color(temp_r,temp_g,temp_b));
        }
        pixels.show();

        led_ring_last_control_time = now;
      }
    }else if(current_led_ring_mode == LED_RING_CIRCLE){//--------------------------------------------------------------
       // 指定の色でサークルを描く  

      if(now - led_ring_last_control_time > LED_RING_CIRCLE_INTERVAL){
        // サークル用インデックス作成
        uint8_t led_ring_index_back1 = led_ring_index - 1;
        led_ring_index_back1 = (led_ring_index_back1 < 0) ? led_ring_index_back1+NUM_OF_LED: led_ring_index_back1;
        uint8_t led_ring_index_back2 = led_ring_index - 2;
        led_ring_index_back2 = (led_ring_index_back2 < 0) ? led_ring_index_back2+NUM_OF_LED: led_ring_index_back2;
        uint8_t led_ring_index_back3 = led_ring_index - 3;
        led_ring_index_back3 = (led_ring_index_back3 < 0) ? led_ring_index_back3+NUM_OF_LED: led_ring_index_back3;

        for(uint8_t i=0;i<NUM_OF_LED;i++){
          if(i == led_ring_index){
            pixels.setPixelColor(i, pixels.Color(current_led_ring_r,current_led_ring_g,current_led_ring_b));
          }else if(i == led_ring_index_back1){
            pixels.setPixelColor(i, pixels.Color(current_led_ring_r/2,current_led_ring_g/2,current_led_ring_b/2));
          }else if(i == led_ring_index_back2){
            pixels.setPixelColor(i, pixels.Color(current_led_ring_r/4,current_led_ring_g/4,current_led_ring_b/4));
          }else if(i == led_ring_index_back3){  
            pixels.setPixelColor(i, pixels.Color(current_led_ring_r/8,current_led_ring_g/8,current_led_ring_b/8));
          }else{
            pixels.setPixelColor(i, pixels.Color(0,0,0));
          }       
        }
        pixels.show();

        led_ring_index++;
        if(led_ring_index >= NUM_OF_LED){
          led_ring_index = 0;
        }

        led_ring_last_control_time = now;
      }

    }
  }
}

void setup()
{
  //Serial.begin(115200);
  //Serial.println(F("Initialize Start ..."));
  //----------ボタン初期化----------
  pinMode(START_PIN,    INPUT_PULLUP);
  pinMode(STOP_PIN,     INPUT_PULLUP);
  pinMode(NEXT_PIN,     INPUT_PULLUP);
  pinMode(PREV_PIN,     INPUT_PULLUP);
  pinMode(VOL_UP_PIN,   INPUT_PULLUP);
  pinMode(VOL_DOWN_PIN, INPUT_PULLUP);
  //----------NFC初期化----------
  bool ret = HkNfcRw::open();
  while (!ret) {}
  //Serial.println(F("NFC Ready!"));
  //----------DFPlayer初期化----------
  mySerial.begin (9600);
  mp3_set_serial (mySerial);  //set softwareSerial for DFPlayer-mini mp3 module 
  mp3_set_volume (volume);
  //Serial.println(F("DFPlayer Ready!"));
  //----------フルカラーLED初期化----------
  pixels.begin();
  pixels.show(); // Initialize all pixels to 'off'
  //Serial.println(F("LED Ready!"));
  //-----起動音を鳴らす-----
  mp3_play(1);
}

void loop()
{
  //////////////////// NFCの処理 ///////////////////////
  HkNfcRw::Type type = HkNfcRw::detect(true, false, false);
  if(type == HkNfcRw::NFC_A) {
    // Type A タグが見つかったとき
    //Serial.println(F("Type A Detect!"));

    if(HkNfcRw::getNfcId(uid)){      

      current_uid = uid[6];
      for(int i=5; i>=0; i--){
        current_uid <<= 8; current_uid |= uid[i];
      }
      //Serial.print(F("UID:"));
      //Serial.println(current_uid);   

      // UIDが変わった時のみ処理
      if(last_uid != current_uid){
        if(current_uid == UID_MARIO){
          set_led_ring_pattern(LED_RING_CIRCLE, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MIN);
        }else if(current_uid == UID_LINK){
          set_led_ring_pattern(LED_RING_CIRCLE, LED_RING_BRIGHTNESS_MIN, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MIN);
        }else if(current_uid == UID_NES){
          set_led_ring_pattern(LED_RING_CIRCLE, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MIN, LED_RING_BRIGHTNESS_MIN);
        }              
        last_uid = current_uid; // 現在検出中のUIDを保持
        detected_count = 0; // 検出カウントの初期化
      }

      not_detected_count = 0; // 未検出カウントの初期化
      detected_count++;

      if(detected_count == 10){        
        if(current_uid == UID_MARIO){
          set_led_ring_pattern(LED_RING_ALL_ON, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MIN);
        }else if(current_uid == UID_LINK){
          set_led_ring_pattern(LED_RING_ALL_ON, LED_RING_BRIGHTNESS_MIN, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MIN);
        }else if(current_uid == UID_NES){
          set_led_ring_pattern(LED_RING_ALL_ON, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MIN, LED_RING_BRIGHTNESS_MIN);
        }          
      }else if(detected_count == 11){   
        if(current_uid == UID_MARIO){
          game_index = GAME_MARIO;   
        }else if(current_uid == UID_LINK){
          game_index = GAME_ZELDA;
        }else if(current_uid == UID_NES){
          game_index = GAME_MOTHER;
        }
        music_index = 0;
        mp3_play(pgm_read_byte(&game_sounds[game_index][music_index]));
        music_index++;
      }
      if(detected_count > 11){
        // detected_countをカウントアップさせない
        detected_count = 12;  
      }  

    }else{
      //Serial.println(F("Failed to get UID."));
    }
  } else {
    // Type A タグが見つからなかったとき
    // (ライブラリの都合か、タグを見つけた後も検出->未検出->検出->未検出-> ... という動作になる)

    // 未検出カウントが規定回数連続すると、UIDを初期化する。
    // (未検出カウントはタグの検出時とUIDの初期後に初期化される)

    //Serial.println(F("No Tag."));

    not_detected_count++;
    if(not_detected_count == UID_RESET_COUNT){
      //Serial.println(F("Reset UID."));
      last_uid = 0;
      not_detected_count = 0;

      if(isPlaying){
        mp3_pause();
        isPlaying = false;
      }
      music_index = 0;
      game_index  = GAME_NULL;

      set_led_ring_pattern(LED_RING_FADE, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MAX, LED_RING_BRIGHTNESS_MAX);
    }
  }

  //////////////////// DFPlayerの処理 ///////////////////////
  uint8_t startState = digitalRead(START_PIN);
  if(lastStartState == OFF && startState == ON && last_uid != 0){ 
    if(!isPlaying){
      mp3_play(pgm_read_byte(&game_sounds[game_index][music_index]));
      isPlaying = true;
    }
  }
  lastStartState = startState;

  uint8_t stopState = digitalRead(STOP_PIN);
  if(lastStopState == OFF && stopState == ON && last_uid != 0){
    if(isPlaying){
      mp3_pause();
      isPlaying = false;
    }    
  }
  lastStopState = stopState;

  uint8_t nextState = digitalRead(NEXT_PIN);
  if(lastNextState == OFF && nextState == ON && last_uid != 0){
    if(music_index == 3){
      music_index = 1;
    }else{
      music_index++;
    }
    mp3_play(pgm_read_byte(&game_sounds[game_index][music_index]));
    isPlaying = true;
  }
  lastNextState = nextState;

  uint8_t prevState = digitalRead(PREV_PIN);
  if(lastPrevState == OFF && prevState == ON && last_uid != 0){
    if(music_index == 1){
      music_index = 3;
    }else{
      music_index--;
    }
    mp3_play(pgm_read_byte(&game_sounds[game_index][music_index]));
    isPlaying = true;
  }
  lastPrevState = prevState;

  uint8_t volUpState = digitalRead(VOL_UP_PIN);
  if(lastVolUpState == OFF && volUpState == ON && last_uid != 0){
    if(volume < VOLUME_MAX){
      volume++;
      mp3_set_volume (volume);
    }
  }
  lastVolUpState = volUpState;

  uint8_t volDownState = digitalRead(VOL_DOWN_PIN);
  if(lastVolDownState == OFF && volDownState == ON && last_uid != 0){
    if(volume > VOLUME_MIN){
      volume--;
      mp3_set_volume (volume);
    }
  }
  lastVolDownState = volDownState;

  if(!isPlaying){
    exec_led_ring_pattern();
  }

  delay(20);
}

前のamiiboオルゴールのソースを流用しているので、LEDの発光についての記述がちょっとオーバーかもしれません。

これをArduino Pro Miniに焼き込みます。ただ、これまで愛用していた3.3V 8MHz版のPro Miniでは、残念ながらRC-S620Sが正常に動作してくれなかったので、今回は5V 16MHz版のPro Miniを使用します。

配線はだいたいこんな感じで。

th_amiibo-music-box-pocket-5

Pro Miniを5V版に変更したことで、電源も何とかして5Vのものを用意しなくてはいけないのではないかと心配したのですが、幸い、3.7Vのリチウムイオンポリマー電池でも動作してくれました。

 簡単な動作確認をした結果がコチラです。

 

もうちょっと動作を詰めたいところもなくはないですが、今回はあまり時間をかけたくないので、これで良しとします。

 

ソフト編は以上で終了です。次回、ハード側に作成物を組み込んでいきます。多分こっちの方が大変です。