Scratch から Arduino を操作しよう、というわけで NetScrattino を作る

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

あたりを見ながら、ひとまずデジタルピンとアナログピンの読み書き、モードの設定、レポートの設定だけを送れるようにしておく。

// 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 で受け取る。

_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形式で書くことができて、こんな風になっている。

{
  "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# で実装してみる。

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 に持って来るというのもできそうな感じはする。

カテゴリー: 開発, F#, Scratch パーマリンク