The jonki

呼ばれて飛び出てじょじょじょじょーんき

Maker Faire Tokyo 2013 でレゴへのプロジェクションマッピングネタで出展してきた【ハード編】

先日のMaker Faire Tokyo 2013にid:hecomiと二人でMont.Blanc.Pjというチーム名で参加し、凸Pというレゴのプロジェクションネタで出展してきました。



既にid:hecomiがソフトウェア部分のところをかなり細かく書いているので、Arduinoの通信周りやプロジェクションする側の筐体作成までの困難な道のりを書いていければと思います。あんまり派手な部分はないので面白くないかもしれません(笑)

反響

Twitterや大手メディア様から反応がありました。本当に嬉しい限りです。ニコニコ学会マッドネス部門のご推薦も頂きました。

レゴの検出解説

既にid:hecomiがレゴの検出方法について詳しい記事を書いています。

筐体

筐体には写真にあるように銀ラックを使っています。色々な筐体を考えていたんですが、意外とこれが良かったです。そしてこの銀ラックによくポスターなんかを貼る5mmの厚さのボードで前面を隠すような箱を作って被せています。

  • 組み立て/分解が簡単
  • 安い(2000円弱)
  • 構造がしっかりしていて、しかも台を追加して内部のガジェットの整理が付きやすい
工夫

外側ができたことで結構良い感じになったのですが、レゴの取り付けなんかを行う際に結構ずれるんですね。キャリブが命のシステムだったのでできるだけ台を安定化させる必要がありました。色々考えた結果以下の3つをやっています。展示当日こどもたちにかなり激しく使われましたが、ほとんど動くことはありませんでした。今思えば台の安定化はかなり大切なポイントでした。

  • 銀ラックの足に滑り止めのスポンジを置く
  • おもり(今回はダンベルw)を下の方に置く
  • 外の白いカバーを銀ラックにヒモできつく固定する。真ん中の台のところに100均で買ったケーブルをまとめるもの(両面テープで付く)を白いカバーにつけてヒモが引っかかるようになっています。

Arduinoからの制御

今回Arduino2台(UNOとDuemilanove)を使って、DCモーター(UNO)、サーボ(UNO)、LED(Duemilanove)をPCからUSBのシリアル通信で操作しています。イーサネットシールドも持っていたのですが、シリアル通信は下記の3つの理由で採択しています。

  • 今回は無線化したりスマートにする必要がなかったので、USBでPCをつないでもデモの見栄えに影響がない
  • イーサネットシールドが高価
  • ネットワーク環境を構築する必要がないので、手間が少なく当日のネットワークトラブルの要因を排除できる
サーボ

サーボにはタミヤのユニバーサルアーム(オレンジの)が付いており、そしてその先に非常に細い糸をつけています。これをドッスンのブロックにつけることでドッスンの上下運動を実現しています。かなりアナログな方法です。
サーボのコントロールはArduinoにはサーボコントロール用のライブラリがあるので簡単です。サーボは配線が3本あり、信号線と電源(プラスとマイナス)です。解説は下記サイトを参考にして下さい。ただし電源は安定化のため外部電源の5Vを使っています。

DCモーター

DCモーターにはレゴのテックシリーズのクレーンなどに使われているキャタピラを使っています。通常の6角の棒とレゴがもちろんそのままでは合わないので、接合部にグルーガンを流し込んで無理矢理固定しています。レゴは基本的にレゴとしか接合しないので、こういうとき大変です。あんまり力がかかると簡単に外れちゃいます。なんか良い方法ほかにないですかね?


ちなみにDCモーターのArduinoの制御はサーボより少し複雑です。今回はTA7291PというICを使って制御します。ただし下記のサイトの例では3ピンが使われていますが、今回は既にサーボで3ピンが使われているので、1、2、3ピンと同じくPWMが使われる9、10、11ピンを使いました。

サーボとDCは下記のようなスケッチで1台のArduinoで制御しています。そのためデータは"d123"や"s23"といったように、単語の始めにサーボかDCを判断する文字、次にそれぞれが扱う数字を送ることでどちらのデバイスを制御するか判断しています。SoftwareSerialはデバッグ用となどで便利です。うまく動かないときは地道にprintデバッグです。

#define IN1PIN 9
#define IN2PIN 10
#define OUTPIN 11

