Scratch を使って Arduino を動かそうとすると http://scratchx.org/ を使うのがいいのだろうけど、どうも自分の環境ではうまく動かない。オフラインの Scratch 2.0 の場合、ファイルメニューをシフトを押しながら開くと「実験的なHTTP拡張を取り込み」というのが出て、適当なHTTPサーバーを作ると繋がるらしいことが分かった。
サーバーを作るのが手間といえば手間なんだけど(ScratchXの場合は、Chrome拡張をインストールすると、Chrome側にHTTPサーバーを立てる仕組みになっている)、一度作っておけば、Arduino 以外に接続するのも楽ではないかなと思って、作ってみることにする。.NET で作れば HttpListener があるので、何とかなるのではないかな、と。
Scrattino 2 | Yengawa Systems
http://www.yengawa.com/scrattino2
Let’s Make With Arduino!
https://lets.makewitharduino.com/sample/scratch/
Scrattino2 のほうは、ArduinoにFirmataを入れるんだけど、HTTPサーバーはMac版しかない。Scratio のほうは、独自プロトコルにしてあって中身は Python で書かれている。
ざっと、Scratio で Scratch の拡張ブロックの操作を確認したところで、まあいけそうなことが分かったので Firmata への接続を作ることにした。
シリアル通信で Firmata に接続する
まずは、NetScrattino から Arduino にシリアル通信する。
シリアル通信は双方向に通信ができるので、NetScrattinoからコマンドを送信すると同時に、定期的に Arduino のほうからアナログピンの状態を送信してくれる。これを保持しておく。
protocol/protocol.md at master ・ firmata/protocol
https://github.com/firmata/protocol/blob/master/protocol.md
firmataプロトコル覚え書き
https://gist.github.com/hiroeorz/7868628
あたりを見ながら、ひとまずデジタルピンとアナログピンの読み書き、モードの設定、レポートの設定だけを送れるようにしておく。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // two byte digital data format, second nibble of byte 0 gives the port number (e.g. 0x92 is the third port, port 2) // 0 digital data, 0x90-0x9F, (MIDI NoteOn, but different data format) // 1 digital pins 0-6 bitmask // 2 digital pin 7 bitmask member this .digitalWrite(pin,value) = let portNumber = (pin >>> 3) &&& 0xFF digitalInputData.[portNumber] <- if value = 0 then digitalInputData.[portNumber] &&& ~~~(1 <<< (pin &&& 0x07)) else digitalInputData.[portNumber] ||| (1 <<< (pin &&& 0x07)) let message = [| DIGITAL_MESSAGE ||| byte (portNumber) byte (digitalInputData.[portNumber] &&& 0x7F) byte (digitalInputData.[portNumber] >>> 7) |] _socket.Write(message, 0, message.Length); |
あれこれ面倒なので、F# で書いたのであった。
Arduino から非同期で送ってくるデータは、DataReceived で受け取る。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | _socket.DataReceived.Add( fun (e) -> while _socket.BytesToRead > 0 do let head = _socket.ReadByte() |> byte match head with // analog 14-bit data format // 0 analog pin, 0xE0-0xEF, (MIDI Pitch Wheel) // 1 analog least significant 7 bits // 2 analog most significant 7 bits | h when ANALOG_MESSAGE <= h && h <= ANALOG_MESSAGE + 15uy -> let pin = int (h - ANALOG_MESSAGE) let lsb = _socket.ReadByte() let msb = _socket.ReadByte() let data = (msb <<< 7) ||| lsb analogInputData.[pin] <- data // two byte digital data format, second nibble of byte 0 gives the port number (e.g. 0x92 is the third port, port 2) // 0 digital data, 0x90-0x9F, (MIDI NoteOn, but different data format) // 1 digital pins 0-6 bitmask // 2 digital pin 7 bitmask | h when DIGITAL_MESSAGE <= h && h <= DIGITAL_MESSAGE + 15uy -> let pin = int (h - DIGITAL_MESSAGE) let lsb = _socket.ReadByte() let msb = _socket.ReadByte() let data = (msb <<< 7) ||| lsb digitalInputData.[pin] <- data | _ -> // read off let d = _socket.ReadExisting() () ) |
HTTPサーバーを作って Scratch に応答する
Scratch の拡張ブロックは、JSON形式で書くことができて、こんな風になっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | { "extensionName" : "Net Scrattino" , "extensionPort" : 5410, "url" : "https://github.com/yokobond/scrattino2" , "blockSpecs" : [ [ " " , "INPUT %m.digitalPinNames mode %m.inputPinModes" , "setMode" , "D2" , "PULLUP" ], [ " " , "OUTPUT %m.digitalPinNames value %m.digitalValues" , "digitalWrite" , "D2" , 0], [ " " , "PWM %m.digitalPinNames value %d.pwmValues" , "analogWrite" , "D2" , 0], [ " " , "SERVO %m.digitalPinNames degree %d.servoValues" , "servoWrite" , "D2" , 0], [ " " , "Set Pin %m.digitalPinNames to %d.digitalPinModes mode" , "setPinMode" , "D2" , "OUTPUT" ], [ " " , "LED %m.digitalPinNames is %m.OnOffValues" , "digitalWrite" , "D2" , "ON" ], [ "-" ], [ "r" , "A0" , "a0" ], [ "r" , "A1" , "a1" ], [ "r" , "A2" , "a2" ], [ "r" , "A3" , "a3" ], [ "r" , "A4" , "a4" ], [ "r" , "A5" , "a5" ], [ "-" ], // ["R", "value of %m.digitalPinNames", "pinValue", "D2"], [ "r" , "D2" , "d2" ], [ "r" , "D3" , "d3" ], [ "r" , "D4" , "d4" ], [ "r" , "D5" , "d5" ], [ "r" , "D6" , "d6" ], [ "r" , "D7" , "d7" ], [ "r" , "D8" , "d8" ], [ "r" , "D9" , "d9" ], [ "r" , "D10" , "d10" ], [ "r" , "D11" , "d11" ], [ "r" , "D12" , "d12" ], [ "r" , "D13" , "d13" ] ], "menus" : { "digitalPinNames" : [ "D2" , "D3" , "D4" , "D5" , "D6" , "D7" , "D8" , "D9" , "D10" , "D11" , "D12" , "D13" ], "analogPinNames" : [ "A0" , "A1" , "A2" , "A3" , "A4" , "A5" ], "digitalPinModes" : [ "INPUT" , "INPUT_PULLUP" , "OUTPUT" , "PWM" , "SERVO" ], "inputPinModes" : [ "PULLUP" , "PULLDOWN" ], "digitalValues" : [0, 1], "OnOffValues" : [ "ON" , "OFF" ], "pwmValues" : [0, 64, 128, 192, 255], "servoValues" : [0, 45, 90, 135, 180], "analogValues" : [0, 256, 512, 768, 1023] } } |
blockSpecs にあるのがブロックの定義で、これを web api な形で呼び出す。
仕様は、https://wiki.scratch.mit.edu/w/images/ExtensionsDoc.HTTP-9-11.pdf に書かれている。ID を使って非同期にデータを送る方法は面倒なんだが、通常はポーリング(/poll)を送って、データを返すパターンが多いので、それだけならばそんなに難しくはない。
で、これも F# で実装してみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | let mutable arduino = new FirmataNET.Arduino() // Scratchから受信するためのHTTPサーバー let Server( port ) = let listener = new System.Net.HttpListener() listener.Prefixes.Add( "http://127.0.0.1:" +(port |> string )+ "/" ) listener.Start() while true do let context = listener.GetContext() let res = context.Response let mutable data = "" let path = context.Request.Url.PathAndQuery match path with | "/poll" -> for i=0 to 5 do data <- data + String.Format( "a{0} {1}\n" , i, arduino.analogRead(i)) for i=2 to 13 do data <- data + String.Format( "d{0} {1}\n" , i, arduino.digitalRead(i)) // デバッグ出力 let mutable debug = "" for i=0 to 5 do debug <- debug + String.Format( "a{0} {1} " , i, arduino.analogRead(i)) debug <- debug + "\n" for i=2 to 13 do debug <- debug + String.Format( "d{0} {1} " , i, arduino.digitalRead(i)) debug <- debug + "\n" // printfn "%s" path // printfn "%s" debug | "/reset_all" -> printfn "/reset_all" arduino.Reset() data <- "ok" | _ -> let pa = path.Split([| '/' |]) match pa.[1] with | "digitalWrite" -> let pin = pa.[2].Substring(1) |> int let value = match pa.[3].ToUpper() with | "ON" -> 1 | "OFF" -> 0 | _ -> pa.[3] |> int arduino.digitalWrite( pin, value ) | "analogWrite" -> let pin = pa.[2].Substring(1) |> int let value = pa.[3] |> int arduino.pinMode( pin, 0x03 ) // PWM arduino.analogWrite( pin, value ) | "servoWrite" -> let pin = pa.[2].Substring(1) |> int let value = pa.[3] |> int arduino.pinMode( pin, 0x04 ) // SERVO arduino.analogWrite( pin, value ) | "setMode" -> let pin = pa.[2].Substring(1) |> int let value = if pa.[3] = "PULLUP" then 0x0B else 0x00 arduino.pinMode( pin, value ) | "setPinMode" -> let pin = pa.[2].Substring(1) |> int let value = match pa.[3] with | "INPUT" -> 0 | "OUTPUT" -> 1 | "PWM" -> 3 | "SERVO" -> 4 | "INPUT_PULLUP" -> 11 | _ -> 1 arduino.pinMode( pin, value ) | _ -> data <- "" printfn "%s" path res.StatusCode <- 200 let sw = new System.IO.StreamWriter( res.OutputStream ) sw.Write( data ) sw.Close() () |
Scratch で拡張ブロックを作ってみる
先に作った JSON を Scratch 2.0 に読み込ませると、自前で作ったブロックが使えるようになる。
旗をクリックしたときとか、スペースキーを押されたとき、などのイベントのブロックがあるが、テストをするときはブロック自体をダブルクリックすれば実行されるので、プロトタイプを作るときには結構便利。mBlock の場合だと、あらかじめ Arduino にデプロイしてしまうので、変更するに書き込まないといけないし。まあ、Firmata 自体がプロトタイプを作るためのものでもあるので、用途的にはちょうどよいかと思う。
簡易プロキシにUIを付ける
最初は、コマンドラインだけでやっていたのだが、Firmata を直接扱えたほうが便利なので、簡易プロキシ(NetScrattino)にUIを付けてみる。
これは WPF で作って、内部的に MVVM パターンになっているので、この解説はまた後で。
Scratchと連携させる
せっかくの Scratch なので、Arduino を操作するだけじゃなくて猫のほうも操作できるようにしておく。
これは、Lチカをしながら猫が走るパターン。LEDをマウスでクリックすると、Arduino上のLEDが光ると同時に絵のLEDも光る。
ざっと、簡単なものとして、
- LEDの点滅
- PWMでLEDの点灯
- サーボを動かす
- ポテンショメーター(回転とかスライダーとか)でアナログピンで読み取る
なところまでできた。後は、順次
スクラッチーノでScratchとArduinoをつなぐ – MeiDe Digital Craft 2016
https://sites.google.com/site/meidedigitalcraft2016/knowhow/scrattino-usage
にある実習を動かすようなプログラムが組めればよいかな。
コード
NetScrattino のコードはこちら
moonmile/NetScrattino: Simple Server to connect from Scratch to Arduino
https://github.com/moonmile/NetScrattino
これから
ScratchX
http://scratchx.org/#extensions
の拡張を見ていくと Kinect とか Leapmotion とかもある。環境が悪いのかよくわからにけど、うちの PC では ScratchX が動かないので何とも言えないのだけど、どうやら、COM 制限のような気がする。このあたりは、別の PC や Mac で試してみよう。
ローカルで実験する場合は、HTTP プロキシを作ったほうが応用範囲が広そうなので(.NETで作れるし)、カメラでの撮影を Scratch 側で制御するとか、物体認識を Scratch に持って来るというのもできそうな感じはする。