Raspberry Pi 用のクロス環境を考察する

Mono を Raspberry Pi でビルドすると、12時間かかってしまうので非常に手間。考えてみれば、Raspberry Pi の CPU は ARM なのだから、Android のクロスコンパイル環境を持ってくれば動くのでは?と思ったのが発端です。

まだ調査中なので、どの環境がよいかは後でまとめます。

■方法

最初は、Linux 上にクロスコンパイラ環境を用意して、ビルドした後に Raspberry Pi にもっていけば、と思ったのですが、いくつか方法が選べることに思い至りました。

  • Linux Debian wheezy のクロスコンパイラ環境で Mono をビルドして、Raspberry Pi にインストール
  • Linux 上の ARM 仮想環境を作って、ビルドしたのち、Raspberry Pi にインストール
  • Android 実機を使ってビルドしたのち、Raspberry Pi にインストール
  • Raspberry Pi 自体をエミュレートする環境を作る

Android の実機を使うのは、ARM 仮想環境(エミュレータ)よりもスピードが速いだろう、って思いつきなのですが、手元の CPU スピードからいけば多少 ARM エミュレーターが遅くても RasPi よりは断然早いだろう、という想像です。Android 実機/エミュレータ に対しては adb で shell を動かすことができるから gcc さえ入れ込めば、とも思ったのですが、Android 上でこれができるのか?というのと Mono から配布されているソースパッケージを Android の環境に合わせるのは結構手間ではないか?との想定から、現実的なところは、Debian 上のクロスコンパイラか、Raspberry Pi のエミューレター環境ですね。エミュレーター環境とはいえ、IO をエミュレートしたいわけではなくて、Raspberry Pi の Debian 環境にそろえたいだけなので、単純に ARM CPU で動作する Linux 環境を作ればよいだけです。

■クロスコンパイラ環境を Debian 上に作る

debian wheezy 上でのクロスコンパイラのビルド を参考にして、クロスコンパイル環境を構築。

GCC_TARGET=armel
dpkg-buildpackage -b -uc -us

のところで、Error になってしまったので、後で調査。

■Scratchbox を使う

Mono:ARM – Mono を見ると、ScratchBox を使う方法が示されている。Scratchbox – ARMクロス開発環境 を見ると仮想環境を作る方法らしい。./configure を直すのは確かに大変なので、直接 ARM の仮想環境を作って、其の中に Gcc の入れ込んでしまうのがよい。

ここの環境に Raspberry Pi の Debian が入ればいいのだが?これができるかどうか分からない。

■qemuを使う

と、ここまで書いて

Raspberry Pi • View topic – Re: Emulating Raspberry Pi in Windows the easy way
http://www.raspberrypi.org/forums/viewtopic.php?f=26&t=5743 

のところに RasPi のエミュレーターがあった。http://sourceforge.net/projects/rpiqemuwindows/ qemu-system-arm.exe を使うらしい。QEMU は Linux 上で動くものが多いけど、Windows 上で動くのであれば、これでいいかも。

と、あっけなく起動できた w これでいいかも。RasPi の ディスクイメージをそのまま使っているので 2GB に制限されているけど、8GB 程度のイメージを作ればそれで使えるのでは?ちょっと試してみよう。

image

 

 

■参考

日記/2014-03-28/debian wheezy 上でのクロスコンパイラのビルド – BAK
http://jr0bak.homelinux.net/~imai/pukiwiki/pukiwiki.php?%C6%FC%B5%AD%2F2014-03-28%2Fdebian%20wheezy%20%BE%E5%A4%C7%A4%CE%A5%AF%A5%ED%A5%B9%A5%B3%A5%F3%A5%D1%A5%A4%A5%E9%A4%CE%A5%D3%A5%EB%A5%C9
Mono:ARM – Mono
http://www.mono-project.com/Mono:ARM
Scratchbox – cross compile enviroument
http://homepage2.nifty.com/SECS/scratchbox/index.html
QEMUでARM環境を手にいれる | 團長の小部屋
http://wwwdantyo.wordpress.com/qemu%E3%81%A7arm%E7%92%B0%E5%A2%83%E3%82%92%E6%89%8B%E3%81%AB%E3%81%84%E3%82%8C%E3%82%8B/

カテゴリー: RaspberryPi | 6件のコメント

Xamarin の FormsGallery を XAML で書き直してみる

Xamarin.Forms のサンプルに FormsGallery があります。Xamarin.Forms で使っているひと通りのコントロールが載っているので、どんなものがあるのか見るのに便利なのです。ただ、中のコードは、C# でひとつひとつのコントロールを作るスタイルになっているので、XAML で作る場合にはどうするのか、いまひとつ解りづらい…ので、コンバートしてみました。

https://github.com/moonmile/XFormsGallery

ところどころ未完成なところがあるのですが、ひとまず1日で出来上がったところまで。
共通プロジェクトは PCL を使っています。XAML を素直に共通化するだけであれば PCL で良いでしょう。WinStore/WinPhone のユニバーサルプロジェクトのようにコードビハイドに手を加える場合には、共有プロジェクトを使うと良いかと。

■注意

現時点の Xamarin.Formsのバージョンは「1.1.1.6206」になっているのですが、Visual Stuido 2013 に含まれているテンプレートでは、「1.0.6186」になっています。ほとんど変わらないですが TableView の概観がちょっと違うので、Xamarin の Sample のように、最新のバージョンに合わせています。プロジェクトごとに NuGet で最新にしてください。

以下、ざっと XAML にするときに手順を書き下しておきます。

■App.cs を書き換える

プロジェクトテンプレートで作ると、PCL プロジェクトの App.cs にページを作るコードがあります。ここをごっそり削って、Page クラスのインスタンスを返します。ただし、サンプルの場合には特殊でナビゲーションを使っています。この場合は特別に NavigationPage のインスタンスを作成します。

public class App
{
    public static Page GetMainPage()
    {
        return new NavigationPage(new HomePage());
    }
}

■簡単な LabelDemoPage を作る

ラベルだけが表示されるページはこんな感じです。

<?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?>
<ContentPage xmlns=&quot;http://xamarin.com/schemas/2014/forms&quot;
					   xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;
					   x:Class=&quot;XFormsGallery.LabelDemoPage&quot;>
	<ContentPage.Padding>
		<OnPlatform x:TypeArguments=&quot;Thickness&quot;>
			<OnPlatform.iOS>10,20,10,5</OnPlatform.iOS>
			<OnPlatform.Android>10,0,10,5</OnPlatform.Android>
			<OnPlatform.WinPhone>10,0,10,5</OnPlatform.WinPhone>
		</OnPlatform>
	</ContentPage.Padding>
	<StackLayout>
		<Label Text=&quot;Label&quot; Font=&quot;50&quot; HorizontalOptions=&quot;Center&quot;></Label>
		<Label Font=&quot;Large&quot; VerticalOptions=&quot;CenterAndExpand&quot;>
			<Label.Text>Xamarin.Forms is a cross-platform natively backed UI toolkit abstraction that allows developers to easily create user interfaces that can be shared across Android, iOS, and Windows Phone.</Label.Text>
		</Label>
	</StackLayout>						
</ContentPage>

