Windows IoT Core と MVVM の関係

先月の .NETラボで Windows IoT Core のデモをやりましたが、そこで使ったソースコードを公開します。

moonmile/WinIoT
https://github.com/moonmile/WinIoT/tree/master/HelloSample

HelloServo のサンプルは Raspberry Pi では動作しない(他では動作している?)のでなんとも言えませんが、他のコードはほどよく Hello 的に使えるものです。当日はコードの解説はせず、概要と動作だけだったので、ちょっとコードのほうも解説しておきます。

環境を整える

木澤さんの I/O ネタにも突っ込みを入れたのですが、現在 Winodws IoT の開発環境づくりは結構な困難を伴います。有償(とされる)Enterprize 版の場合もこのような状況なのかはわかりませんが、少なくとも開発者が自由に使える無償版は、現状こんな感じです。

  • 最新の Windows IoT Core 10.0.10585 を micro SD カードに焼きこんだ時は、Windows 10.0.10585 + Visual Studio 2015 Update 1 の環境が必要になる。
  • 最新の Windows IoT Core 10.0.10585 には、リモート環境が含まれておらず、直前のバージョンから RDBG フォルダをコピーする必要がある。

Windows IoT Core の最新バージョンは Get Started with Windows IoT から Windows IoT Core Dashboard を使ってインストールします。最初の頃はファイルをダウンロードしてコマンドラインからインストール、というややこしい手順だったのですが、最近のものはえらい変わりました。ついでに言うと、先日の発表をした頃よりも変わっていて、自動的に micro SD カードに焼きこむファイルをダウンロードするようです。
image

ちょっと前の従来のものは Download Windows 10 IoT Core からダウンロードができます。

トップページのドキュメント自体が Windows 10 (version 10.0.10240) or better. となっているので、Windows 10 の細かいバージョンが怪しいのですが、デバッグの関係上 Visual Studio で作成するユニバーサルアプリのバージョンと Raspberry Pi 上で動く Windows IoT Core のバージョンは揃えないといけません。

Visual Studio 2015 から Raspberry Pi へプログラムを送り込むにはリモートデバッグが便利なのですが、何故か10585 版の環境には肝心なリモートデバッグの環境が含まれていません。入れ忘れたのか、有償版には入っていて無償版にはないのか、は定かではありませんが 10585 版はそのままではリモートデバッグができません。このあたりは Windows IoT Core ver.10.0.10586 のデバッグを有効にする | Moonmile Solutions Blog を参考にして、RDBG フォルダをコピーしてください。直前のバージョンの RDBG フォルダは http://1drv.ms/1QqAKz4 にあります。

時計を動かす

Windows IoT Core はユニバーサルアプリ(UWP)で作りますが、これの最大のメリットはユーザーインターフェースのテストを普通の PC で行えることです。

HelloSample プロジェクトは、画面に時刻を表示するアプリですが特に Raspberry Pi の GPIO などをアクセスしている訳ではないので、PC 上で動きます。もちろん、Windows Phone 上でも動きます。

image

また、画面は XAML で作成するので MVVM パターンを使うことができます。HelloSample プロジェクトでは、時計の表示部分を1秒ごとに更新していますが、これは TextBlock を直接更新しているわけではなくて、わざわざ Binding を使っています。この程度だとバインディングを使わなくていいのですが、まあ、先の Xamarin + MVVM の流れでこうしています。

 
public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
        this.Loaded += (s, e) =>
        {
            this.DataContext = _vm = new ViewModel(this.Dispatcher);
            _vm.TimeStart();
        };
        this.Unloaded += (s, e) => { _vm.TimeStop(); };
    }
    ViewModel _vm;
}

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    Windows.UI.Core.CoreDispatcher _disp;

    public ViewModel(Windows.UI.Core.CoreDispatcher disp )
    {
        _disp = disp;
    }

    private DateTime _time = DateTime.Now;
    public DateTime Time
    {
        get { return _time; }
        set { this.SetProperty(ref _time, value); }
    }

    Task _task;
    bool _loop = false;
    public void TimeStart()
    {
        _loop = true;
        _task = new Task(async () => {
            while( _loop )
            {
                await _disp.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    this.Time = DateTime.Now;
                });
                await Task.Delay(1000);
            }
        });
        _task.Start();
    }
    public void TimeStop()
    {
        _loop = false;
    }


    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
    {
        if (object.Equals(storage, value)) return false;

        storage = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var eventHandler = this.PropertyChanged;
        if (eventHandler != null)
        {
            eventHandler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

タイマーは Task クラスで別スレッドにしているので、RunAsync メソッドを使ってスレッド越えをして UI にアクセスする必要があります。このあたり、プロパティアクセスと UI アクセスが混在している VM の弱点でもありますが、逆に言えば、Model にスレッド絡みをいれてしまうと View の結合が強くなってしまうので本末転倒、そうなると ViewModel に押し込むのがベターか、といったところです。

Lチカとタクトスイッチ

HelloLED では、GPIO で Lチカ をするのと、タクトスイッチという物理ボタンをブレッドボードに乗せてスイッチに反応するパターンです。いわば、アウトプットが Lチカで、インプットがタクトスイッチです。

image

アウトプットのほうは、LED だけでなく、液晶ディスプレイとか、モーターへの出力とか、出力全般を扱うための基礎になります。インプットは、温度センサーや、マウスやタッチパネル、角速度センサーの検出などの基礎です。インプットは、OS の割り込みのようなものですが、Windows IoT Core では適当なイベントにして戻してくれます。このイベントの内部動作は、後々詳しく説明したいと思うのですが(結局のところ Windows のイベント駆動と同じなので)、ここでは「便利に」イベントとして返してくれるところに注目してください。ちなみに、Arduino IDE でタクトスイッチを扱うときは意外と面倒です。

public MainPage()
{
    this.InitializeComponent();

    var gpio = GpioController.GetDefault();
    this.ledPin = gpio.OpenPin(LED_PIN);
    this.buttonPin = gpio.OpenPin(BUTTON_PIN);

    ledPin.SetDriveMode(GpioPinDriveMode.Output);
    buttonPin.SetDriveMode(GpioPinDriveMode.InputPullUp);

    ledPin.Write(GpioPinValue.Low);
    _isLED = false;

    buttonPin.ValueChanged += ButtonPin_ValueChanged;

}

private async void  ButtonPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs args)
{
    await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => {
        // if (buttonPin.Read() == GpioPinValue.High )
        if ( args.Edge == GpioPinEdge.FallingEdge)
        {
            ellipse1.Fill = new SolidColorBrush(Colors.Red);
        }
        else
        {
            ellipse1.Fill = new SolidColorBrush(Colors.Gray);
        }
    });
}