#define SERVOPIN 3
#define BUFFERSIZE 32
#include <Servo.h>
#include "SoftwareSerial.h"

Servo servo;
// SoftwareSerial softSerial(5, 6);
char buffer[BUFFERSIZE];
void setup() 
{ 
    Serial.begin(9600);
    //softSerial.begin(9600);

	pinMode(IN1PIN, OUTPUT);
	pinMode(IN2PIN, OUTPUT);

    servo.attach(SERVOPIN);
} 

int ReadData(char data[]) 
{
    int val = 0;
    int i = 0;
    buffer[0] = '\0';
    while(1) {
        if(Serial.available()) {
            buffer[i] = Serial.read();
            //softSerial.println("read data " + String(i) + " = " + String(buffer[i]));
            if('\0' == buffer[i]) {
                //softSerial.println("end of data");
                val = atoi(buffer);
                break;
            }
            i++;
            if(i >= BUFFERSIZE) break;
        }
    }

    //softSerial.println("ReadData: " + String(val));
    return val;
}

void loop() 
{
    if(Serial.available()) {
        char head = Serial.read();
        if('d' == head) {
        	//softSerial.println("Motor Mode");
        	int val = ReadData(buffer);
        	//softSerial.println("Motor val: " + String(motorVal));
        	MotorDrive(val);
        } else if('s' == head) {
        	//softSerial.println("Servo Mode");
        	int val = ReadData(buffer);
        	//softSerial.println("Servo val: " + String(servoVal));
        	ServoDrive(val);
        }
    }
} 

void MotorDrive(int val) 
{
	if(val > 10) {
		digitalWrite(IN1PIN, HIGH);
		digitalWrite(IN2PIN, LOW);
        analogWrite(OUTPIN, val); 

	} else if(val < -10) {
        	digitalWrite(IN1PIN, LOW);
        	digitalWrite(IN2PIN, HIGH);
        	analogWrite(OUTPIN, -1.0f * val); 
	} else { 
		digitalWrite(IN1PIN, LOW);
		digitalWrite(IN2PIN, LOW);
	}
}

void ServoDrive(int val)
{
    //softSerial.println("ServoDrive: " + String(val));
    servo.write(val);   
}
LED

LEDはゲーム用やシステムの動作確認用などに多く使うかなぁと準備していましたが、結局時間がなくてLEDは1つしか使いませんでした(コインブロック)。TLC5940というICで16個まで1つのArduinoで簡単に制御できますが、今回に限っては完全にスペックオーバーでした。

#include "Tlc5940.h"
#include "SoftwareSerial.h"

#define RET_OK 0
#define RET_ERROR -1

#define MAX_BRIGHTNESS 4095 // 0-4095 
#define POWAN_SPEED 100     // microsecond 
#define BUFFERSIZE 32

#define LED_NUM 16

TLC_CHANNEL_TYPE ch; 
int targetChannel;  // current channel 
int targetBrightness;   // current brightness 
char buffer[BUFFERSIZE];
int LED[LED_NUM];


SoftwareSerial softSerial(5, 6);

void setup() { 
    Serial.begin(9600);
    softSerial.begin(9600);

    Tlc.init(); 
    Tlc.clear(); 
    for(int i = 0; i < LED_NUM; i++) { 
        LED[i] = 0;
        Tlc.set(i, LED[i]); 
    } 
    Tlc.update(); 
} 

void loop() 
{
    if(Serial.available()) {
        int ret = ReadData(buffer, targetChannel, targetBrightness);
        if(ret == RET_OK) {
            if(targetChannel >= 0 && targetChannel < LED_NUM) {
                LED[targetChannel] = targetBrightness;
            }
        }
    }

    for(int i = 0; i < LED_NUM; i++) { 
        Tlc.set(i, LED[i]);
        // Tlc.set(i, (sin(micros() / 1000000.0f) + 1.0f) * 2000);
        Tlc.update();
        delayMicroseconds(POWAN_SPEED); 
    } 
} 

