Firmata を使って Xamarin.Android から Arduino に接続(F#版)

Firmata を使って Xamarin.Android から Arduino に接続する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/7185

これの F# 版を作ります。Xamarin.Android は主に C# で作ることが多いでしょうが、オール F# で作ることができます。Visual Studio 2013 では、Visual F# の Android テンプレートがあるので、そのまま使えます。

Firmata.NET を F# 版に書き直す

少し書き方が違いますが、ざっと書き下したのが以下のコードです。

namespace Firmata.NET

open System
open Android.App
open Android.Content
open Android.Runtime
open Android.Views
open Android.Widget
open Android.OS

open Android.Bluetooth
open System.Linq
open Java.Util

module ARDUINO = 
    let INPUT = 0
    let OUTPUT = 1
    let LOW = 0
    let HIGH = 1
    

type Arduino() as this =
    let INPUT = 0
    let OUTPUT = 1
    let LOW = 0
    let HIGH = 1

    let DIGITAL_MESSAGE = 0x90uy    // send data for a digital port
    let ANALOG_MESSAGE = 0xE0uy     // send data for an analog pin (or PWM)
    let REPORT_ANALOG = 0xC0uy      // enable analog input by pin #
    let REPORT_DIGITAL = 0xD0uy     // enable digital input by port
    let SET_PIN_MODE = 0xF4uy       // set a pin to INPUT/OUTPUT/PWM/etc
    let REPORT_VERSION = 0xF9uy     // report firmware version
    let SYSTEM_RESET = 0xFFuy       // reset from MIDI
    let START_SYSEX = 0xF0uy        // start a MIDI SysEx message
    let END_SYSEX = 0xF7uy          // end a MIDI SysEx message


    let mutable _socket:BluetoothSocket = null
    let mutable autoStart = false
    let mutable delay = 0

    let mutable digitalOutputData = Array.zeroCreate(16)
    let mutable digitalInputData = Array.zeroCreate(16)
    let mutable analogInputData = Array.zeroCreate(16)

    do
        if autoStart = true then
            delay <- 0 
            this.Connect()
            this.Open()

    /// Connect Bluetooth on Arduino.
    member this.Connect() =
        let adapter = BluetoothAdapter.DefaultAdapter
        if adapter = null then
            raise (Exception("No Bluetooth adapter found."))
        if adapter.IsEnabled = false then
            raise (Exception("Bluetooth adapter is not enabled."))
        let device = adapter.BondedDevices.FirstOrDefault( fun x -> x.Name = "HC-06" )
        if device = null then
            raise (Exception("Named device not found."))
        _socket <- device.CreateRfcommSocketToServiceRecord(UUID.FromString("00001101-0000-1000-8000-00805f9b34fb"))
        _socket.Connect()

    member this.Open() =
        // let mutable command = Array.create<byte>(2)
        for i=0 to 5 do
            let command = [|
                REPORT_ANALOG ||| byte(i)
                1uy
            |]
            _socket.OutputStream.Write( command, 0, command.Length )
        for i=0 to 1 do
            let command = [|
                REPORT_DIGITAL ||| byte(i)
                1uy
            |]
            _socket.OutputStream.Write( command, 0, command.Length )

    member this.Close() = 
        _socket.Close()
        _socket <- null

    member this.digitalRead(pin:int):int =
        (digitalInputData.[pin >>> 3] >>> (pin &&& 0x07)) &&& 0x01
    member this.analogRead(pin:int):int =
        analogInputData.[pin]

    member this.pinMode(pin,mode) =
        let message = [|
            SET_PIN_MODE
            byte(pin)
            byte(mode)
        |]
        _socket.OutputStream.Write( message, 0, message.Length )

    member this.digitalWrite(pin,value) =
        let portNumber = (pin >>> 3) &&& 0xFF
        digitalOutputData.[portNumber] <-
            if value = 0 then
                digitalOutputData.[portNumber] &&& ~~~(1 <<< (pin &&& 0x07))
            else 
                digitalOutputData.[portNumber] ||| (1 <<< (pin &&& 0x07)) 
        let message = [|
            DIGITAL_MESSAGE ||| byte(portNumber) 
            byte(digitalOutputData.[portNumber] &&& 0x7F)
            byte(digitalOutputData.[portNumber] >>> 7)
        |]
        _socket.OutputStream.Write(message, 0, message.Length);
            
    member this.analogWrite(pin,value) = 
        let message = [|
            ANALOG_MESSAGE ||| (byte(pin) &&& 0x0Fuy)
            byte(value &&& 0x7F)
            byte(value >>> 7)
        |]
        _socket.OutputStream.Write(message, 0, message.Length);

    member this.setDigitalInputs( portNumber, portData ) =
        digitalInputData.[portNumber] <- portData

    member this.setAnalogInput( pin, value ) =
        analogInputData.[pin] <- value

定数が module を使っているのは愛嬌として、メッセージの配列を作るところは、直接作れるので若干楽ですね。ビット演算子が「&&&」や「|||」を使わないといけないので、文字数的に冗長なのは残念な感じがしますが、まあ、これはこれで良しということで。
int から byte へのキャストが頻発するのは、F# の宿命です。型を合わせないといけないので、アップキャストだけでなくダウンキャストに対しても、明示的な型のキャストが必要になります。このあたりインターフェースプログラミングをしているとちょっと冗長な感じになります。

マニフェストを設定する

Bluetooth を扱うための、パーミッションの設定は C# と同じです。

MainActivity

ざっと書いたので、ボタンのクリックイベントのところが雑ではありますが、C# よりも短くかけます。フィルタを多用する場合やフローチャート的に状態遷移する場合は、関数型言語 F# を使うとすんなりと書けるはずなんですけどね。このあたりは、Arduino 戦車に距離センサーを付けて自律化したときに試してみましょう。

type MainActivity () =
    inherit Activity ()

    let mutable buttonConnect:Button = null
    let mutable buttonOpen:Button = null
    let mutable buttonLedOn:Button = null
    let mutable buttonLedOff:Button = null
    let mutable arduino:Arduino = new Arduino()

    override this.OnCreate (bundle) =

        base.OnCreate (bundle)

        // Set our view from the "main" layout resource
        this.SetContentView (Resource_Layout.Main)

        // Get our button from the layout resource, and attach an event to it
        buttonConnect <- this.FindViewById<Button>(Resource_Id.buttonConnect)
        buttonOpen <- this.FindViewById<Button>(Resource_Id.buttonOpen)
        buttonLedOn <- this.FindViewById<Button>(Resource_Id.buttonLEDon)
        buttonLedOff <- this.FindViewById<Button>(Resource_Id.buttonLEDoff)


        buttonConnect.Click.Add( fun args -> 
            arduino.Connect();
            buttonConnect.Text <- "connected.";
        )
        buttonOpen.Click.Add( fun args -> 
            arduino.Open();
            buttonConnect.Text <- "Firmata opend.";
            arduino.pinMode(5, ARDUINO.OUTPUT );
            arduino.digitalWrite(5, ARDUINO.LOW);
        )
        buttonLedOn.Click.Add( fun e -> this.OnClickLedOn(buttonLedOn,e) )
        buttonLedOff.Click.Add( fun e -> this.OnClickLedOff(buttonLedOff,e) )

    member this.OnClickLedOn(sender,e) =
        arduino.digitalWrite(5, ARDUINO.HIGH)
    member this.OnClickLedOff(sender,e) =
        arduino.digitalWrite(5, ARDUINO.LOW)

動かす

見た目は C# 版と変わりませんが、F# のアプリが動いています。

Windows に乗せ換えて(実は、ストアアプリ版の Firmata ライブラリも作ってある)、F# からコマンドライン的に使えると、Ruby や Node.js のようにスクリプト言語のように使うことが可能です。このあたり、先の firmata のサイトに Haskell があるので、比較するのも面白いかなと(私は Haskell は全然ダメなんですけど)。

カテゴリー: Android, Arduino, F#, Xamarin パーマリンク