OnPlatform のところは、OSによってコードを切り替えらるところで、元のサンプルの Device.OnPlatform にあたります。Top だけ切り替えられるような気もするのですが、うまくいかなかったので Thickness まるごと切り替えます。iOS だけ 20 ドット広げてあるのは、iPhone のステータスバー用です。ただし、ナビゲーターの場合は自動でステータスバーに対応しているらしく、これはあまり意味がありません。全画面表示するページを作るときに使う技ですね。

元ネタの LabelDemoPage を見ると、次のようになるので、解りやすいような解りにくいような。

    class LabelDemoPage : ContentPage
    {
        public LabelDemoPage()
        {
            Label header = new Label
            {
                Text = "Label",
                Font = Font.BoldSystemFontOfSize(50),
                HorizontalOptions = LayoutOptions.Center
            };

            Label label = new Label
            {
                Text =
                    "Xamarin.Forms is a cross-platform natively " +
                    "backed UI toolkit abstraction that allows " +
                    "developers to easily create user interfaces " +
                    "that can be shared across Android, iOS, and " +
                    "Windows Phone.",

                Font = Font.SystemFontOfSize(NamedSize.Large),
                VerticalOptions = LayoutOptions.CenterAndExpand
            };

            // Accomodate iPhone status bar.
            this.Padding = new Thickness(10, Device.OnPlatform(20, 0, 0), 10, 5);

            // Build the page.
            this.Content = new StackLayout
            {
                Children = 
                {
                    header,
                    label
                }
            };
        }
    }

XAML 自体は、Xamarin Studio 上で手書きをします。Visual Studio 上でも書けないことはないのですが、タグのコード補完が効かないので、非常にやりにくいです。本当はデザイナが欲しいところでしょうが、スマートフォンのような小さな画面の場合には自由度が少ないので、ほとんど StackLayout か Grid を使ってしまうのであまり関係ないでしょう。ですが、iPad や Surface のようなタブレットPC のレイアウトをするときにはデザイナは必須ですよね。このあたりは、今後どうするのか不明です。

Windows Phone(SilverLightですが)の XAML に直す時に ConvertPageToUIElement メソッドを呼び出しているので、この逆変換ができれば Visual Studio 上で XAML のデザインができるようになるかもしれません。

■HomePage ページを作る

トップページは、メニュー用のページです。
TableView でリスト表示をしているのですが、この親子関係が解りづらいところです。

<?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?>
<ContentPage xmlns=&quot;http://xamarin.com/schemas/2014/forms&quot;
				xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;
				x:Class=&quot;XFormsGallery.HomePage&quot;
				Title=&quot;XAML Forms Gallery&quot;>
	<TableView Intent=&quot;Menu&quot;>
		<TableView.Root>
			<TableSection Title=&quot;Views for Presentation&quot;>
				<TextCell Text=&quot;Label&quot; x:Name=&quot;cellLabel&quot; ></TextCell>
				<TextCell Text=&quot;Image&quot; x:Name=&quot;cellImage&quot;></TextCell>
				<TextCell Text=&quot;BoxView&quot; x:Name=&quot;cellBoxView&quot;></TextCell>
				<TextCell Text=&quot;WebView&quot; x:Name=&quot;cellWebView&quot;></TextCell>
				<TextCell Text=&quot;Map&quot; x:Name=&quot;cellMap&quot;></TextCell>
			</TableSection>
			<TableSection Title=&quot;Views that Initiate Commands&quot;>
				<TextCell Text=&quot;Button&quot; x:Name=&quot;cellButton&quot; ></TextCell>
				<TextCell Text=&quot;SearchBar&quot; x:Name=&quot;cellSearchBar&quot;></TextCell>
			</TableSection>
...

ただし、元ネタの HomePage.cs を覗いてみると、TableView <- TableView.Root <- TableRoot <- TableSection <- TextCell <- TextCell の関係が解るので、そのまま書き写しています。XAML では TableRoot を省略しても動作するようです(TableView.Rootのほうは省略できないんですよね。ここは Children にして欲しかった)。