int ReadData(char *data, int &channel, int &brightness) 
{
    int ret = RET_ERROR;

    int i = 0;
    buffer[0] = '\0';
    while(1) {
        if(Serial.available()) {
            buffer[i] = Serial.read();
            softSerial.println("read data " + String(i) + " = " + String(buffer[i]));
            if('\0' == buffer[i]) {
                sscanf(buffer, "%d,%d", &channel, &brightness);
                softSerial.println("ReadData: " + String(channel) + "," + String(brightness));
                if(channel < 0 || channel >= LED_NUM) ret = RET_ERROR;
                else if(brightness < 0 || brightness > MAX_BRIGHTNESS) ret = RET_ERROR;
                else ret = RET_OK;
                break;
            }
            i++;
            if(i >= BUFFERSIZE) {
                ret = RET_ERROR;
                break;
            }
        }
    }

    return ret;
}
シリアル通信のホスト側

今回メインのゲームがUnityであるため、UnityとArduinoシリアル通信を行っています。PC側にはUSBケーブルが2本、それぞれUNOとDuemilanoveが繋がっていて、同時に様々なメッセージが送れます。
今回はライブラリをid:hecomiに提供するということで、シングルトン風にそれぞれのインスタンスを取得し、簡単なAPIを叩いてもらうようにしました。特にSerialの通信周りは使う側からは隠れています。(と、いいつつポートはあらかじめ指定する必要があるんですが)
長くなってしまうのでシリアル通信部をラップするSerialHandler.cs、DCモーターのライブラリ(DCMotorController.cs)、そしてその使い方のソースコードを載せます。LEDとサーボもDCモーターのライブラリと基本的に同じです。

  • SerialHandler.cs
using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System.Collections.Generic;
using System.Text;

public enum ArduinoType {
	DUEMILANOVE,
	UNO,
}

public class SerialHandler {
	const string DuemilanovePort = "/dev/tty.usbserial-A800ey7d";
	const string UnoPort 		 = "/dev/tty.usbmodemfa1311";
	const int BaudRate 			 = 9600;
	
	static private Dictionary<ArduinoType, SerialHandler> m_handlerDict = new Dictionary<ArduinoType, SerialHandler>();
	
	private SerialPort 		m_serial;
	
	private SerialHandler(ArduinoType type)
	{
		switch(type)
		{
		case ArduinoType.DUEMILANOVE:
			Debug.Log("Try to open DUEMILANOVE Port " + DuemilanovePort);
			m_serial = new SerialPort(DuemilanovePort, BaudRate, Parity.None, 8, StopBits.One);
			break;
		case ArduinoType.UNO:
			Debug.Log("Try to open UNO Port " + UnoPort);
			m_serial = new SerialPort(UnoPort, BaudRate, Parity.None, 8, StopBits.One);
			break;
		}
		
		Start();
	}
	
	static public SerialHandler GetSerialHandler(ArduinoType type)
	{
		if(type != ArduinoType.DUEMILANOVE && type != ArduinoType.UNO) return null;
		
		SerialHandler handler = null;
		if(!m_handlerDict.ContainsKey(type)) {
			handler = new SerialHandler(type);
			m_handlerDict.Add(type, handler);
		} else {
			handler = m_handlerDict[type];	
		}
	
		return handler;
	}
	
	private void OnSerialDataReceived(object sender, SerialDataReceivedEventArgs e)
	{
		Debug.Log("OnSerialDataReceived");
		SerialPort port = (SerialPort)sender;
		byte[] buf = new byte[1024];
		int len = port.Read(buf, 0, 1024);
		string s = Encoding.GetEncoding("Shift_JIS").GetString(buf, 0, len);
		Debug.Log(s);
	}

	private void OnSerialErrorReceived(object sender, SerialErrorReceivedEventArgs e)
	{
		Debug.Log("Serial port error: " + e.EventType.ToString ("G"));
	}
	
	private void IOErrorHandler()
	{
		Debug.LogError("IOException!!!!");
		Stop();
	}
	
	private void Start() {
		if(m_serial != null) {
			if(m_serial.IsOpen) {
				Debug.LogError("Failed to open Serial Port, already open!");
				m_serial.Close();
			} else {
				try
				{
					m_serial.DataReceived += OnSerialDataReceived;
					m_serial.ErrorReceived += OnSerialErrorReceived;
					m_serial.Open();
					m_serial.DtrEnable = true;
					m_serial.RtsEnable = true;
					m_serial.ReadTimeout = 50;
					Debug.Log("Open Serial port");
				}
				catch(System.IO.IOException)
				{
					IOErrorHandler();
				}
			}
		}
	}
	
	public void Stop() 
    {
		if(m_serial != null) {
			Debug.Log("CLose Serial Port");
			m_serial.Close();
		}
    }
		
