Xamarin.Forms で Micorosft Band の加速度センサー値を取得する
Xamarin の中の人が Nuget に Xamarin 用 Microsoft Band SDK をアップしてくれたようです
これは・・・Xamarin.Forms 用のライブラリを作る意味がなくなったような;
まぁ勉強用とわりきって順次実装を試してみようと思います
まずは無難なところで加速度をとっていきます
ソースコードの一式は下記にあります!
細かい実装などはこちらを参照ください
※ 順次改修していく予定なので、この記事の内容が現時点のソースより古い可能性があります
ソリューション構成はこうなりました(Android は iOS と同じ構成なので省略しています)
センサー情報表示タブ用に SensorReadingViewModel を追加し、iOS と Android に NativeBandSensorManager と NativeBandSensorBase、加速度センサー用のクラスを2種類追加しました
センサー管理クラスは次の通り
/// <summary> /// iOS 用センサー管理クラス /// </summary> public class NativeBandSensorManager : IBandSensorManager { /// <summary> /// センサー管理クラス /// </summary> private Native.Sensors.IBandSensorManager manager = null; /// <summary> /// コンストラクタ /// </summary> /// <param name="client">iOS 用接続クライアント</param> public NativeBandSensorManager(Native.BandClient client) { this.manager = client.SensorManager; App.Container.RegisterInstance<IBandSensor<IBandAccelerometerReading>>( new NativeBandAcceleromerter(manager), new ContainerControlledLifetimeManager()); } /// <summary> /// 加速度センサー /// </summary> public IBandSensor<IBandAccelerometerReading> Accelerometer { get { return App.Container.Resolve<IBandSensor<IBandAccelerometerReading>>(); } } /// <summary> /// Galvanic 肌反応 (GSR) センサー /// </summary> public IBandContactSensor Contact { get { return App.Container.Resolve<IBandContactSensor>(); } } /// <summary> /// 移動距離センサー /// </summary> public IBandSensor<IBandDistanceReading> Distance { get { return App.Container.Resolve<IBandSensor<IBandDistanceReading>>(); } } /// <summary> /// ジャイロセンサー /// </summary> public IBandSensor<IBandGyroscopeReading> Gyroscope { get { return App.Container.Resolve<IBandSensor<IBandGyroscopeReading>>(); } } /// <summary> /// 心拍センサー /// </summary> public IBandSensor<IBandHeartRateReading> HeartRate { get { return App.Container.Resolve<IBandSensor<IBandHeartRateReading>>(); } } /// <summary> /// 歩数センサー /// </summary> public IBandSensor<IBandPedometerReading> Pedometer { get { return App.Container.Resolve<IBandSensor<IBandPedometerReading>>(); } } /// <summary> /// 肌温度センサー /// </summary> public IBandSensor<IBandSkinTemperatureReading> SkinTemperature { get { return App.Container.Resolve<IBandSensor<IBandSkinTemperatureReading>>(); } } /// <summary> /// 紫外線センサー /// </summary> public IBandSensor<IBandUVReading> UV { get { return App.Container.Resolve<IBandSensor<IBandUVReading>>(); } } /// <summary> /// 運動量センサー /// </summary> public IBandSensor<IBandCaloriesReading> Calories { get { return App.Container.Resolve<IBandSensor<IBandCaloriesReading>>(); } } }
これほんとに管理しているだけ、しかも DI コンテナのおかげでかなりすっかすかです
次にセンサークラスの基底クラス
/// <summary> /// iOS 用センサー基底クラス /// </summary> /// <typeparam name="T">センサー情報クラス</typeparam> public abstract class NativeBandSensorBase<T> : IBandSensor<T> where T : IBandSensorReading { /// <summary> /// 対応センサー検出インターバル時間 /// </summary> protected static readonly IEnumerable<TimeSpan> NativeSupportedReportingIntervals = new List<TimeSpan>(); /// <summary> /// 対応可否フラグ /// </summary> public virtual bool IsSupported { get { return true; } } /// <summary> /// センサー値変更イベント /// </summary> public abstract event EventHandler<BandSensorReadingEventArgs<T>> ReadingChanged; /// <summary> /// コンストラクタ /// </summary> /// <param name="manager">Band センサー管理クラス</param> public NativeBandSensorBase(Native.Sensors.IBandSensorManager manager) { } /// <summary> /// センサー検知を開始する /// </summary> /// <remarks>中断は非対応</remarks> /// <param name="token">中断トークン</param> /// <returns>成功した場合は<code>true</code>、それ以外は<code>false</code></returns> [Obsolete("CancellationToken is not supported for iOS.")] public virtual Task<bool> StartReadingsAsync(CancellationToken token) { return this.StartReadingsAsync(); } /// <summary> /// センサー検知を開始する /// </summary> /// <returns>成功した場合は<code>true</code>、それ以外は<code>false</code></returns> public abstract Task<bool> StartReadingsAsync(); /// <summary> /// センサー検知を停止する /// </summary> /// <remarks>非対応</remarks> /// <param name="token">中断トークン</param> /// <returns>Task</returns> [Obsolete("CancellationToken is not supported for iOS.")] public virtual Task StopReadingsAsync(CancellationToken token) { return this.StopReadingsAsync(); } /// <summary> /// センサー検知を停止する /// </summary> /// <returns>Task</returns> public abstract Task StopReadingsAsync(); /// <summary> /// 対応センサー検出インターバル時間 /// </summary> [Obsolete("SupportedReportingIntervals is not supported for iOS.")] public virtual IEnumerable<TimeSpan> SupportedReportingIntervals { get { return NativeSupportedReportingIntervals; } } /// <summary> /// センサー検出インターバル時間 /// </summary> /// <remarks>非対応</remarks> [Obsolete("SupportedReportingIntervals is not supported for iOS.")] public virtual TimeSpan ReportingInterval { get { throw new NotSupportedException("Microsoft Band SDK for iOS not supported get or set 'ReportingInterval' property."); } set { throw new NotSupportedException("Microsoft Band SDK for iOS not supported get or set 'ReportingInterval' property."); } } /// <summary> /// 現在のユーザー承諾状態を取得する /// </summary> /// <returns>ユーザー承諾状態</returns> public virtual UserConsent GetCurrentUserConsent() { return UserConsent.Granted; } /// <summary> /// センサー利用のユーザー承諾を要求する /// </summary> /// <param name="token">中断トークン</param> /// <remarks>非対応</remarks> /// <returns>成功した場合は<code>true</code>、それ以外は<code>false</code></returns> [Obsolete("SupportedReportingIntervals is not supported for iOS.")] public virtual Task<bool> RequestUserConsentAsync(CancellationToken token) { return this.RequestUserConsentAsync(); } /// <summary> /// センサー利用のユーザー承諾を要求する /// </summary> /// <returns>成功した場合は<code>true</code>、それ以外は<code>false</code></returns> public virtual Task<bool> RequestUserConsentAsync() { return Task.FromResult(true); } }
Android の方は GitHub のソースを参照ください
iOS 用 SDK には CancellationToken による中断やインターバル時間の設定ができないので、こちらは Obsolete 属性を付けて非推奨にしています
また、ジェネリックの T には各センサー用のセンサーデータインターフェースが入ります
どうも Band デバイス上でユーザーに承諾を得てから利用する作法みたいですが、日本語がおとうふ表示される・・・のでとりあえず承諾を得ないままセンサーデータを取得する実装にしています
この抽象クラスを加速度センサー用に実装したのが次のクラス
/// <summary> /// iOS 用加速度センサー /// </summary> public class NativeBandAcceleromerter : NativeBandSensorBase<IBandAccelerometerReading> { /// <summary> /// 加速度センサー /// </summary> private Native.Sensors.AccelerometerSensor sensor = null; /// <summary> /// センサー値変更イベント /// </summary> public override event EventHandler<BandSensorReadingEventArgs<IBandAccelerometerReading>> ReadingChanged; /// <summary> /// コンストラクタ /// </summary> /// <param name="manager">Band センサー管理クラス</param> public NativeBandAcceleromerter(Native.Sensors.IBandSensorManager manager) : base(manager) { this.sensor = Native.Sensors.BandSensorManagerExtensions.CreateAccelerometerSensor(manager); this.sensor.ReadingChanged += this.OnReadingChanged; } /// <summary> /// センサー値変更イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnReadingChanged(object sender, Native.Sensors.BandSensorDataEventArgs<Native.Sensors.BandSensorAccelerometerData> e) { if (this.ReadingChanged == null) { return; } this.ReadingChanged.Invoke( this, new BandSensorReadingEventArgs<IBandAccelerometerReading>(new NativeBandAccelerometerReading(e.SensorReading))); } /// <summary> /// センサー検知を開始する /// </summary> /// <returns>成功した場合は<code>true</code>、それ以外は<code>false</code></returns> public override Task<bool> StartReadingsAsync() { this.sensor.StartReadings(); return Task.FromResult(true); } /// <summary> /// センサー検知を停止する /// </summary> /// <returns>Task</returns> public override Task StopReadingsAsync() { return Task.Run(() => this.sensor.StopReadings()); } }
ネイティブの SDK を触っているだけです
そしてこのクラスでやりとりするセンサーデータのいれものが次の通り
/// <summary> /// iOS 用加速度データ /// </summary> public class NativeBandAccelerometerReading : IBandAccelerometerReading { /// <summary> /// コンストラクタ /// </summary> /// <param name="data">センサーデータ</param> public NativeBandAccelerometerReading(Native.Sensors.BandSensorAccelerometerData data) { this.Timestamp = DateTime.Now; this.AccelerationX = data.AccelerationX; this.AccelerationY = data.AccelerationY; this.AccelerationZ = data.AccelerationZ; } /// <summary> /// 検出日時 /// </summary> public DateTimeOffset Timestamp { get; private set; } /// <summary> /// X 軸加速度 /// </summary> public double AccelerationX { get; private set; } /// <summary> /// Y 軸加速度 /// </summary> public double AccelerationY { get; private set; } /// <summary> /// Z 軸加速度 /// </summary> public double AccelerationZ { get; private set; } }
IBandAccelerometerReading に従って作っているだけ
これで加速度センサーが利用できるはずなので、今度は画面側を修正します
/// <summary> /// トップ画面の ViewModel /// </summary> public class TopPageViewModel : BindableBase { ~ 中略 ~ #region SensorReading /// <summary> /// センサー情報 /// </summary> private SensorReadingViewModel sensorReading = null; /// <summary> /// センサー情報 /// </summary> public SensorReadingViewModel SensorReading { get { return this.sensorReading; } set { this.SetProperty<SensorReadingViewModel>(ref this.sensorReading, value); } } #endregion //SensorReading /// <summary> /// コンストラクタ /// </summary> /// <param name="manager">Microsoft Band デバイス管理クラス</param> [InjectionConstructor] public TopPageViewModel(IBandClientManager manager) { this.manager = manager; this.SelectBasicsCommand = new DelegateCommand(this.SelectBasics, () => { return !this.ShowBasics; }); this.SelectSensorsCommand = DelegateCommand.FromAsyncHandler(this.SelectSensors, () => { return !this.ShowSensors; }); this.ConnectCommand = DelegateCommand.FromAsyncHandler(this.Connect); App.Container.RegisterType<SensorReadingViewModel>(new ContainerControlledLifetimeManager()); } /// <summary> /// 基本設定表示切替 /// </summary> private void SelectBasics() { this.ShowSensors = false; this.ShowBasics = true; ((DelegateCommand)this.SelectBasicsCommand).RaiseCanExecuteChanged(); ((DelegateCommand)this.SelectSensorsCommand).RaiseCanExecuteChanged(); } /// <summary> /// センサー情報表示切替 /// </summary> private async Task SelectSensors() { if (!this.IsConnected) { await App.Navigation.CurrentPage.DisplayAlert("Warning", "No Microsoft Band connected.", "OK"); return; } this.ShowBasics = false; this.ShowSensors = true; ((DelegateCommand)this.SelectBasicsCommand).RaiseCanExecuteChanged(); ((DelegateCommand)this.SelectSensorsCommand).RaiseCanExecuteChanged(); } /// <summary> /// 接続処理 /// </summary> /// <returns>Task</returns> private async Task Connect() { var devices = await this.manager.GetBandsAsync(); if (!devices.Any()) { await App.Navigation.CurrentPage.DisplayAlert("Warning", "No Microsoft Band found.", "OK"); return; } var device = devices.First(); this.ConnectMessage = "Connecting to Band..."; var client = await this.manager.ConnectAsync(device); if (client == null) { await App.Navigation.CurrentPage.DisplayAlert( "Error", string.Format("Failed to connect to Microsoft Band '{0}'.", device.Name), "OK"); return; } // 別の場所から利用できるように DI コンテナに登録 App.Container.RegisterInstance<IBandClient>(client, new ContainerControlledLifetimeManager()); App.Container.RegisterInstance<IBandInfo>(device, new ContainerControlledLifetimeManager()); this.SensorReading = App.Container.Resolve<SensorReadingViewModel>(); this.ConnectMessage = string.Empty; this.BandName = device.Name; this.HardwareVersion = await client.GetHardwareVersionAsync(); this.FirmwareVersion = await client.GetFirmwareVersionAsync(); this.IsConnected = true; await App.Navigation.CurrentPage.DisplayAlert( "Connected", string.Format("Microsoft Band '{0}' connected.", device.Name), "OK"); } }
こちらはトップ画面用の ViewModel
/// <summary> /// センサー情報表示切替 /// </summary> private async Task SelectSensors() { if (!this.IsConnected) { await App.Navigation.CurrentPage.DisplayAlert("Warning", "No Microsoft Band connected.", "OK"); return; } this.ShowBasics = false; this.ShowSensors = true; ((DelegateCommand)this.SelectBasicsCommand).RaiseCanExecuteChanged(); ((DelegateCommand)this.SelectSensorsCommand).RaiseCanExecuteChanged(); }
未接続でセンサー情報タブに切り替えようとした場合はアラートを出すようにしました
/// <summary> /// 接続処理 /// </summary> /// <returns>Task</returns> private async Task Connect() { var devices = await this.manager.GetBandsAsync(); if (!devices.Any()) { await App.Navigation.CurrentPage.DisplayAlert("Warning", "No Microsoft Band found.", "OK"); return; } var device = devices.First(); this.ConnectMessage = "Connecting to Band..."; var client = await this.manager.ConnectAsync(device); if (client == null) { await App.Navigation.CurrentPage.DisplayAlert( "Error", string.Format("Failed to connect to Microsoft Band '{0}'.", device.Name), "OK"); return; } // 別の場所から利用できるように DI コンテナに登録 App.Container.RegisterInstance<IBandClient>(client, new ContainerControlledLifetimeManager()); App.Container.RegisterInstance<IBandInfo>(device, new ContainerControlledLifetimeManager()); this.SensorReading = App.Container.Resolve<SensorReadingViewModel>(); this.ConnectMessage = string.Empty; this.BandName = device.Name; this.HardwareVersion = await client.GetHardwareVersionAsync(); this.FirmwareVersion = await client.GetFirmwareVersionAsync(); this.IsConnected = true; await App.Navigation.CurrentPage.DisplayAlert( "Connected", string.Format("Microsoft Band '{0}' connected.", device.Name), "OK"); }
また、Band に接続したら、センサー情報タブ用の ViewModel を DI コンテナ経由でインスタンス化して突っ込んでいます
/// <summary> /// センサー情報 ViewModel /// </summary> public class SensorReadingViewModel : BindableBase { /// <summary> /// 接続クライアント /// </summary> private IBandClient client = null; #region IsSensorDetecting /// <summary> /// センサー監視フラグ /// </summary> private bool isSensorDetecting = false; /// <summary> /// センサー監視フラグ /// </summary> public bool IsSensorDetecting { get { return this.isSensorDetecting; } set { this.SetProperty(ref this.isSensorDetecting, value); } } #endregion //IsSensorDetecting #region ChangeDetectSensorsCommand /// <summary> /// センサー監視切替コマンド /// </summary> public ICommand ChangeDetectSensorsCommand { get; private set; } #endregion //ChangeDetectSensorsCommand #region Acceleromerter /// <summary> /// X 軸加速度 /// </summary> private double accelerationX = 0d; /// <summary> /// X 軸加速度 /// </summary> public double AccelerationX { get { return this.accelerationX; } set { this.SetProperty<double>(ref this.accelerationX, value); } } /// <summary> /// Y 軸加速度 /// </summary> private double accelerationY = 0d; /// <summary> /// Y 軸加速度 /// </summary> public double AccelerationY { get { return this.accelerationY; } set { this.SetProperty<double>(ref this.accelerationY, value); } } /// <summary> /// Z 軸加速度 /// </summary> private double accelerationZ = 0d; /// <summary> /// Z 軸加速度 /// </summary> public double AccelerationZ { get { return this.accelerationZ; } set { this.SetProperty<double>(ref this.accelerationZ, value); } } #endregion //Acceleromerter /// <summary> /// コンストラクタ /// </summary> /// <param name="client">接続クライアント</param> [InjectionConstructor] public SensorReadingViewModel(IBandClient client) { this.client = client; this.ChangeDetectSensorsCommand = DelegateCommand<bool>.FromAsyncHandler(this.ChangeDetectSensors); } /// <summary> /// センサー監視切替 /// </summary> /// <param name="detecting">センサー監視フラグ</param> /// <returns>Task</returns> private async Task ChangeDetectSensors(bool detecting) { if (detecting) { // 加速度センサーの検知開始 if (this.client.SensorManager.Accelerometer.IsSupported) { await this.client.SensorManager.Accelerometer.StartReadingsAsync(); this.client.SensorManager.Accelerometer.ReadingChanged += this.OnAccelerometerReadingChanged; } } else { // 加速度センサーの検知終了 if (this.client.SensorManager.Accelerometer.IsSupported) { await this.client.SensorManager.Accelerometer.StopReadingsAsync(); this.client.SensorManager.Accelerometer.ReadingChanged -= this.OnAccelerometerReadingChanged; this.AccelerationX = 0d; this.AccelerationY = 0d; this.AccelerationZ = 0d; } } } /// <summary> /// 加速度変更イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnAccelerometerReadingChanged(object sender, BandSensorReadingEventArgs<IBandAccelerometerReading> e) { if (e == null) { return; } this.AccelerationX = e.SensorReading.AccelerationX; this.AccelerationY = e.SensorReading.AccelerationY; this.AccelerationZ = e.SensorReading.AccelerationZ; } }
そのセンサー情報タブ用の ViewModel はこんな感じで、コンストラクタに引数を付けることで、DI コンテナ経由で接続後の IBandClient インスタンスを受け渡しています・・・こうすると TopPageViewModel と SensorReadingViewModel がお互いに疎結合になるので保守性が上がります
あとは Band SDK をラッピングした API を使うだけ
// 加速度センサーの検知開始 if (this.client.SensorManager.Accelerometer.IsSupported) { await this.client.SensorManager.Accelerometer.StartReadingsAsync(); this.client.SensorManager.Accelerometer.ReadingChanged += this.OnAccelerometerReadingChanged; }
センサー値の検知を開始するコードがこの部分です
IsSupported が true だったら .Accelerometer.StartReadingsAsync() メソッドを呼び、加速度センサー値の変更イベントの購読を開始するようにしています
// 加速度センサーの検知終了 if (this.client.SensorManager.Accelerometer.IsSupported) { await this.client.SensorManager.Accelerometer.StopReadingsAsync(); this.client.SensorManager.Accelerometer.ReadingChanged -= this.OnAccelerometerReadingChanged; this.AccelerationX = 0d; this.AccelerationY = 0d; this.AccelerationZ = 0d; }
逆に検知を止めるには、開始の時の反対の操作をするだけ
/// <summary> /// 加速度変更イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnAccelerometerReadingChanged(object sender, BandSensorReadingEventArgs<IBandAccelerometerReading> e) { if (e == null) { return; } this.AccelerationX = e.SensorReading.AccelerationX; this.AccelerationY = e.SensorReading.AccelerationY; this.AccelerationZ = e.SensorReading.AccelerationZ; }
加速度の変更イベントハンドラは受けとったイベント引数をプロパティに受け渡しているだけ・・・実にかんたん
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:cv="clr-namespace:XamarinBandSample.Converters;assembly=XamarinBandSample" xmlns:t="clr-namespace:XamarinBandSample.Triggers;assembly=XamarinBandSample" xmlns:prismmvvm="clr-namespace:Prism.Mvvm;assembly=XamarinBandSample" prismmvvm:ViewModelLocator.AutoWireViewModel="true" x:Class="XamarinBandSample.Views.TopPage"> ~ 中略 ~ <!-- Sensor Info Pain--> <ContentView IsVisible="{Binding ShowSensors}"> <Grid Padding="10" RowSpacing="10" ColumnSpacing="10" BindingContext="{Binding SensorReading}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="100"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Text="Detecting" Grid.Column="0" Grid.Row="0" VerticalOptions="Center" FontSize="Medium"/> <Switch IsToggled="{Binding IsSensorDetecting}" Grid.Column="1" Grid.Row="0"> <Switch.Triggers> <EventTrigger Event="Toggled"> <t:InvokeCommandAction Command="{Binding ChangeDetectSensorsCommand}" CommandParameter="{Binding IsSensorDetecting}"/> </EventTrigger> </Switch.Triggers> </Switch> <StackLayout Orientation="Horizontal" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Spacing="5"> <Label Text="Accelometer:" FontSize="Medium"/> <StackLayout Orientation="Vertical" Padding="10,0,0,0" Spacing="10"> <Label Text="{Binding AccelerationX, StringFormat='x={0}'}" FontSize="Small"/> <Label Text="{Binding AccelerationY, StringFormat='y={0}'}" FontSize="Small"/> <Label Text="{Binding AccelerationZ, StringFormat='z={0}'}" FontSize="Small"/> </StackLayout> </StackLayout> </Grid> </ContentView> </Grid> </ContentPage>
最後に XAML に加速度センサー取得値表示用の UI を追加
<Label Text="Accelometer:" FontSize="Medium"/> <StackLayout Orientation="Vertical" Padding="10,0,0,0" Spacing="10"> <Label Text="{Binding AccelerationX, StringFormat='x={0}'}" FontSize="Small"/> <Label Text="{Binding AccelerationY, StringFormat='y={0}'}" FontSize="Small"/> <Label Text="{Binding AccelerationZ, StringFormat='z={0}'}" FontSize="Small"/> </StackLayout>
X、Y、Z 軸の各加速度を表示するようにしました
さて、お楽しみのビルド実行!
加速度トレター!ヤッター!