Connected Coffee Maker Part 4 – Mobile App, Powered by Xamarin

In part 3 of this series, we enabled our connected coffee maker to be controlled via a Web API using Maple Server. In this part, we’re going to explore how to easily create a mobile app that runs on iOS, Android, and Windows, powered by Xamarin.Forms that connects to and controls the coffee maker remotely, from the palm of your hand

You can find the source code for the application in our Netduino_Samples repo.

Xamarin.Forms

Despite the fact that the app runs on iOS, Android, and Windows, it has very little code and is incredibly simple, owing to the fact that we built it in Xamarin.Forms. Xamarin.Forms is a technology that allows you to create native applications that run on multiple platforms using a single codebase. For many projects, virtually all of the code is shared between the platforms. Additionally, thanks to Microsoft, Xamarin.Forms is available for free via the Visual Studio Community Edition.

UI Code

The app has two screens; the home screen which displays the status of the connected coffee maker, and a button to turn it on and off, as well as a configure screen where you can enter the IP address of the appliance to control:

Home Page XAML

We’ve built the UI using XAML, a declarative UI markup language which makes complex layouts simple, and allows a clear separation between the page logic/functionality, and the element rendering.

Our homepage is nested in a ContentPage which provides automatic navigation. Xamarin.Forms will render elements as their native types on each platform, but we wanted to customize the look of our elements, so much of the code is sizing and color modifications. Additionally; we customized the navigation toolbar to add our “Configure” button using the ToolbarItems collection:

 <?xml version="1.0" encoding="UTF-8"?>
 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="ApplianceRemote.StatusPage">
     <ContentPage.ToolbarItems>
         <ToolbarItem Text="Configure" Clicked="Configure_Clicked" />
     </ContentPage.ToolbarItems>
     <ContentPage.Content>
         <StackLayout>
             <Label x:Name="lblHeader" Text="Current Status" HorizontalOptions="Center"
                   FontSize="25" Margin="0, 20, 0, 0"></Label>
             <Label x:Name="lblStatus" HorizontalOptions="Center"></Label>
             <Button x:Name="btnAction" Clicked="Configure_Clicked" Margin="0,175,0,0" 
                    Image="ConfigureAppliance.png" />
         </StackLayout>
     </ContentPage.Content>
 </ContentPage>
 

The code behind for this page is also very simple. We could have probably simplified even more by using Xamarin.Forms bindings, but for clarity, we’re manually setting the UI state and persisting back to our App object. The important methods here are OnAppearing, Configure_Clicked, and UpdateStatus:

 using System;
 using System.Collections.Generic;
 using System.Net;
 using System.Net.Http;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 using Xamarin.Forms;
 
 namespace ApplianceRemote
 {
     public partial class StatusPage : ContentPage
     {
         ApiHelper apiHelper;
 
         public StatusPage()
         {
             InitializeComponent();
             btnAction.CommandParameter = Commands.Configure;
             this.Title = "Connected Appliance";
         }
 
         async protected override void OnAppearing()
         {
             if (string.IsNullOrEmpty(App.HostAddress))
             {
                 lblStatus.Text = "Not Connected";
             }
             else
             {
                 apiHelper = new ApiHelper(App.HostAddress);
 
                 lblStatus.Text = "Connecting to " + App.HostAddress;
 
                 App.IsConnected = await apiHelper.Connect();
                 if(App.IsConnected)
                 {
                     if (await apiHelper.CheckStatus())
                     {
                         App.ApplianceStatus = ApplianceStatus.On;
                     }
                     else
                     {
                         App.ApplianceStatus = ApplianceStatus.Off;
                     }    
                 }
                 UpdateStatus();
             }
         }
 
         async void Configure_Clicked(object sender, EventArgs e)
         {
             if(sender is Button)
             {
                 var s = (Button)sender;
                 if (s.CommandParameter.ToString() == Commands.Configure.ToString())
                 {
                     await Navigation.PushAsync(new ConfigurePage());
                 }
                 else if (s.CommandParameter.ToString() == Commands.TurnOn.ToString())
                 {
                     if (await apiHelper.TurnOn())
                     {
                         UpdateStatus();
                     }
                 }
                 else if (s.CommandParameter.ToString()== Commands.TurnOff.ToString())
                 {
                     if (await apiHelper.TurnOff())
                     {
                         UpdateStatus();
                     }
                 }
                 else
                 {
                     throw new ArgumentException();
                 }
             }
             else
             {
                 await Navigation.PushAsync(new ConfigurePage());
             }
 
         }
 
         void UpdateStatus()
         {
             if (!App.IsConnected)
             {
                 lblStatus.Text = "Not Connected";
                 btnAction.CommandParameter = Commands.Configure;
                 btnAction.Image = "ConfigureAppliance.png";
             }
             else if (App.ApplianceStatus == ApplianceStatus.On)
             {
                 lblStatus.Text = "On";
                 btnAction.CommandParameter = Commands.TurnOff;
                 btnAction.Image = "Stop.png";
             }
             else
             {
                 lblStatus.Text = "Off";
                 btnAction.CommandParameter = Commands.TurnOn;
                 btnAction.Image = "Start.png";
             }
         }
     }
 
     public enum Commands{
         Configure,
         TurnOn,
         TurnOff
     }
 }
 

OnAppearing

OnAppearing is called just before the page is displayed to the user and presents an opportunity to update the screen with any new state information. In our case, we check to see if we’re connected to the appliance and display the appropriate connection and on/off state information.

Configured_Clicked