タクトスイッチのボタンイベントは、ValueChanged として扱います。これは Read だけで読み取ると、押している瞬間は電気信号として +/- をうろうろすることがあるからです。タクトスイッチとはいえ、電留が流れる部分はアナログ的な電気接点ですから、物理的に接点がちょっと離れたり押されたりするわけです。ですが、ソフトウェアのほうでは 0/1 のデジタルで判断したいので、途中の揺れの部分は必要ありません。むしろ、0/1 のあたりをうろうろしているのは邪魔ですよね。というわけで、ValueChanged イベントは、一定ので電圧以下から以上になるタイミング、あるいは一定の電圧以上から以下になるタイミングでイベントを発生しています…ってはずなのですが、内部はあとで調べてみます。少なくとも、微妙な接点によるうろいろしたところを削って、0/1 で反応させることができます。

また、一見、UI のボタンイベントのように見えますが、ハードウェアからイベントなのでスレッド越えのときと同じように Dispatcher.RunAsync を使う必要があります。このあたりのスレッドを意識しなければいけないのは、Windows IoT Core の難しいところなのか、不備なのかは謎です…が、ワタクシ的には「不備」ですね。もうちょっと MVVM パターンも含めてスレッド越えを意識しないようなコードにしたいところです。

モーターを動かす

本当はサーボモーターの制御を試したかったのですが、Windows.Devices.Pwm の中身がからっぽっぽいので、急遽ブラシモータに変更しました。Windows.Devices.Pwm のほうはいずれ中身を作っていきたいと思います。

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
        this.Loaded += MainPage_Loaded;
        this.Unloaded += (s, e) => _motor.Stop();
    }

    private void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        var gpio = GpioController.GetDefault();
        _motor = new Motor(gpio.OpenPin(22), gpio.OpenPin(27));
    }

    Motor _motor;

    private void clickFront(object sender, RoutedEventArgs e)
    {
        _motor.GoFront();
    }
    private void clickBack(object sender, RoutedEventArgs e)
    {
        _motor.GoBack();
    }
    private void clickStop(object sender, RoutedEventArgs e)
    {
        _motor.Stop();
    }
}

public class Motor
{
    GpioPin _front, _back;
    public Motor( GpioPin front, GpioPin back )
    {
        _front = front;
        _back = back;

        _front.SetDriveMode(GpioPinDriveMode.Output);
        _front.Write(GpioPinValue.Low);
        _back.SetDriveMode(GpioPinDriveMode.Output);
        _back.Write(GpioPinValue.Low);
    }

    public void GoFront()
    {
        _front.Write(GpioPinValue.High);
        _back.Write(GpioPinValue.Low);
    }
    public void GoBack()
    {
        _front.Write(GpioPinValue.Low);
        _back.Write(GpioPinValue.High);
    }
    public void Stop()
    {
        _front.Write(GpioPinValue.Low);
        _back.Write(GpioPinValue.Low);
    }
}

モーターを前後に動かすためには、+ と – を交換すればよいだけです。ただし、ふつうのモーターを動かすためにはある程度の電流が必要になるので、Raspberry Pi の GPIO に直接さして動かすわけにはいきません。別途モータードライバが必要になります。ちなみに、小さなモーター(携帯用の振動モーターなど)は直接さしても動きます。

+/- を逆転させるために、2本のGPIOを使います。それぞれの GPIO に対して HIGH/LOW を逆転させてもよいのですが、自前で Motor クラスを作成しています。この Motor クラスを使って制御ができるのがオブジェクト指向のよいところですよね。ここにハードウェア制御だけでなく、液晶ディスプレイに状態をあらわすような VM を追加していけば、うまく V-VM-M が分離できるてシミュレータで動かしたり、インターネット経由で動かしたりというアスペクト指向的な実装も可能になります。

カテゴリー: C#, RaspberryPi パーマリンク