	public string CreateSendData<T>(string header, T data)
	{
		return header + data.ToString() + "\0";
	}
	
	public void SendData(string data)
	{
		try
		{
			m_serial.Write(data);
		}
		catch(System.IO.IOException)
		{
			IOErrorHandler();
		}
	}
	
}
  • DCMotorController.cs
using UnityEngine;
using System.Collections;

public class DCMotorController {
	private const string DCMotorHeader = "d";  // Arduinoの制御用ヘッダ
	
	static private DCMotorController s_instance = null;
	
	private SerialHandler m_handler = null;
	
	private DCMotorController()
	{
		m_handler = SerialHandler.GetSerialHandler(ArduinoType.UNO);
	}
	
	static public DCMotorController Instance
	{
		get 
		{
			if(s_instance == null)
			{
				s_instance = new DCMotorController();	
			}
			return s_instance;
		}
	}
	
	/// <summary>
	/// Sets the DC motor speed.
	/// </summary>
	/// <param name='speed'>
	/// Speed. Reverse Rot: -255 to -11, Stop: -10 to 10, Forward Rot: 11 to 255
	/// </param>
	public void SetSpeed(int speed)
	{
		if(m_handler != null) {
			var data =  m_handler.CreateSendData<float>(DCMotorHeader, speed);
			m_handler.SendData(data);
		}
	}
	
	public void Quit()
	{
		if(m_handler != null) {
			m_handler.Stop();	
			m_handler = null;
		}
	}
}
  • 使い方
using UnityEngine;
using System.Collections.Generic;

public class SerialTestClient : MonoBehaviour {
    private DCMotorController m_dcController;
    private ServoController   m_servoController;
    private LEDController     m_ledController;
    
    public int m_servoValue = 0;
    public int m_motorValue = 0;
    public List<int> m_ledValueList = new List<int>();
    
    // Use this for initialization
    void Start () {
        m_dcController = DCMotorController.Instance;
        m_servoController = ServoController.Instance;
        m_ledController = LEDController.Instance;
        
        for(int i = 0; i < 16; i++) {
            m_ledValueList.Add(0);
        }
    }
    
    // Update is called once per frame
    void Update () {
    }
                
    void OnGUI() {
        // Servo Update
        var val = 0;
        val = (int)GUI.HorizontalSlider(new Rect(10, 10, 200, 40), m_servoValue, 0, 180);
        if(val != m_servoValue) {
            m_servoValue = val;
            if(m_servoController != null) {
                m_servoController.SetDegree(m_servoValue);  
            }
        }
        GUI.Label(new Rect(220, 10, 100, 40), "Servo: " + m_servoValue.ToString());
        
        // DCMotor Update
        val = (int)GUI.HorizontalSlider(new Rect(10, 50, 200, 40), m_motorValue, -255, 255);
        if(val != m_motorValue) {
            m_motorValue = val;
            if(m_dcController != null) {
                m_dcController.SetSpeed(m_motorValue);  
            }
        }
        GUI.Label(new Rect(220, 50, 100, 40), "DCMotor: " + m_motorValue.ToString());
        
        // LED Update
        for(int i = 0; i < m_ledValueList.Count; i++) {
            var rect = new Rect(10 + i * 35, 90, 40, 150);
            val = (int)GUI.VerticalSlider(rect, m_ledValueList[i], 0, 100);
            if(val != m_ledValueList[i]) {
                m_ledValueList[i] = val;
                if(m_ledController != null) {
                    m_ledController.SetLedWithBrightness(i, m_ledValueList[i]); 
                }
            }
            GUI.Label(rect, "LED" + i.ToString() + ": " + m_ledValueList[i].ToString());
        }
    }
    
    void OnApplicationQuit() 
    {
        if(m_dcController != null) {
            m_dcController.Quit();
        }
        if(m_servoController != null) {
            m_servoController.Quit();
        }
        if(m_ledController != null) {
            m_ledController.Quit();
        }
    }
}

最後に

自分もid:hecomiもソフトウェア屋なのでハード面、デザイン面、もう少しがんばりたかったなぁというのが本音です。が、反響も大きくいろんなところから結構声がかかっていたりして結構嬉しいです。今回は2ヶ月ぐらいしか時間がなかったので来年のMakerや他の展示にも向けてどんどん頑張っていきたいところです。