Both the toolbar Configure button, and the main button on the home page call the Configure_Clicked clicked method. First we check to see if it was a button or a toolbar item that called it. If the main button called it, we check the command state to see whether the coffee maker should be turned on or off, and then make the appropriate call, depending on that state. If the toolbar item called the method, then we simply push the configuration page on the navigation stack.

UpdateStatus

The UpdateStatus method is called by both OnAppearing and Configure_Clicked, and has two purposes; it updates the UI with the appliance state, and it keeps the command parameter in synch with the appliance state.

Configure Page XAML

The configure page allows us to set an IP of the connected appliance to communicate with:

It’s even simpler than the home page, and has a single entry for the IP address:

 <?xml version="1.0" encoding="UTF-8"?>
 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="ApplianceRemote.ConfigurePage">
     <ContentPage.Content>
         <StackLayout Orientation="Horizontal" Padding="20,20,20,20">
             <StackLayout>
                 <Label Text="IP Address"></Label>
             </StackLayout>
             <StackLayout HorizontalOptions="EndAndExpand">
                 <Entry x:Name="IPAddressEntry" Placeholder="0.0.0.0" 
                       Keyboard="NumericTextChanged="IPAddressEntry_TextChanged" 
                       WidthRequest="150" />
             </StackLayout>
         </StackLayout>
     </ContentPage.Content>
 </ContentPage>
 
 

Again, the code behind for this uses manual UI updates instead of binding for clarity:

 using System;
 using System.Collections.Generic;
 
 using Xamarin.Forms;
 
 namespace ApplianceRemote
 {
     public partial class ConfigurePage : ContentPage
     {
         public ConfigurePage()
         {
             InitializeComponent();
             IPAddressEntry.Text = App.HostAddress;
         }
 
         void IPAddressEntry_TextChanged(object sender, EventArgs e)
         {
             App.HostAddress = ((TextChangedEventArgs)e).NewTextValue;
         }
     }
 }
 

Web API Access Code

The final, and most important part of the code is the ApiHelper class, which is responsible for actually communicating with the Web API:

 using System;
 using System.Net.Http;
 using System.Threading.Tasks;
 using Newtonsoft.Json.Linq;
 
 namespace ApplianceRemote
 {
     public class ApiHelper
     {
         private string _hostAddress;
         private string _apiBase = "";
 
         public ApiHelper(string hostAddress)
         {
             if(hostAddress == "127.0.0.1"){
                 _hostAddress = "localhost:5000";
             }
             else{
                 _hostAddress = hostAddress;
             }
         }
 
         async public Task<bool> CheckStatus()
         {
             HttpClient client = new HttpClient();
             client.BaseAddress = new Uri("http://" + _hostAddress+ "/" + _apiBase);
             var response = await client.GetAsync("status");
             if (response.IsSuccessStatusCode)
             {
                 var result = JObject.Parse(await response.Content.ReadAsStringAsync());
                 return result["isPowerOn"].Value<bool>();
             }
             else
             {
                 throw new InvalidOperationException("Could not connect to device");
             }
         }
 
         async public Task<bool> Connect()
         {
             HttpClient client = new HttpClient();
             client.Timeout = new TimeSpan(00030);
             client.BaseAddress = new Uri("http://" + _hostAddress+ "/" + _apiBase);
             try{
                 var response = await client.GetAsync("status");
                 return response.IsSuccessStatusCode;    
             }
             catch(Exception ex){
                 return false;
             }
         }
 
         async public Task<bool> TurnOn()
         {
             return await PowerCommand("turnon");
         }
 
         async public Task<bool> TurnOff()
         {
             return await PowerCommand("turnoff");
         }
 
         async private Task<bool> PowerCommand(string command)
         {
             HttpClient client = new HttpClient();
             client.BaseAddress = new Uri("http://" + _hostAddress + "/" + _apiBase);
 
             var response = await client.PostAsync(command, null);
 
             if (response.IsSuccessStatusCode)
             {
                 if (command == "turnon")
                 {
                     App.ApplianceStatus = ApplianceStatus.On;
                 }
                 else
                 {
                     App.ApplianceStatus = ApplianceStatus.Off;
                 }
             }
             return response.IsSuccessStatusCode;
         }
     }
 }
 

ApiHelper uses the standard HttpClient to make the get and post requests to and NewtonSoft.JSON to parse the JSON output from Maple Server. The core of the code is the CheckStatus, Connect, and PowerCommand methods.

CheckStatus

The CheckStatus method calls the /Status get endpoint to check whether or not the appliance is turned on. The URI of the endpoint is very simply constructed from the address of the endpoint, and _apiBase, which is just an empty placeholder in case the API was located in a subfolder.

Connect

The Connect method is similar to CheckStatus, except that we define a short timeout so that the user is informed quickly if the endpoint is not accessible.

PowerCommand

The PowerCommand method is called for both turning the coffee maker on and off, and the api endpoint, turnon/turnoff, is passed as a string. In each case, the endpoint is executed as a post request.

Extending the Connected Coffee Maker Application

This blog series and sample solution illustrated just how easy it is to create a connected appliance that uses household electricity with .NET using Netduino and Xamarin. We purposefully kept it simple, but it should serve as a fantastic base on which to expand. Everything from the 3D printable baseboard to the mobile code is open source, so feel free to dig in, fork, clean, extend, etc. Off the top of my head, I can think of some pretty easy project extensions such as an auto-off timer or a scheduler that would be easy and fun to implement, and I challenge you to give them a try!

You could also add sensor feedback for temperature and cycle power on the burner to get just the right temperature of coffee you wanted. In fact, in a future blog post, we’re going to take this a little further and start looking at sensor feedback circuits and programming to make much more intelligent connected things, so stay tuned for more!