注意:本記事では紫外線LEDを使用します。紫外線LEDは見た目は薄い青紫色に光りますが、紫外線は目に見えないため見た目よりもかなり強い光を発しています。紫外線LEDの光を直接覗き込むようなことは絶対にしないでください。回復不能の視覚障害がおきる可能性があります。特に小さいお子様などが近くにいる場合などは注意が必要です。(ブラックライトや紫外線灯と同様の注意が必要です)
#ネタ出し
今年もやってまいりましたtoio™️で作ってみた友の会Advent Calender。なんか記事かかないとなー、Maker Faire Tokyo 2020出展顛末記を前編後編にして2枠は書くとして、なんか新ネタを一つぐらい、といろいろ考えました。
プレイマット上にプロジェクタでプロジェクションマッピング、というネタはすでにたくさんの方々が製作済ですし、Maker Faire Tokyo 2020でもDiorama Shootingというすばらしい作品が出ています。じゃあ、レーザー投影とかどうだろう? toio™️ core cubeにミラーをつけてガルバノミラーを作ってレーザーポインタの光を投影、とか。とはいえtoio™️ core cubeが素早く正確に回転できるとはいえ、レーザーの残像がきれいに見えるレベルでの高速反復動作は難しいだろうし、そもそもミラーを2セット直交配置する方法が思いつかない。
光で描く...そういえばプレイマット上に移動した軌跡がタートルグラフィック的に残らないのはちょっと不満に思っていました。(toio™️のビジュアルプログラミングでは画面上には軌跡が描けますが。)
とはいえ、ペンとかでマット上に描いてしまうと消せないし... そうだ! 蓄光塗料と紫外線LEDを使えば軌跡が描けるし、時間がたてば消える!
というわけで開発用プレイマットに蓄光塗料を塗って試してみることにしました。
#蓄光塗料
amazonで「蓄光塗料」で検索。いろいろあるのですが、紙の開発用プレイマットに塗るので水性のものがよかろうと「シンロイヒ 水性夜光ペイント AQUA GLOW グリーン」を選択。ついでに塗るための刷毛も注文しました。
不安材料としては、蓄光塗料は原理上、顔料であることです。(染料の蓄光塗料は探しましたが見当たらなかった) 顔料なので細かい不透明な粒が紙の上に残ります。プレイマットには微小なドットが印刷してあり、これをtoio™️ core cube底面の読み取りセンサが読み取って絶対位置を得られるわけですが、顔料の粒子がドットの印刷をかくしてしまう恐れがあります。塗料としては隠蔽性が高いことは利点ではあるのですが、顔料塗料の場合それがデメリットになります。
toio™️で作ってみた友の会でも、「プレイマットになにか描く場合、顔料は良くない。染料系インクがいい。」というレポートを聞いたような気がします。が、カラーコピー機で印刷してうまくいった(ただし、薄めに印刷しないとだめ)というお話も聞いたので、まあやってみるか、水性だから水で薄めれば薄くなるし、ということでやってみました。
プレイマットに塗る
とりあえず単品でtoio™️ core cubeを買うと付属してくる簡易マット(折り目がついているのでちょっと使いづらい)を実験台として、濃度を変えて塗ってみます。原液のまま、水で2倍希釈(50%)、水で4倍希釈(25%)の3パターン。
塗料の仕様上は一瓶20gで0.02㎡(2回塗り)ですが、2回も塗ったら濃すぎるのと、0.02㎡では20cm x 10cm程度しか塗れません。A3サイズのプレイマット全体ぐらいは塗りたいので1回塗りですませます。
塗ってみると粉っぽいというか顔料ですねーこれ、という感じ。水で薄めればそれなりに薄くなります。水彩絵の具っぽい感じ。
塗ったあとを目視する限りは微小ドットが塗りつぶされずに見えてはいるのですが...さてどうなるか。
位置が読み取れるか?
読み取りセンサのnotifyでtoioIDを表示するpythonスクリプトを書いて実験。toio™️ core cubeをマットの塗料を塗った部分に載せてみました。toioID(座標や角度))は読めてはいるようです。toio™️ core cubeをくるくる回すと読み取った角度値が変化していました。
1回塗り、50%濃度、25%濃度のどれでも読み取りができたので、これはいける! あとは光るかどうかだなと。
塗料が薄くても光る?
蓄光塗料を光らせるためには紫外線LEDを使います。本当は秋葉原の秋月電子に買いにいきたかったかったのですが、コロナ禍のおり、秋月電子は日曜祭日はお休みで週末に買いにいけるのは土曜のみ。なかなか買い物に行くチャンスがありません。だいぶ割高にはなりますが、塗料を買うついでに紫外線LEDもamazonで買ってしまいました。(uxcell UV発光ダイオード クリアラウンドパープル紫外線 LEDダイオードDIP照明 電球ランプ電子部品 3個 5mm 365-370nm)
今回、toio™️ core cubeの移動制御と紫外線LEDの点灯制御にはM5stick-Cを使います。バッテリー搭載でBLEが使えますし、すでにtoio™️ core cubeを動かす事例も豊富です。また、紫外線LEDはVfが3.2〜3.4Vなので、3.3VのGPIOのM5stick-Cに直結でも光らせることができて簡単です。
G26にLEDの足の長い方(+)を、GNDに足の短い方(-)を差し込む
とりあえずこれで、G26をHighにすれば光らせることができます。M5Stick-Cのボタンを押すとG26をHighにするスケッチを書き込んで実験してみます。
M5Stick-Cのボタンを押しながら、手で紫外線LEDをプレイマットの塗料を塗った面の上で動かしてみます。
#define UVLED_PIN 26 // G26ピンを使う
#define UVLED_ON HIGH
#define UVLED_OFF LOW
M5.begin();
pinMode(UVLED_PIN, OUTPUT);
digitalWrite(UVLED_PIN, UVLED_OFF); // 消灯
digitalWrite(UVLED_PIN, UVLED_OFF); // 点灯
明るいところではほとんど見えませんが、照明を落として暗くすると、紫外線LEDが通った跡が残っているのがわかります。数分立つと消えます。
光りかたとしては原液塗りのところはわりとくっきり、50%のところはやや薄い、25%のところはだいぶ薄いというか光りかたにムラがある感じでした。
まあ、原液塗りでも位置と角度のtoioIDは読めたからいけるでしょ、よく光るほうが見栄えがいいから原液塗りでいっちゃえ、ということで、開発用プレイマット1枚全面に原液濃度で塗りました。A3一枚でほぼ1瓶全部使い切ります。
toio™️ core cubeとM5Stick-Cの準備
toio™️ core cube、トッププレート、M5Stick-C、紫外線LED、セロハンテープを用意
toio™️ core cubeにトッププレートをつけ、M5Stick-Cを載せてセロハンテープで固定したあと、紫外線LEDの足の長いほうをG26、短い方をGNDにに挿します
もう一度LEDのの足を曲げてtoio™️ core cubeの側面にくっつけ、走行中にぐらぐらしないようにセロテープで固定します。
この例ではtoio™️ core cubeの後ろ側の側面にLEDがくっつくようにしています。
目標指定付きモーター制御が動かない
さあ、あとは目標指定付きモーター制御を使って図形や文字を描くようにtoio™️ core cubeを制御すればよし!と、目標位置指定の移動を試しましたが...まともに動かない。orz
動こうとはするけどもなにかを見失っている感じででまったく目標位置に向かって動いて行ってはくれません。塗料が邪魔をしてプレイマットのドットが正しく読み取れていないようです。静止状態でそれなりに位置や角度が読めたのでいけるかと思ったのですが、目標指定付きモーター制御の場合は、移動しながらプレイマットのドットを読まないといけないため、条件が厳しいものと推測されます。また、スマホのカメラで撮影すると肉眼で見たよりも塗料が光って見えるため、センサーも光が強く見えていてドットの読み取りを光が阻害しているのかもしれません。
原液100%じゃなくて50%、25%に薄めたところはどうか?と最初に塗った簡易プレイマットで試してみてもやっぱりだめで、まともに目標指定付きモーター制御が動きませんでした。
目標指定が効かなくてもモーターは動く
目標位置指定移動が効かないなら、モーター速度と時間のみでなんとかすればいいじゃない、ということで、モーター速度と回転時間のみで動かしてみました。
動画(YouTube)
うずまきのコード
lspd = 80;
rspd = 60;
digitalWrite(UVLED_PIN, UVLED_ON);
while(rspd > 0){
setMotor(lspd, rspd, 0);
delay(800); // 800msごとに
lspd = lspd -5; // 左車輪スピードを落としながら回る
rspd = rspd - 20; // 右車輪は左車輪より一定数遅くする
}
setMotor(0, 0, 0); // 停止
digitalWrite(UVLED_PIN, UVLED_OFF);
四角形のコード
spd = 60; // 辺を描く速度
rotspd = 35; // 回転時の速度
d1 = 600; // 辺を描いて直進する時間(ms)
d2 = 440; // 約90°回転する時間(ms)
for(int i = 0 ; i < 4; i++){ // 一定時間進んだら90°回転を4回
digitalWrite(UVLED_PIN, UVLED_ON);
setMotor(spd, spd, d1);
delay(d1 + 100);
digitalWrite(UVLED_PIN, UVLED_OFF);
setMotor(rotspd, 0, d2);
delay(d2 + 100);
}
五芒星のコード
spd = 60; // 辺を描く速度
rotspd = 100; // 回転時の速度
d1 = 800; // 辺を描いて直進する時間(ms)
d2 = 290; // 約36°回転する時間(ms)
for(int i = 0 ; i < 5; i++){ // 一定時間進んだら36°回転を4回
digitalWrite(UVLED_PIN, UVLED_ON);
setMotor(spd, spd, d1);
delay(d1 + 100);
digitalWrite(UVLED_PIN, UVLED_OFF);
setMotor(rotspd, 0, d2);
delay(d2 + 100);
}
お団子?のコード
digitalWrite(UVLED_PIN, UVLED_ON);
spd = 50; // 串を描く速度
d1 = 500; // 串を描く時間
d2 = 780; // お団子を描く時間
rotspd = 40; // お団子を描く速度
for(int i = 0 ; i < 3; i++){
setMotor(spd, spd, d1); // 串を描く
delay(d1 + 100);
setMotor(rotspd, -rotspd, d2); //お団子を描く
delay(d2 + 100);
}
setMotor(spd, spd, d1);
delay(d1 + 100);
digitalWrite(UVLED_PIN, UVLED_OFF);
M5Stick-C用のArduinoスケッチのコード
以下をクリックして表示(基本的にkotobukiさんのnote記事「一定時間が経過したことを知らせしてくれるロボットをつくるのまるパクリです)
M5Stick-C用のArduinoスケッチのコード全部はここをクリック
#include <M5StickC.h>
#include <BLEDevice.h>
#define UVLED_PIN 26
#define UVLED_ON HIGH
#define UVLED_OFF LOW
unsigned int pattern = 0;
// toio Core Cube Service UUID
static BLEUUID coreCubeServiceUUID("10B20100-5B3B-4571-9508-CF3EFCD7BBAE");
// toio Core Cube Motor Characteristic UUID
static BLEUUID coreCubeMotorCharacteristicUUID("10B20102-5B3B-4571-9508-CF3EFCD7BBAE");
static BLERemoteCharacteristic *pCoreCubeMotorChar;
static BLEAdvertisedDevice *myDevice;
enum
{
SCANNING = 0,
READY_TO_CONNECT,
CONNECTED,
RUNNING,
DISCONNECTED,
NO_CORE_CUBE,
};
int state = SCANNING;
int lastState = state;
class MyClientCallback : public BLEClientCallbacks
{
void onConnect(BLEClient *client)
{
state = CONNECTED;
}
void onDisconnect(BLEClient *client)
{
state = DISCONNECTED;
}
};
BLEClient *pClient = NULL;
bool connectToCoreCube()
{
pClient = BLEDevice::createClient();
pClient->setClientCallbacks(new MyClientCallback());
pClient->connect(myDevice);
BLERemoteService *pRemoteService = pClient->getService(coreCubeServiceUUID);
if (pRemoteService == nullptr)
{
pClient->disconnect();
return false;
}
pCoreCubeMotorChar = pRemoteService->getCharacteristic(coreCubeMotorCharacteristicUUID);
if (pCoreCubeMotorChar == nullptr)
{
pClient->disconnect();
return false;
}
return true;
}
class MyAdvertisedDeviceCallback : public BLEAdvertisedDeviceCallbacks
{
void onResult(BLEAdvertisedDevice advertisedDevice)
{
Serial.print("BLEAdvertisedDevice: ");
Serial.println(advertisedDevice.toString().c_str());
if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(coreCubeServiceUUID))
{
BLEDevice::getScan()->stop();
myDevice = new BLEAdvertisedDevice(advertisedDevice);
state = READY_TO_CONNECT;
}
else
{
state = NO_CORE_CUBE;
}
}
};
void setup()
{
M5.begin();
pinMode(UVLED_PIN, OUTPUT);
digitalWrite(UVLED_PIN, UVLED_OFF);
M5.Lcd.setRotation(1);
M5.Lcd.setTextFont(2);
Serial.begin(115200);
BLEDevice::init("");
BLEScan *pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallback());
pBLEScan->setInterval(1349);
pBLEScan->setWindow(449);
pBLEScan->setActiveScan(true);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.drawString("Turn on a Core Cube", 10, 10);
pBLEScan->start(30);
}
void loop()
{
M5.update();
int spd;
int rotspd;
unsigned int d1;
unsigned int d2;
int lspd, rspd;
switch (state)
{
case READY_TO_CONNECT:
connectToCoreCube();
break;
case CONNECTED:
if(M5.BtnB.wasPressed())
{
pattern = pattern + 1;
if(pattern > 3) pattern = 0;
changeLCD(pattern);
}
if (M5.BtnA.wasPressed())
{
state = RUNNING;
}
break;
case RUNNING:
delay(1000);
switch (pattern){
case 0: // uzumaki
lspd = 80;
rspd =60;
digitalWrite(UVLED_PIN, UVLED_ON);
while(rspd > 0){
setMotor(lspd, rspd, 0);
delay(800);
lspd = lspd -5;
rspd = rspd - 20;
}
setMotor(0, 0, 0);
digitalWrite(UVLED_PIN, UVLED_OFF);
break;
case 1: //square
spd = 60;
rotspd = 35;
d1 = 600;
d2 = 440;
for(int i = 0 ; i < 4; i++){
digitalWrite(UVLED_PIN, UVLED_ON);
setMotor(spd, spd, d1);
delay(d1 + 100);
digitalWrite(UVLED_PIN, UVLED_OFF);
setMotor(rotspd, 0, d2);
delay(d2 + 100);
}
break;
case 2: // star?
spd = 60;
rotspd = 100;
d1 = 800;
d2 = 290;
for(int i = 0 ; i < 5; i++){
digitalWrite(UVLED_PIN, UVLED_ON);
setMotor(spd, spd, d1);
delay(d1 + 100);
digitalWrite(UVLED_PIN, UVLED_OFF);
setMotor(rotspd, 0, d2);
delay(d2 + 100);
}
break;
case 3: //dango
digitalWrite(UVLED_PIN, UVLED_ON);
spd = 50;
d1 = 500;
d2 = 780;
rotspd = 40;
for(int i = 0 ; i < 3; i++){
setMotor(spd, spd, d1);
delay(d1 + 100);
setMotor(rotspd, -rotspd, d2);
delay(d2 + 100);
}
setMotor(spd, spd, d1);
delay(d1 + 100);
digitalWrite(UVLED_PIN, UVLED_OFF);
break;
}
//pClient->disconnect();
digitalWrite(UVLED_PIN, UVLED_OFF);
state = CONNECTED;
break;
default:
break;
}
if (lastState != state)
{
M5.Lcd.fillScreen(BLACK);
switch (state)
{
case READY_TO_CONNECT:
M5.Lcd.drawString("Ready to connect", 10, 10);
break;
case CONNECTED:
changeLCD(pattern);
break;
case RUNNING:
M5.Lcd.drawString("Running", 10, 10);
break;
case DISCONNECTED:
M5.Lcd.drawString("Disconnected", 10, 10);
break;
case NO_CORE_CUBE:
M5.Lcd.drawString("Couldn't find a Cube", 10, 10);
break;
default:
break;
}
lastState = state;
}
delay(100);
}
void changeLCD(unsigned int pattern)
{
M5.Lcd.drawString("Connected", 10, 10);
M5.Lcd.drawString("Select B button", 10, 25);
M5.Lcd.drawString("Pattern", 10, 40);
M5.Lcd.drawString(String(pattern), 80, 40);
M5.Lcd.drawString("Start A button", 10, 55);
}
void setMotor(int right, int left, unsigned int duration_ms)
{
uint8_t data[8] = {0};
data[0] = 0x02; // Control type (motor control 2)
data[1] = 0x01; // motorID left
data[2] = 0x01; // forward direction
data[3] = left;
if(left < 0)
{
data[2] = 0x02; // reverse direction
data[3] = - left;
}
data[4] = 0x02; // motorID right
data[5] = 0x01; // forward direction
data[6] = right;
if(right < 0)
{
data[5] = 0x02; // reverse direction
data[6] = - right;
}
data[7] = duration_ms / 10;
pCoreCubeMotorChar->writeValue(data, sizeof(data));
}
さいごに
プレイマットの微小ドットによる目標位置指定移動ができないのでtoio™️の真骨頂である精密な移動制御ができないのではありますが、モーター速度と時間の調整だけでもとりあえずは光の軌跡ができました。まあ、こうなるとそもそも下に敷くのがtoio™️用のプレイマットである必要がないので、最初から塗料が塗ってある蓄光シートとかを使ったほうが簡単だと思います。(未テスト)
amazonで検索してでてきた蓄光シートの例(toio™️ core cubeの移動スピードを考えるとA3以上のサイズがおすすめです)
-
約A3(約30cm×約42cm)-1枚 01蓄光グリーン
- (追記) これは取り寄せて試しましたが、今回作った開発用プレイマットに蓄光塗料を塗ったやつよりは光り方がだいぶ弱かったです。
- 小松プロセス 高輝度蓄光シート アクリルコートタイプ A3サイズ
こういうシートの上からtoioIDの微小ドットが印刷できたりすればいいのですが...
toioIDの微小ドットが印刷された透明な大判シールとかが出るといいなーと夢見つつ、本記事は終わりとなります。
あと、しつこいようですが、紫外線LEDの光を直接覗き込むようなことは絶対にしないでください。
謝辞
今回、M5Stick-CからBLEでtoio™️ core cubeと通信してモーターを動かすスケッチの制作は、kotobukiさんのnote記事「一定時間が経過したことを知らせしてくれるロボットをつくる)」を参考にさせていただきました。簡単に動かすことができて大変助かりました。