UWPアプリでバックグラウンドとフォアグラウンドを連携する

Link background and foreground UWP apps
前回では「Arduino Wiring Application for Windows IoT Core」を使ったラズパイでの高速GPIO制御のお話をしました。先述の通りこのプロジェクトはバックグラウンドアプリのため、単体ではGUIが使えません。と、いうわけでバックグラウンドとフォアグラウンドを連携するプログラミングのお話をしたいと思います。今回は手始めとしてすべてC#でプログラムを組んでいます。

一番手っ取り早いのは、バックグラウンドアプリをアプリサービスとして動作させ、必要に応じて登録されたフォアグラウンドアプリにイベントを送信する方法です。バックグラウンドタスクとは違い、アプリサービスではストレージを介さずに直接データをやりとりすることができるのも、これを利用する理由の一つです。

それではプログラミングを始めましょう。まずはソリューションを作成して、フォアグラウンドアプリ(空白のアプリ)プロジェクトを作成し、次にバックグラウンドアプリ(Background Application (IoT))プロジェクトを作成して同じソリューションに追加します。続いてソリューションのプロパティーを開き、「プロジェクトの依存関係」でフォアグラウンドアプリがバックグラウンドアプリに依存していることを指定することで、デバッグ時に両方のアプリが動作するようにします。
続いてバックグラウンドアプリをサービスとして動作させるように、マニフェストを変更します。一覧にある「宣言」にある「バックグラウンドタスク」を削除して、かわりに「App Service」を追加します。プロパティの「名前」と「エントリポイント」は「バックグラウンドタスク」での定義と同じにします。
LEDとタクトスイッチを配置しただけの至極単純なものですが、今回のプログラムにおける配線図を掲載しておきます。
次にコード例を紹介します。フォアグラウンドアプリではバックグラウンドアプリの「App Service」で指定した名前を連携先として登録し、GUI上のLEDボタンが押されたらバックグラウンドアプリにGPIO 17の値を切り替えるメッセージを送信しています。逆にタクトスイッチが押されてGPIO 18の値が変われば、登録されたフォアグラウンドアプリにその旨のメッセージが送信されます。
なお、LightningProviderについては過去の記事をご覧ください。
mainpage.cs
// UiApp.MainPage in UiApp
using System;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

namespace UiApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private static readonly string ServiceName = "BgApp";
        private AppServiceConnection service = null;

        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            var listing = await AppServiceCatalog.FindAppServiceProvidersAsync(ServiceName);
            if (listing.Count != 1) return;

            service = new AppServiceConnection();
            service.AppServiceName = ServiceName;
            service.PackageFamilyName = listing[0].PackageFamilyName;

            var res = await service.OpenAsync();
            if (res != AppServiceConnectionStatus.Success) {
                txtStatus.Text = "Can't connect to " + res.ToString();
            } else {
                service.RequestReceived += OnServiceRequestReceived;
                txtStatus.Text = "Ready";
            }
        }

        private SolidColorBrush brush_on = new SolidColorBrush(Color.FromArgb(255, 209, 236, 255));
        private SolidColorBrush brush_off = new SolidColorBrush(Color.FromArgb(255, 255, 170, 204));

        private async void OnServiceRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => {
                var d = args.GetDeferral();

                ValueSet res = new ValueSet();

                string lblmsg = "";
                SolidColorBrush bgbrush = new SolidColorBrush();
                try {
                    bool pin = (bool)args.Request.Message["pin"];
                    if(pin == true) {
                        lblmsg = "Switch On";
                        bgbrush = brush_on;
                    } else {
                        lblmsg = "Switch Off";
                        bgbrush = brush_off;
                    }
                    res.Add("message", "ok");
                } catch (Exception ex) {
                    res.Add("message", ex.Message);
                }
                await args.Request.SendResponseAsync(res);

                lblSwitch.Text = lblmsg;
                pnlSwitch.Background = bgbrush;

                d.Complete();
            });
        }

        private bool led_on = false;

        private async void OnLedButtonClick(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            SolidColorBrush fgbrush = new SolidColorBrush(Colors.Black);

            led_on = !led_on;

            ValueSet message = new ValueSet();
            message.Add("led", led_on);

            var resp = await service.SendMessageAsync(message);
            string msg;
            if (resp.Status == AppServiceResponseStatus.Success) {
                msg = resp.Message["message"].ToString();
            } else {
                msg = "An error has occured : " + resp.Status;
                fgbrush = new SolidColorBrush(Colors.Red);
            }

            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => {
                btnLED.Content = string.Format("LED {0}", (led_on == true) ? "On" : "Off");
                btnLED.Background = (led_on == true) ? brush_on : brush_off;
                txtStatus.Text = msg;
                txtStatus.Foreground = fgbrush;
            });
        }
    }
}
startup.cs
// StartupTask.cs in BgApp
using System;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.Devices;
using Windows.Devices.Gpio;
using Windows.Foundation.Collections;
using Microsoft.IoT.Lightning.Providers;

namespace BgApp
{
    public sealed class StartupTask : IBackgroundTask
    {
        private BackgroundTaskDeferral deferral = null;
        private AppServiceConnection appcon = null;

        private GpioPin pin17 = null, pin18 = null;

        public void Run(IBackgroundTaskInstance taskInstance)
        {
            // Keep the background task
            deferral = taskInstance.GetDeferral();
            taskInstance.Canceled += OnTaskCanceled;

            var triggerDetails = taskInstance.TriggerDetails as AppServiceTriggerDetails;
            appcon = triggerDetails.AppServiceConnection;
            appcon.RequestReceived += OnConnectionReceived;

            // Initialize the pins by direct memory mapping.
            if (LightningProvider.IsLightningEnabled) {
                LowLevelDevicesController.DefaultProvider = LightningProvider.GetAggregateProvider();
            }

            GpioController gc = GpioController.GetDefault();

            pin17 = gc.OpenPin(17);
            pin17.SetDriveMode(GpioPinDriveMode.Output);

            pin18 = gc.OpenPin(18);
            pin18.SetDriveMode(GpioPinDriveMode.Input);
            pin18.ValueChanged += OnPin18Changed;
        }

        private async void OnPin18Changed(GpioPin sender, GpioPinValueChangedEventArgs args)
        {
            var messages = new ValueSet();
            if (pin18.Read() == GpioPinValue.High) {
                messages.Add("pin", true);
            } else {
                messages.Add("pin", false);
            }
            await appcon.SendMessageAsync(messages);
        }

        private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            if (deferral != null) {
                deferral.Complete();
                deferral = null;
            }
        }

        private async void OnConnectionReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            var deferral = args.GetDeferral();

            ValueSet resp = new ValueSet();
            try {
                bool led = (bool)args.Request.Message["led"];
                if (pin17 != null) {
                    pin17.Write(led ? GpioPinValue.High : GpioPinValue.Low);
                    resp.Add("message", "ok");
                }else {
                    resp.Add("message", "pin17 is not initialized.");
                }
            } catch (Exception ex) {
                resp.Add("message", ex.Message);
            }
            await args.Request.SendResponseAsync(resp);

            deferral.Complete();
        }
    }
}
XAMLファイルによるUIレイアウトを含めたフルプロジェクトはこちらよりダウンロードできます。
2016/11/03