this.Title = "Forms Gallery";
this.Content = new TableView
    {
        Intent = TableIntent.Menu,
        Root = new TableRoot
        {
            new TableSection("Views for Presentation")
            {
                new TextCell
                {
                    Text = "Label",
                    Command = navigateCommand,
                    CommandParameter = typeof(LabelDemoPage)
                },

                new TextCell
                {
                    Text = "Image",
                    Command = navigateCommand,
                    CommandParameter = typeof(ImageDemoPage)
                },

タップなどの操作は非常に制限されています。労力を減らすたか、これから実装するのか解りませんが、TextCell の場合は Command パターンで呼び出しをしています。ただし、主要なボタンコントロールなどには Clicked イベントが用意されているので、そのままコードビハイドで書くことができます。


WPFやWinStoreで XAML を手書きしたことがある人ならば、デザイナがなくても大体想像して書けるかなという感じですね。Xamarin.Forms の DTD は無いようなので、親子関係はマニュアル http://iosapi.xamarin.com/?link=N%3aXamarin.Forms を見る必要があるのですが、まあ元ネタを見比べていけば、これも想像で書ける範囲かと思います。

カテゴリー: C#, Xamarin | Xamarin の FormsGallery を XAML で書き直してみる はコメントを受け付けていません

F# を使ってRaspberryPi+BrickPi+LOGO Mindstorms EV3 を動かす

F# に限らないのですが、試しにモーター制御の部分だけ .NET 版の BrickPi を作りました。

https://github.com/moonmile/BrickPiNet

– BrickPi.h と brickpinet.c で .NET に公開可能なインターフェースを作る
– BrickPi.cs と BPi.cs で .NET で包む
– sample.cs、sampleF.fs のように C#/F# で制御する

というパターンです。BrickPi_Python などを見ると、それぞれの言語でシリアルポートに直接アクセスしているのですが、C# からポートアクセスは面倒だし、BlickPi.h 自体で十分なインターフェースになっているのでこれをそのまま使います。BrickPi.h 自体は C言語で掛かれているので、そのまま shared で公開して、これを BrickPi.cs で DllImport するというスタイルになっています。

sample.cs と sampleF.fs の場合は、ほとんど生の BrickPi クラスを使っているので他のプログラム言語との対応が取りやすくなっています。が、そのままでオブジェクト指向的に扱いにくいので、BPi クラスを作ってオブジェクト化しています。複数のモーター制御のところで MVVM パターンを使っているのはお試しです。BrickPi の場合は、ポート呼び出しの後に戻りがないので、INotifyPropertyChanged と相性がよいかもしれません。次はセンサーから入って来る操作なので、event 的に作るか、reactive 的に作るか、といったところです。

以下、ざっとファイルの解説を。

■Makefile

Raspberry Pi 上でビルドする必要があるので、Makefile を作っています。先のブログにも書きましたが、RasPi の現状のレポジトリにある mono では F# 3.1 が動かないので、mono のビルドが必須になります。それだと相当大変なので、単に試したい方は、F# のほう(fshaprcのところ)は削除して、apt-get install mono-complete の後、ビルドを試してみてください。

all: sample.exe 
	sampleF.exe 
	libbrickpinet.so 
	sampleCs.exe 
	sampleF2.exe

sample.exe: libbrickpinet.so BrickPi.dll sample.cs
	gmcs /out:sample.exe -sdk:4.5 /r:BrickPi.dll sample.cs 
sampleCs.exe: libbrickpinet.so BrickPi.dll sampleCs.cs
	gmcs /out:sampleCs.exe -sdk:4.5 /r:BrickPi.dll sampleCs.cs 
sampleF.exe: libbrickpinet.so BrickPi.dll
	fsharpc /out:sampleF.exe /r:BrickPi.dll sampleF.fs 
sampleF2.exe: libbrickpinet.so BrickPi.dll
	fsharpc /out:sampleF.exe /r:BrickPi.dll sampleF2.fs 

libbrickpinet.so: brickpinet.o 
	gcc -fPIC -shared -o libbrickpinet.so brickpinet.c -lrt -lm -L/usr/local/lib -lwiringPi
BrickPi.dll: BrickPi.cs 
	gmcs /target:library /out:BrickPi.dll 
	-sdk:4.5 
	BindableBase.cs  
	BrickPi.cs 
	BPi.cs

■brickpinet.c

libbrickpinet.so で BrickPi API を公開させています。BrickPi 構造体へのアクセスがいちいち書いてるのは、C# から C言語の構造体の手間を省くためです。box してもよいのですが、Mono の場合 box は使えるのか?とか調べるのが面倒だったので。

/*
 * BrickPi Interface for .NET 
 */
#include "tick.h"
#include "BrickPi.h"

void SetTimeout(int time) {
	BrickPi.Timeout = time;
}
int GetTimeout() {
	return BrickPi.Timeout;
}
/*
  Motors
*/
void SetMotorSpeed(int motor, int speed) {
	BrickPi.MotorSpeed[motor] = speed;
}
int GetMotorSpeed(int motor) {
	return BrickPi.MotorSpeed[motor];
}
void SetMotorEnable(int motor, int b) {
	BrickPi.MotorEnable[motor] = b == 0 ? 0 : 1;
}
int GetMotorEnable(int motor) {
	return BrickPi.MotorEnable[motor] == 0 ? 0 : 1;
}
...

■BrickPi.cs

単純な.NET版のラッパークラスです。他のプログラム言語との対応がとりやすい形にします。

namespace BrickPiNet
{
    public class BrickPi
    {
        public const int PORT_A = 0;
        public const int PORT_B = 1;
        public const int PORT_C = 2;
        public const int PORT_D = 3;

        public const int PORT_1 = 0;
        public const int PORT_2 = 1;
        public const int PORT_3 = 2;
        public const int PORT_4 = 3;

        public const int MASK_D0_M = 0x01;
        public const int MASK_D1_M = 0x02;
        public const int MASK_9V = 0x04;
        public const int MASK_D0_S = 0x08;
        public const int MASK_D1_S = 0x10;

~~~
        [DllImport("libbrickpinet", EntryPoint = "BrickPiSetup")]
        public static extern int Setup();
        [DllImport("libbrickpinet", EntryPoint = "BrickPiSetupSensors")]
        public static extern int SetupSensors();
        [DllImport("libbrickpinet", EntryPoint = "BrickPiUpdateValues")]
        public static extern void UpdateValues();
        [DllImport("libbrickpinet", EntryPoint = "BrickPiSetTimeout")]
        public static extern void InitTimeout();

■BPi.cs

いちいち関数を呼び出すのは面倒なので、オブジェクト指向っぽく直したのが BPi クラスです。
値を設定した後に、BrickPi.UpdateValues() を呼び出さなくてもよいようにしてあります。ただ、プロパティの変更にセンシティブなのは MVVM の欠点で、このあたりは別なところで回避しようかなと思ってます。例えば、2つのモーターの向きを同時に変える、というようなパターンがそれに相当します。

public class BPi
{
    public ObservableCollection<BPiMotor> Motors;
    public bool AutoUpdate { get; set; }

    private int _timeout = 3000;
    public int Timeout
    {
        get { return _timeout; }
        set
        {
            if (_timeout != value)
            {
                _timeout = value;
                BrickPi.SetTimeout(value);
                BrickPi.InitTimeout();
            }
        }
    }

    /// <summary>
    /// Constructor
    /// </summary>
    public BPi()
    {
        this.Motors = new ObservableCollection<BPiMotor>();
        this.Motors.CollectionChanged += Motors_CollectionChanged;
        this.AutoUpdate = false;
    }
    public void Setup()
    {
        int res = BrickPi.Setup();
        if (res != 0)
        {
            Console.WriteLine(&quot;&quot;);
            throw new Exception(string.Format(&quot;Error: BrickPi.Setup: {0}&quot;, res ));
        }
    }

    void Motors_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        // var m = sender as BPiMotor;
        switch ( e.Action ) {
            case NotifyCollectionChangedAction.Add:
                foreach (BPiMotor it in e.NewItems)
                {
                    it.PropertyChanged += it_PropertyChanged;
                    BrickPi.SetMotorEnable(it.Port, it.Enabled);
                }
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (BPiMotor it in e.OldItems)
                {
                    it.PropertyChanged -= it_PropertyChanged;
                    BrickPi.SetMotorEnable(it.Port, false);
                }
                break;
            case NotifyCollectionChangedAction.Reset:
                break;
        }
        int res = BrickPi.SetupSensors();
        if (res != 0)
        {
            Console.WriteLine(&quot;&quot;);
            throw new Exception(string.Format(&quot;Error: BrickPi.SetupSensors: {0}&quot;, res));
        }
    }

    void it_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (this.AutoUpdate == true) 
            Update();
    }

    /// <summary>
    /// call UpdateValues
    /// </summary>
    public void Update()
    {
        BrickPi.UpdateValues();
        System.Threading.Thread.Sleep(10);
    }
}
public class BPiMotor : BindableBase
{
    public int Port { get; set; }

    private bool _enabled = false;
    public bool Enabled
    {
        get { return _enabled; }
        set
        {
            BrickPi.SetMotorEnable(this.Port, value);
            this.SetProperty(ref this._enabled, value);
        }
    }

    private int _speed = 0;
    public int Speed
    {
        get { return _speed; }
        set
        {
            BrickPi.SetMotorSpeed(this.Port, value);
            this.SetProperty(ref this._speed, value );
        }
    }
}

実際に制御するサンプルコードです。

■sampleCs.cs

設定部分がちょっとオブジェクト指向っぽい感じですね。最終的には、Xamarin.iOS/Android を使って、iPad から制御できるようにします。通信部分は、.NET で簡易HTTPサーバー作ってRESTでやり取りするという感じです。

class Program2
{
    void main()
    {
        int speed = 200;
        var bpi = new BPi();
        // initialize 
        bpi.Setup();
        bpi.AutoUpdate = true;
        var motor1 = new BPiMotor() { Port = BrickPi.PORT_B, Enabled = true };
        var motor2 = new BPiMotor() { Port = BrickPi.PORT_C, Enabled = true };
        bpi.Motors.Add(motor1);
        bpi.Motors.Add(motor2);
        bpi.Timeout = 3000;

        Console.WriteLine("start");
        bool loop = true;
        while (loop)
        {
            var k = Console.ReadKey();
            switch (k.Key)
            {
                case ConsoleKey.W:
                    motor1.Speed = speed;
                    motor2.Speed = speed;
                    break;
                case ConsoleKey.A:
                    motor1.Speed = speed;
                    motor2.Speed = -speed;
                    break;
                case ConsoleKey.D:
                    motor1.Speed = -speed;
                    motor2.Speed = speed;
                    break;
                case ConsoleKey.S:
                    motor1.Speed = -speed;
                    motor2.Speed = -speed;
                    break;
                case ConsoleKey.X:
                    motor1.Speed = 0;
                    motor2.Speed = 0;
                    break;
                case ConsoleKey.Q:
                    loop = false;
                    break;
            }
            // bpi.Update();
        }
    }
    static void Main(string[] args)
    {
        var pro = new Program2();
        pro.main();
    }
}

■sampleF2.fs

F# の場合も C# とほぼ同じですが、括弧がない分だけ若干短く書けます。これも HTTP 経由で制御できるようにする予定です。

module sampleF2
open System
open BrickPiNet 

let speed = 200
// main
let bpi = new BPi()
bpi.Setup()
bpi.AutoUpdate <- true
let motor1 = new BPiMotor( Port = BrickPi.PORT_B, Enabled = true )
let motor2 = new BPiMotor( Port = BrickPi.PORT_B, Enabled = true )
bpi.Motors.Add(motor1)
bpi.Motors.Add(motor2)
bpi.Timeout <- 3000 
Console.WriteLine(&quot;start&quot;)
let mutable loop = true
while loop do
    let key = Console.ReadKey()
    match key.Key with
        | ConsoleKey.W -> 
            motor1.Speed <- speed 
            motor2.Speed <- speed 
        | ConsoleKey.A -> 
            motor1.Speed <- speed 
            motor2.Speed <- -speed 
        | ConsoleKey.D -> 
            motor1.Speed <- -speed 
            motor2.Speed <- speed 
        | ConsoleKey.S -> 
            motor1.Speed <- -speed 
            motor2.Speed <- -speed 
        | ConsoleKey.X -> 
            motor1.Speed <- 0
            motor2.Speed <- 0
        | ConsoleKey.Q -> 
            loop <- false
        | _ -> ()

F# の場合、面白いのはインタープリタ(fsharpi)でも動くことです。Raspberry Pi 上のターミナルで、fsharpi でインタープリタを起動した後で、以下を一気に通します。シリアルポートアクセスのため sudo で root 権限で動かしてください。

#r &quot;BrickPi.dll&quot; ;;
open System ;;
open BrickPiNet ;;
let speed = 200 ;;
// main
let bpi = new BPi() ;;
bpi.Setup() ;;
bpi.AutoUpdate <- true ;;
let motor1 = new BPiMotor( Port = BrickPi.PORT_B, Enabled = true ) ;;
let motor2 = new BPiMotor( Port = BrickPi.PORT_B, Enabled = true ) ;;
bpi.Motors.Add(motor1) ;;
bpi.Motors.Add(motor2) ;;
bpi.Timeout <- 3000 ;;

その後、

motor1.Speed <- 200 ;;

のように F# のコードを書くとそのまま車輪が動き出します。このあたりの流れは F# インタープリタならではのところです。制御コマンドっぽい形で動かせるので、テストとか試行錯誤のときに便利そうです。

カテゴリー: C#, F#, RaspberryPi | F# を使ってRaspberryPi+BrickPi+LOGO Mindstorms EV3 を動かす はコメントを受け付けていません

BrickPi でモーターの動作確認まで

Raspberry Pi で F# が動かせたので、今度は BrickPi の設定です。

image

BrickPi Image Setup: Setting up your SD Card For the BrickPi
http://www.dexterindustries.com/BrickPi/getting-started/pi-prep/

にある Modify your own image にしたがって、BrickPi パッケージをインストールしていきます。

git clone https://github.com/DexterInd/BrickPi.git

から始まって、ちまちまと設定をしないといけないので、Download and use our modified Raspbian image on your own SD Card のほうから、BrickPi インストール済みの OS をダウンロードしたほうが良さそうです。今回は、mono, fsharp のインストールが大変なので、ちまちまと modify していきました。sudo nano /etc/inittab のあたりとか、既に Raspberry Pi 配布の Debian では設定済みのところがあるので、あれ?と思うところもありあますが、そのまま進んで完了です。

■サンプルを動かして動作確認する

サンプルは Progam メニューの中に色々あります。グラフィカルにプログラミングができる Scratch も魅力的ではあるのですが、最終的には F# で動かしたいのでここは飛ばし、

Scratch

C ← BrickPi
http://www.dexterindustries.com/BrickPi/program-it/c/

でビルドをします。

BrickPi のボードの MB と MC に EV3 のモーターをつなげて

cd BrickPi_C/Project_Examples/simplebot/
cp ../../Drivers/*.h .
gcc -o sbot simplebot_simple.c -lrt -lm -L/usr/local/lib -lwiringPi
sudo ./sbot

でモーターが動きます。Drivers ディレクトリからコピーしているのは、BrickPi.h と tick.h とい2つのファイルです。

最初、sbot のようにユーザーモードで動かして、なんか BrickPiSetup で -1 を返すし変だなーと思っていたのですが、どうやら root 権限じゃないと動かないようです。シリアルポート ttyAMA0 を読み書きするのでルート権限が必要なようです。

ソース自体は実に単純で https://github.com/DexterInd/BrickPi_C/blob/master/Project_Examples/simplebot/simplebot_simple.c を見ると、

  • BrickPiSetup で初期化
  • BrickPi.Address に通信アドレス設定
  • BrickPi.MotorEnable[motor1] のモーターの設定
  • BrickPiSetupSensors() でセンサーに設定
  • BrickPi.MotorSpeed[motor1] にモーターのスピードを設定
  • BrickPiUpdateValues() で設定をアップロード

のようなパターンでできます。C# のサンプルはないので、適宜 API を DllImport する必要がありそうですが、まあなんとかなるかな。

■WiFi と電源供給の関係

image

モーターの駆動は上の 9V 電池から、WiFi などの動作は下の mini USB から取ってきているようです。BrickPi の 9V 電源を指しただけでも Raspberry Pi は動作するのですが、WiFi を差し込むと電圧が足りないのか止まってしまうんですよね。手元にあるのが、Logitec の Skylink LAN-W300n/U2S というものなのですが、消費電力が多いのかもしれません。

ちなみに、新しく買ってきた elecom WDC-150U2MBK は Raspberry Pi が認識しないというパターンに陥っているので、慎重に選んだほうがよさそうです。いくつかのサイトを見ると、Logitec か bufferlo の製品を使っている場合が多そうです。たしか、基盤は一緒なので、ドングルの名前さえうまく設定してやれば Raspberry Pi で認識するようになったハズなのですが。

■参考

工作と小物のがらくた部屋: BrickPi を入手しました。
http://junkroom2cyberrobotics.blogspot.jp/2014/02/brickpi.html

このサイトを見ると Planex の BT-micro3H2X が高いですがよさそうです。WiFi と Bluetooth が一緒になっているのでよさそうです。

カテゴリー: RaspberryPi | BrickPi でモーターの動作確認まで はコメントを受け付けていません

Raspberry Pi で F# を動かすまで(まとめ)

手元にあるのが Raspberry Pi のメモリが 256MB タイプのためか F# をビルドするときに失敗します。が、別途の Debian でビルドした DLL を install することで回避しています。
手持ちの SD カードは 8GB です。

■下準備

画面は使わずにターミナルだけで作業をします。初期状態で ssh が使える状態になっているので、起動したときの IP だけわかれば OK。初回のみ HDMI でディスプレィをつなげて固定 IP にしておけばよいでしょう。DHCP でも自宅の場合はそう変わらないので、そのままでもいいかなと。

  1. Raspbian Debian Wheezy をインストール
  2. sudo respi-config して Expand Filesystem を実行。こうすると SD カード一杯まで容量を増やしていくれる。
  3. sudo passwd root でパスワードを設定しておく。
  4. adduser でユーザを作る。
  5. visudo で sudo できるようにしておく。
  6. sudo apt-get update する。
  7. sudo apt-get upgrade する。
  8. sudo apt-get install avahi-daemon する。ホスト名を公開するため。
  9. sudo apt-get install samba する。Windows からファイルを送るため

■ビルドの準備

Mono と F# を git から持ってきてビルドします。mono 自体は sudo apt-get mono-complete で持ってこれるのですが、何故か F# 3.1 で使おうとするこけます。また、Rasberry Pi のレポジトリには fsharp パッケージがないので、自前でビルドする必要があります。が、Raspberry Pi 上で F# のコードをビルドしようとすると、途中でテストのエラーでこけます。

という状態なので、Mono 3.2.8 + F# 3.1 の組み合わせの場合は、

  • Mono を Raspberry Pi でダウンロードしてビルド、インストール
  • F# を別の Debian でダウンロードしてビルド
  • 出来上がった F# を Raspberry Pi にコピーしてインストール

という手順になります。F# を別の環境で make install するので、ビルドするディレクトリ名は揃えたほうが無難です(異なる場合は、ln -s すれば ok)。可能ならば ARM 環境で Mono をビルドすればいいんでしょうが。Visual Studio を使えばできるかも。

■ビルドする

http://fsharp.org/use/linux/ にある Option 2: Build and install the F# 3.1 runtime, compiler and tools に従って Mono をビルドします。うちの環境では 12 時間以上かかりました。出来上がったバイナリは大事にとっておきましょう。

sudo make install すると無事 mono が使える状態になります。

image

お次は、F# をビルドするのですが、これは別の Debian でやります。git clone https://github.com/fsharp/fsharp  からダウンロードしてビルドします。これは普通の PC(私の場合は Hyper-V 上の Debian)なのであっという間です。ちなみに、Mono のビルドも結構なスピードで出来上がりますが、中身が ARM ではないので、Raspberry Pi にはコピーできません…と思うのですがどうなんでしょうね?私の場合は 12 時間かけて Raspberry Pi 上でビルドしたのですが。

できあがった F# のバイナリを tar で固めて、Raspberry Pi 上に持っていきます。そして sudo make install すれば、無事 F# が使えるようになります。

■F# を動かす

インタープリタは fsharpi です。コンパイラは fsharpc ですね。Scala の fsc とバッティングしたので名前が長くなっています。つーか、Scala 入れてないから、fsc で ln -s してもいいですね。

image

コマンドラインから fsharpi を打った後にしばらく待たされます。更に let a = … と打った後にしばらく待たされます。どうやら mono ががんばっている模様で、ある程度ライブラリのロードが終わったら早くなります。top でみると結構上に貼りついてしまうのでなかなか大変そうです。

image

ビルドした後は分からないのですが、センサー取り込みのような素早い処理には向かないけど、ロボット制御のようなコマンド送信には十分使えるかなと思っています。まあ、それで BrickPi を購入したわけですが。de:code で MS太田さんが .NET Micro Framework で EV3 を操作できたそうなので、MF のほうでも試してみたいですね。つーか、動くのか? F#
ちょっと古いですが、2010年ごろのブログに Managed、Native、.NET Framework? – デバイスとITの架け橋 – Site Home – MSDN Blogs の冒頭「参加されていたのは組込み業界の皆さんだったにも拘らず、F#を使っている方が何人かいらっしゃって」とあるので、制御系にはうけがいいかもしれません。コンピュテーション式なんてのは、DSL 的に有用ですからね。RSNP あたりでしたっけ?

そんな訳で、お次は BrickPi のセットアップです。

カテゴリー: F#, RaspberryPi | Raspberry Pi で F# を動かすまで(まとめ) はコメントを受け付けていません

Raspberry Pi で F# を動かす(陽性かくにん版)

F# ビルドの続きです。

■fsharp のビルドでこける

正確には F# proto(F# 3.1 をビルドするための F# 3.0)のテストでこけます。

image_thumb

ガベージコレクションのテスト(かな?)をしているところで、alloc 出来なくてこけているので おそらく RasPi のメモリ不足ですね。手元の Raspberry Pi って初期型なので、メモリが少なくって USB が 2 つついているけど 256MB しかないんですよね。

image_thumb[1]

おそらく、ここの GC チェックを外してしまえばビルドできると思うのですが、どうも手軽ではない。そもそも Mono 3.2.7 のメモリでこける件もテスト自体でこけているので実害はないかもしれない。

■Debian でビルドした F# をインストールする

で、そもそもが「 FSharp.Core は環境依存していないだろう」という想定のもとに、別の Debian でビルドした FSharp.Core 等を Raspberry Pi のほうにインストールしてしまいます。これでいいのかっどうか不明なのですが(Debian 構築のほうは Hyper-V 上で x64)、同じ Linux 上だしなんとかなるかもしれません。

Debian でビルドした fsharp を tar で固めて、Raspberry Pi へコピー。その後、フォルダ名を合わせてから sudo make install します。ビルド環境のパスをあわせるのは、makefile 等にビルド環境が書き込まれているからです。

無事インストールして fsharpi します。

image

fsharpi を動かすと30秒ぐらい待たされますが、一応動きます。元ネタは 大事なことは全部MLが教えてくれた ~ Apple の Swift の mutability 周りの件を理解する – Oh, you `re no (fun _ → more) です。どうやら mono の読み込みの時にかなりパワーを使っているらしく、新しい文法を使うたびに動作が重いです。一度、読み込むと動きがスムースになるのでキャッシュあたりですかね。

という訳で例の FsRandom で陽性かくにん! も動かしてみましょう。Windows の NuGet で持ってきた FsRandom.dll を Raspberry Pi にコピーして

fsharpc -r:FsRandom.dll stap.fs

すると、無事 stap.exe が出来上がります。これを実行すると…無事 198599 番目に(でよかったのかな?)陽性かくにんできます。

image

このプログラム自体は結構なスピードで動くので、初回の mono のロードだけ重たいみたいです。

カテゴリー: F#, RaspberryPi | 1件のコメント

Mono と F# を CentOS/Debian 上でビルドしている途中

なぜか、現状の Raspberry Pi 上では

という状態で、このテストエラーは Xamarin Bug  https://bugzilla.xamarin.com/show_bug.cgi?id=17654 を見ると直っているらしい。この現象自体は Mono 3.2.7 で発生しているので、手元の Mono 3.2.8 は大丈夫なはずなんだけど、同じようにダメ。おそらく RasPi にあるレポジトリが何らかのタイミングで古いのではないか?と思のだが…これを、RasPi 上でビルドするとえらい時間がかかる。6時間かかってもまだ Mono のビルドが終わらない。

まだまだ掛かりそうなので、その前に Mono 3.2.7 と F# 3.1 の組みあわせが Linux 上で動くかどうか確認しておこう。RasPi の CPU は、ARM1176JZF-S なので ARM になる。F# のアセンブリがそのまま動けばいいのだが…これはどうなんだろう?中身は機種依存していない感じなんだが。

■CentOS に g++ を入れる

sudo yum install gcc-g++

最新の CentOS には apt-get が使えなくなっているそうなので yum を使う。手元の Raspberry Pi は http://www.raspberrypi.org/downloads/ から RASPBIAN Debian Wheezy を使っているのでそれに揃えればいいのだが…いや、一旦そろえてみよう。

CentOS 上では Mono 3.2.7 はビルドできるのだが(多々エラーがでているのが気になるけど)、その後、F# 側で ./autogen.sh –prefix /usr すると、

[masuda@centos fsharp]$ ./autogen.sh --prefix /usr
checking whether make sets $(MAKE)... yes
checking for pkg-config... /usr/bin/pkg-config
configure: "pkg-config: /usr/bin/pkg-config"
configure: "PKG_CONFIG_LIBDIR: "
configure: error: "You need mono 3.0"
[masuda@centos fsharp]$

になって詰む。ピンポイントで mono 3.0.x が必要なのだろうか?

■Debian で Mono/F# をビルドする。

Debian のレポジトリには F# パッケージがあるのだが、http://fsharp.org/use/linux/ に従って Mono からビルドしていく。コマンド自体も apt-get が使えるのでそのままコピペしながら実行。途中でたらたらと Mono をビルドしている Raspberry Pi を追い抜いて、F# のビルドまで実行している途中。あっさり、Mono 3.2.7 の問題もスルーしていったので、おそらく Raspberry Pi のレポジトリにある Mono が古いバージョンなのかもしれない。

■Respberry Pi で mono のビルドが続く

約15時間ほどかけて Raspberry Pi 上で Mono のビルドが完了。USB メモリ上でやっているので、アクセスが遅くてビルドが遅いという話もある。8GB の SD メモリを使うと幾分はやいかも。

続けて F# のビルドに入るのだが、早々に Mono 3.2.7 の問題をクリアした。やっぱり Raspberry Pi レポジトリの Mono パッケージがおかしいらしい。OS のバージョンとかもあるのかもしれないが、それぞれの最新版で作ると通るので、ひとまず安心。

カテゴリー: F#, RaspberryPi | Mono と F# を CentOS/Debian 上でビルドしている途中 はコメントを受け付けていません

Raspberry Pi で F# を動かすまで…なんだがまだ終わらず

随分前から Raspberry Pi を持っているのですが、しばらく放置中だったので再開。LEGO Mindstorms EV3 を動くところまで続ける予定です。

最初は mono と F# を入れるところまで

■ Mono をインストールする

Mono on RaspberryPi でHelloWorld – 銀の光と碧い空 を参考にして

sudo apt-get install mono-complete

で一発インストールです。私の場合 SD カードが 2GB という小さいメモリだったので、mono を入れた途端に df が 100% になって詰み。仕方がないので、余っていた 4GB の SD カードに OS を入れ直して再インストールする羽目に。

# 追記
# respi-config で Expand Filesystem すると、SD カード一杯に容量を拡張します。初期状態では、ディスクイメージのために 2.6GB ぐらいしかないんですね。実行すると手元の 8GB のメモリを認識できました。

…が、いざ Use F# on Linux | The F# Software Foundation を参考にして、

sudo apt-get install fsharp

しようとしたものの git clone した後に make したところで再びメモリ不足。F# のビルドは結構容量が多いのですね(と思ったけど、間違えて mono をビルドしてた orz、まあ USB メモリのマウントの方法が分かったので良しとするか)

さて、8GB の SD カードがあればいいのですが、手元にないので思案。幸いにして手元に USB メモリならばあるので、USBメモリのマウント : ふじかわ家のページ を参照してマウント。

mount /dev/sda1 /mnt/usbmem

が、更に問題があって、この USB メモリは FA32 フォーマットのために Linux で使う chmod 777 とかが正しく動かない。パーミッションを正しく動作させるためには ext3 あたりでフォーマットしなおさないと駄目なので、CentOS : USBメモリをLinux用にフォーマット « Demence/Cup fdisk でパーティションを削除したのち、mkfs.ext3 でフォーマット

fdisk /dev/sda1
...
mkfs.ext3 /dev/sda1

■F#をビルド…がエラーになるので

やっとこさ準備が整ったので git clone してからビルド…なのだが、make でエラーになる。

FSharp Build error on the Raspberry Pi under Mono 3.2.7 · Issue #260 · fsharp/fsharp
https://github.com/fsharp/fsharp/issues/260

と同じ現象で、手元のバージョンは Mono 3.2.8 で直っているはずなのだが。raspberry pi の mono-complete が何らかのタイミングで古いのかもしれない。仕方がないので、元に戻って mono からビルドをする。

という訳で、mono からビルド中。6時間経ったがまだ終わらず。

image

カテゴリー: F#, RaspberryPi | Raspberry Pi で F# を動かすまで…なんだがまだ終わらず はコメントを受け付けていません

Xamarin.Forms でネイティブのイベントハンドラを拾う(iOS/Android編)

Xamarin.Forms でネイティブのイベントハンドラを拾う(Windows Phone編) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/5908

の続きです。やり方は同じなのですが、iOS/Androidの場合にはネイティブのコントロールには Name プロパティがないので、Windows Phone のような FindName メソッドはないですね。iOS の場合は Outlet、Android の場合は FindViewById になるので、操作がちょっと違います。このあたりは、似たような操作(あるいは、適当なメソッドで包んでしまう)にしないと、手間がかかるので後でまとめていきましょう。

■iOSのRendererを表示する。

iOS の場合は、画面のルートが UIViewController で、その下の各種の UIView があります。Xamarin製XAMLでPageオブジェクトを作成した後は、CreateViewControllerメソッドでUIViewControllerを作ります。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Forms.Init();

    window = new UIWindow(UIScreen.MainScreen.Bounds);
    // window.RootViewController = App.GetMainPage().CreateViewController();
    var page = App.GetMainPage();
    window.RootViewController = page.CreateViewController();
    window.MakeKeyAndVisible();
    Disp(window.RootViewController);

    return true;
}

void Disp(UIViewController vc, string spc = "")
{
    Debug.WriteLine("{0}{1}", spc, vc.GetType().Name);
    Disp(vc.View);
}
void Disp(UIView vi, string spc = "")
{
    Debug.WriteLine("{0}{1}", spc, vi.GetType().Name);
    foreach (var it in vi.Subviews)
    {
        Disp(it, spc + " ");
    }
}

■iOSのネイティブコントロールにイベントをつける

方法は Windows Phone と同じで、レンダラのツリーを探索してネイティブのコントロールを探し出します。Xamarin製XAMLには名前をつけておいて、対応するネイティブコントロールを返す SetNamePageToUIelement メソッドを作ります。

UIControl SetNamePageToUIelement(string name, Xamarin.Forms.Page page)
{
    var el = page.FindByName<View>(name);
    if (el != null)
    {
        var rend = FindRenderer(el);
        if (rend != null)
        {
            // var en = rend as EntryRenderer;
            // en.Control.Name = name;
            // リフレクションで
            var pa = rend as UIView;
            var pi = pa.GetType().GetProperty(&quot;Control&quot;);
            var obj = pi.GetValue(pa);
            //obj.GetType().GetProperty(&quot;Name&quot;).SetValue(obj, name);

            return obj as UIControl;
        }
    }
    return null;
}

UIView FindRenderer(View ent)
{
    return Search(window.RootViewController.View, ent);
}
UIView Search(UIView el, View ent)
{

    var pa = el as UIView;
    if (pa != null)
    {
        var pi = pa.GetType().GetProperty(&quot;Element&quot;);
        if (pi != null)
        {
            var enel = pi.GetValue(pa);
            if (enel == ent)
            {
                return pa;
            }
        }
        foreach (var it in pa.Subviews)
        {
            var ret = Search(it, ent);
            if (ret != null)
            {
                return ret;
            }
        }
    }
    return null;
}

レンダラのツリーでペアになっている、Xamarin製コントロールとiOS謹製コントロールは、それぞれ Element プロパティと Control プロパティで取得できます。ただし、レンダラーで使ってるクラスが ViewRenderer<TView, TNativeView> のようにジェネリックになっているため、各コントロールごとにクラスが作成されています。対応するコントロールに対してのキャストをいちいちやってもいいのですが、所詮プロパティだけが欲しいのですから、リフレクションを使って省力化します。

ネイティブコントロールは UIControl を基底クラスにしているので、これを戻します。各種のイベントを付加したい場合は、もとのクラスにキャストする必要あります。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Forms.Init();

    window = new UIWindow(UIScreen.MainScreen.Bounds);
    // window.RootViewController = App.GetMainPage().CreateViewController();
    var page = App.GetMainPage();
    window.RootViewController = page.CreateViewController();
    window.MakeKeyAndVisible();
    Disp(window.RootViewController);

    UIControl uc = SetNamePageToUIelement("textUserName", page);
    var obj = uc as UITextField;
    obj.AllTouchEvents += obj_AllTouchEvents;

    return true;
}

■AndroidでRendererを表示する

Androidの場合は明示的なコンバーターを呼び出していません。SetPage メソッド内で隠蔽化されていて、レンダラのルートが解りづらいのですが、this.Window.DecorView でルートとなるビューが取得できます。ややこしいのですが、Android.Views.View と Xamarin.Forms.View と名前が混在しています。Xamarin.Forms.View のほうは、Xamarin製XAMLで使うViewで、Android.Views.View のほうはレンダリングツリーの構築のための View です。

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);

    Xamarin.Forms.Forms.Init(this, bundle);

    // SetPage(App.GetMainPage());
    var page = App.GetMainPage();
    SetPage(page);
    Disp(this.Window.DecorView);
}

void Disp(Android.Views.View vi, string spc = &quot;&quot;)
{
    System.Diagnostics.Debug.WriteLine(&quot;{0}{1}&quot;, spc, vi.GetType().Name);
    var vg = vi as ViewGroup;
    if (vg != null)
    {
        for (int i = 0; i < vg.ChildCount; i++)
        {
            var v = vg.GetChildAt(i);
            Disp(v, spc + &quot; &quot;);
        }
    }
}

Android の場合、子コントロールを取得するためには ViewGroup にキャストをします。Children コレクションを持たせてもいいような気がするのですが、Android はそういう流儀みたいです。

■Androidのネイティブコントロールにイベントを設定する

戻り値のオブジェクトが Android.Views.View になるだけで、iOS と動作は同じです。このあたりは、Windows Phone も含めてライブラリ化したいところですね。

Android.Views.View SetNamePageToUIelement(string name, Xamarin.Forms.Page page)
{
    var el = page.FindByName<Xamarin.Forms.View>(name);
    if (el != null)
    {
        var rend = FindRenderer(el);
        if (rend != null)
        {
            // リフレクションで
            var pa = rend as Android.Views.View;
            var pi = pa.GetType().GetProperty(&quot;Control&quot;);
            var obj = pi.GetValue(pa);

            return obj as Android.Views.View;
        }
    }
    return null;
}

Android.Views.View FindRenderer(Xamarin.Forms.View ent)
{
    return Search(this.Window.DecorView, ent);
}
Android.Views.View Search(Android.Views.View el, Xamarin.Forms.View ent)
{

    var pa = el as Android.Views.View;
    if (pa != null)
    {

        var pi = pa.GetType().GetProperty(&quot;Element&quot;);
        if (pi != null)
        {
            var enel = pi.GetValue(pa);
            if (enel == ent)
            {
                return pa;
            }
        }
        var vg = pa as ViewGroup;
        if (vg != null)
        {
            for (int i = 0; i < vg.ChildCount; i++)
            {
                var ret = Search(vg.GetChildAt(i), ent);
                if (ret != null)
                {
                    return ret;
                }
            }
        }
    }
    return null;
}

イベントの種類が、iOS/Android/WP と随分違うので一概に共通化できませんが、それぞれのネイティブのイベントを使うことができます。

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);
    Xamarin.Forms.Forms.Init(this, bundle);

    // SetPage(App.GetMainPage());
    var page = App.GetMainPage();
    SetPage(page);
    Disp(this.Window.DecorView);

    Android.Views.View vi = SetNamePageToUIelement("textUserName", page);
    vi.FocusChange += vi_FocusChange;
}

おそらく、将来的には基本的なタップイベントのようなものは、Xamarin.Forms で実装されるでしょうから、こまめに共通化してもあまり意味はないかなと思っています(まあ、現時点では Click イベントぐらいしかないので、実務的な意味あるんですが)。むしろ、スワイプやピンチのような特有な操作を共通にしておくとライブラリ的に意味があるかもしれません。ちょっとそのあたりは Xamarin.Forms のイベント絡みがどうなるのかが不明なので、なんとも言えませんね。

ただ、自前の TMPuzzle を移植してみた感じでは、圧倒的にイベント絡みの処理は足りなそうなので、なんらかの補完はしないと駄目そうです。ゲームアプリの場合には、ピンチ、スワイプ、コマのスライド、得点のアニメーションなど、通常のコントロールにはない操作が出てくるので、そのあたりが必要です。ええ、もちろん Unity や MonoGame を使えばいいんでしょうが、手軽に作れるパズルアプリってのは考えているので、そのあたりはおいおいと。

カテゴリー: 開発, Android, Xamarin, iOS | Xamarin.Forms でネイティブのイベントハンドラを拾う(iOS/Android編) はコメントを受け付けていません

Xamarin.Forms でネイティブのイベントハンドラを拾う(Windows Phone編)

Xamarin.Forms ではイベント絡みが隠蔽化されていて、ボタンのクリックイベントやテキストボックスの変更イベントぐらいしか発生しません。このため、パズルゲームでは画像(Imageタグ)のタップイベントを Xamarin.Froms でパズルゲームを作る(iOS/Android版) | Moonmile Solutions Blog のように TapGestureRecognizer を使っています。(今後はどうか分からないのですが)TapGestureRecognizer クラスでは、情報が何もわたってこなくてタップなりスワイプなりの操作をしようとすると、ネイティブな UI コントロールに切り替える必要が出てきます。なので、さっくっと作れそうなページであれば Xamarin.Forms で、複雑なコントロールの組み合わせであればネイティブで、ってことになるんでしょうが…いや、ちょっと待てよ。それぞれのプラットフォームでは、Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this); のように、Xamarin.Forms の XAML から各プラットフォームへのコンバータが動いています。Windows Phone の場合は ConvertPageToUIElement、iOS の場合は CreateViewController が使われています。Android の場合は SetPage が直接呼び出されていて中身が不明ですが、たぶん内容は似た感じになっているハズです。

これは、Xamarin.Forms 製の XAML から、MS 製の XAML にコンバートしていることを示しているわけで、何等かの形で内部で Windows Phone 特有のコントロールにして持っています。少し調べていて、レンダリングの部分で Xamarin.Forms の Label から iOS の UILabel を取り出す – Qiita のように LabelRenderer などで変更できることが解りました。更に調べていくと、なんとか Renderer のようなものがいっぱいあります。

image

実は、直前のバージョンでは EntryRenderer が none public になっていて手が出せなかったのですが(iOS と Android の Renderer は public になっていました)。何故か、つい最近公開された、1.1.0.6201 では、ここの Renderer が公開になっていました。ええ、ついでに内部でペアでもっていてる Element プロパティと Control プロパティも public になっています(直前のバージョンでは、名前すら違っていたのは内緒です)。ここで、情報を整理すると、

  • Xamarin.Forms.ContentPage が Xamarin.Forms の XAML ツリーを持っている。
  • ConvertPageToUIElement 等を呼び出すと、MS 製の XAML ツリーを作成する。
  • 同時に、Renderer を含む UIElement のツリーを作成し、Xamarin 製と MS 製の対応をツリー状にして保持する。
  • Xamarin.Forms 製のコントロールは、Element プロパティで取得する。
  • MS 製のコントロールは、Control プロパティで取得する。

のような感じになっています。おおまかに書くとこんな感じです。中身をみるとテキストボックスの場合には、TextBox と Password の2つのコントロールを持っているので、Xamarin.Forms 側では Entry 、MS XAML では Canvs になっています。

image

なので、Xamarin.Forms 側のコントロールから、うまく MS XAML のコントロールを見つけてやれば、Tap や Mouse イベント等のネイティブなイベントを設定できるはずです。

■Renderer のツリー を覗いてみる

ためしに、Windows Phone の MainPage クラスのコンストラクタを弄って、ツリーを書き出してみます。

public MainPage()
{
    InitializeComponent();

    Forms.Init();
    Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this);
    Disp(Content);
}

 

void Disp(UIElement el, string spc = "")
{
    var pa = el as Panel;
    if (pa == null)
    {
        Debug.WriteLine("{0}{1}", spc, el.GetType().Name);
    }
    else
    {
        Debug.WriteLine("{0}{1} '{2}'", spc, pa.GetType().Name, pa.Name);
        foreach (var it in pa.Children)
        {
            Disp(it, spc + " ");
        }
    }
}

これを実行すると、こんな感じに Renderer のツリーが取得できます。Name プロパティを出力してみたのですが、残念ながら空になっています。名前は別途 FindName で検索しないと駄目っぽいです。

Canvas ''
 PageRenderer ''
  ViewRenderer ''
   LabelRenderer ''
    TextBlock
   ViewRenderer ''
    EntryRenderer ''
     Canvas ''
      PhoneTextBox
      PasswordBox
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
   ViewRenderer ''
    ImageRenderer ''
     Image
    ImageRenderer ''
     Image
    ImageRenderer ''
     Image

■ Entry コントロールを見つけ出して、イベントを設定する

しかし、Element プロパティを調べれば、対応する Control プロパティで Windows Phone のコントロールが見つかることが分かったので、試しに Entry コントロールだけチェックしてみます。

public MainPage()
{
    InitializeComponent();

    Forms.Init();
    this.page = TMPuzzleXForms.App.GetMainPage() as TMPuzzleXForms.MainPage;
    // Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this);
    this.Content = page.ConvertPageToUIElement(this);
    Disp(Content);

    SetNamePageToUIelement("textUserName", this.page);
    var obj = this.FindName("textUserName") as UIElement;
    obj.LostFocus += obj_LostFocus;
}

void obj_LostFocus(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("lost focus");
}

Xamarin.Forms 側で textUserName と名前つけたコントロールに対応する Windows Phone のコントロールを見つけ出します。そして、LostFocus 時にデバッグ出力するコードです。

ツリーから探し出すコードはこんな風になります。コントロールを直接返すのではなく、いちど Xamarin.Forms で付けた名前と同じものを Windows Phone のコントロールにもつけています。こうすると、後から FindName で見つけられるので汎用性があります。

void SetNamePageToUIelement(string name, Xamarin.Forms.Page page )
{
    var el = page.FindByName<Entry>(name);
    if (el != null)
    {
        var rend = FindRenderer( el );
        if (rend != null)
        {
            var en = rend as EntryRenderer;
            en.Control.Name = name;
        }
    }
}

UIElement FindRenderer(Entry ent)
{
    return Search( this.Content, ent );
}
UIElement Search(UIElement el, Entry ent)
{
    var pa = el as Panel;
    if (pa != null)
    {
        var en = pa as EntryRenderer;
        if (en != null && en.Element == ent)
        {
            return en;
        }
        foreach (var it in pa.Children)
        {
            var ret = Search(it, ent);
            if (ret != null)
            {
                return ret;
            }
        }
    }
    return null;
}

まあ、いちいち探索をすると遅くなってしまうので、一度 Xamarin.Forms のツリーを Renderer で探索してしまってから名前を付けるとよいでしょう。このあたりは、後日やる予定。

ネイティブのコントロールイベントが取れるので Image コントロールの Tap イベントも付加できます。おそらく、iOS/Android も同じ方式でできると思うので、このあたりは共通して使えるようにしていきます。ちょっとやっかいなのは、EntryRenderer クラスは共通のインターフェースから継承されていなくて、ViewRenderer<Entry, System.Windows.Controls.Canvas> な感じでひとつひとつジェネリックが使われてるってところですね。取得したいところが、Control プロパティと Element プロパティなので、共通に持っている Canvas では駄目という…ここは各コントロールごとに書くしかないのかな。あるいはリフレクションを使うとうまく作れるかも。

    public class VisualElementRenderer<TElement, TNativeElement> : Canvas, IVisualElementRenderer, IRegisterable
        where TElement : Xamarin.Forms.VisualElement
        where TNativeElement : System.Windows.FrameworkElement
    {
        public VisualElementRenderer();

        protected bool AutoPackage { get; set; }
        protected bool AutoTrack { get; set; }
        public UIElement ContainerElement { get; }
        public TNativeElement Control { get; }
        public TElement Element { get; }
        protected VisualElementTracker Tracker { get; set; }

このテクニックを、月曜日のコンテストに間に合わせるか…どうかは不明。どうせならば汎用的に作っておきたいし、Image コントロールのタップは3機種同じように作りたいので。

カテゴリー: Windows Phone, Xamarin | Xamarin.Forms でネイティブのイベントハンドラを拾う(Windows Phone編) はコメントを受け付けていません