Bicycle Computer #2 – Getting the Project Started

This is the second in a series of articles demonstrating how someone with modest .NET programming capabilities can now write applications for embedded devices using the .NET Micro Framework and Visual Studio. You can get to the first article of this series by following this link.

In this second article, we will start to lay out the UI so that we can start to envision the user experience. As we create the project, we need to give some thought to the architecture of the application. You may well hear us promote the MVC (Model View Controller) architecture. This architecture is unbeatable for making a low power device. The components (Model, View, and Controller) represent the Data, the rendering of the Data, and the application behavior. In this architecture, the ‘activity’ of the application is driven asynchronously by external events only as they are needed. The rest of the time, the processor can be in a low power state. The only difficulty with this approach is that implementing incremental functionality can be spread across updates to each of the three components – not a problem ordinarily but it makes writing these articles a little more cumbersome. For that reason, I will loosely structure the code with MVC in mind but write the functionality in a synchronous way initially. Then, in one of the later articles, we will transform this code into asynchronous execution and you will be able to compare the two approaches and we will be able to discuss the benefits directly.

Views

Let’s start with mocking up some of the views to get an idea of what works. Once I get a few views going and some potential user interaction, I can put them in front of my cyclist expert for feedback. There are two approaches to UI in NETMF. You can draw everything yourself or you can use the WPF derived classes. For an application this complex, I wouldn’t image doing without the WPF derived support. (I use the term ‘WPF derive’d as there are differences between what we support in NETMF and the full .NET. To begin with, these are very small screens so the UIs are simpler. As a result, the libraries are a subset of the full .NET. The biggest thing that customers note however is that we don’t support XAML ( and therefor the designers). This means that we will code the UI directly using the WPF derived classes. We will see that it is a little wordy but in the end, it will greatly reduce the work and complexity while increasing the flexibility.

Let’s start with a simple welcome screen.

When you create a new NETMF Windows Application, the templates that are installed as part of the SDK give us the start with a mainWindow already instantiated. Below I have cleaned it up a bit and added the components that we will be working with as we progress the application – a model, our first view, and a controller class.

using Microsoft.SPOT.Presentation;
using Microsoft.SPOT.Presentation.Media;

namespace NETMFBikeComputer
{
    public class Program : Microsoft.SPOT.Application
    {

        public static void Main()
        {

            Program myApplication = new Program();
            RideDataModel model = new RideDataModel();

            ReadyView readyView = new ReadyView(model);
            SettingsView settingsView = new SettingsView(model);
            RideView rideView = new RideView(model);
            DataView dataView = new DataView(model);
            SummaryView summaryView = new SummaryView(model);

            Window mainWindow = myApplication.CreateWindow(readyView);

            ComputerController controller = new ComputerController(myApplication, model, readyView, settingsView, rideView, dataView, summaryView, mainWindow);

            // Start the application
            myApplication.Run(mainWindow);
        }

        private Window CreateWindow(UIElement view)
        {
            mainWindow = new Window();
            mainWindow.Height = SystemMetrics.ScreenHeight;
            mainWindow.Width = SystemMetrics.ScreenWidth;
            mainWindow.Background = new ImageBrush(Resources.GetBitmap(Resources.BitmapResources.ReadyImage));

            mainWindow.Child = view;

            mainWindow.Visibility = Visibility.Visible;

            return mainWindow;

        }

        private Window mainWindow;

    }

}

 

As you can see, I put the bike rider image that I wanted as the background for the welcome screen as the background of the main window. Now let’s create our first view – the welcome or ‘ready’ view.

using Microsoft.SPOT.Presentation;
using Microsoft.SPOT.Presentation.Controls;

namespace NETMFBikeComputer
{
    class ReadyView : View
    {

        public ReadyView(RideDataModel model) : base(model)
        {

            ////////////////////////////////////////////////////////////////////////////////////////
            //  Title area
            //

            StackPanel titlePanel = new StackPanel(Orientation.Vertical);
            titlePanel.HorizontalAlignment = HorizontalAlignment.Center;
            titlePanel.VerticalAlignment = VerticalAlignment.Top;

            Image titleBMP = new Image(Resources.GetBitmap(Resources.BitmapResources.TitleBMP));
            titleBMP.SetMargin(0, 0, 0,150);

            titleBMP.HorizontalAlignment = HorizontalAlignment.Center;
            titleBMP.VerticalAlignment = VerticalAlignment.Center;

            titlePanel.Children.Add(titleBMP);

            Child = titlePanel;

        }
    }
}

 

That is pretty simple and with one element, we can’t go too far wrong. I made a bitmap of the title since it gave me more flexibility on the font that I used. The bitmaps (bicycle rider and title) can be merged at some point but for now, I’ll keep them separate while I decide for sure how this should look.

clip_image002

Let’s make the RideView. (I plan to have both an instantaneous view (what is happening now) and an aggregate view (what has happened across the entire ride. This is the instantaneous view.) This is going to be a more complex view – we want to show the time of day, date, temperature, elapsed time of the ride, the cadence of our pedaling, the speed, the rate of climb or incline that we are on, and the distance we are going – quite a complex view. The layout will be:

clip_image004

Starting with a first approximation (with the data values hardcoded for now), we have create a StackPanel for everything to fit into that orders all of its contents vertically. The StackPanels inside that the order their contents horizontally and we play with margins a bit to make things a little more presentable. (There is additional UI support available from Jens Kühner available at https://www.innobedded.com which I will look at using to enhance the UI once I have decided what it will look like.)

using System;

using Microsoft.SPOT;
using Microsoft.SPOT.Presentation;
using Microsoft.SPOT.Presentation.Controls;

namespace NETMFBikeComputer
{
    class RideView : View
    {

        public RideView(RideDataModel model)
            : base(model)
        {

            _model = model;

            if (_model.Units.Current().Label == Resources.GetString(Resources.StringResources.MetricUnits))
                _metric = true;
            else
                _metric = false;

            StackPanel ridePanel = new StackPanel(Orientation.Vertical);
            ridePanel.HorizontalAlignment = HorizontalAlignment.Center;
            ridePanel.VerticalAlignment = VerticalAlignment.Center;

            ////////////////////////////////////////////////////////////////////////////////////////
            //  Top Area
            //

            StackPanel  _topPanel = new StackPanel(Orientation.Horizontal);
            _topPanel.HorizontalAlignment = HorizontalAlignment.Center;
            _topPanel.VerticalAlignment = VerticalAlignment.Center;

            Text _date = new Text(Resources.GetFont(Resources.FontResources.miramob), DateTime.Today.Month.ToString() + "/" + DateTime.Today.Day.ToString() + "/" + DateTime.Today.Year.ToString());
            _date.SetMargin(0, 0,40, 0);

            Text _time = new Text(Resources.GetFont(Resources.FontResources.miramob), DateTime.Now.Hour.ToString() + ":" + DateTime.Now.Minute.ToString() + ":" + DateTime.Now.Second.ToString());
            _time.SetMargin(0, 0, 40, 0);

            _temperature = new Text(Resources.GetFont(Resources.FontResources.miramob), _model.getTemperature().ToString("F1") + " C");
            _topPanel.Children.Add(_date);
            _topPanel.Children.Add(_time);
            _topPanel.Children.Add(_temperature);

            ////////////////////////////////////////////////////////////////////////////////////////
            //  Middle Area
            //
            StackPanel _midPanel = new StackPanel(Orientation.Horizontal);
            StackPanel _leftMidPanel = new StackPanel(Orientation.Vertical);
            _leftMidPanel.VerticalAlignment = VerticalAlignment.Top;
            _leftMidPanel.SetMargin(0,0,0,50);

            Text _elapsedTime = new Text(Resources.GetFont(Resources.FontResources.nina48), "4:26");
            _elapsedTime.HorizontalAlignment = HorizontalAlignment.Center;
            Text _etLabel = new Text(Resources.GetFont(Resources.FontResources.miramob), Resources.GetString(Resources.StringResources.etLabel));
            _etLabel.HorizontalAlignment = HorizontalAlignment.Center;

            _leftMidPanel.Children.Add(_elapsedTime);
            _leftMidPanel.Children.Add(_etLabel);

            StackPanel _midMidPanel = new StackPanel(Orientation.Vertical);
            _midMidPanel.SetMargin(30, 50, 30, 0);

            _speed = new Text(Resources.GetFont(Resources.FontResources.nina48), _model.getSpeed().ToString("F1"));
            _speed.VerticalAlignment = VerticalAlignment.Bottom;
            if (_metric)
                _speedLabel = new Text(Resources.GetFont(Resources.FontResources.miramob), Resources.GetString(Resources.StringResources.kphLabel));
            else
                _speedLabel = new Text(Resources.GetFont(Resources.FontResources.miramob), Resources.GetString(Resources.StringResources.mphLabel));
            _speedLabel.VerticalAlignment = VerticalAlignment.Bottom;
            _speedLabel.HorizontalAlignment = HorizontalAlignment.Center;

            _midMidPanel.Children.Add(_speed);
            _midMidPanel.Children.Add(_speedLabel);

            StackPanel _rightMidPanel = new StackPanel(Orientation.Vertical);
            _rightMidPanel.SetMargin(20, 0, 0, 50);

            Text _cadence = new Text(Resources.GetFont(Resources.FontResources.nina48), _model.getCadence().ToString());
            _cadence.HorizontalAlignment = HorizontalAlignment.Center;

            Text _rpmLabel = new Text(Resources.GetFont(Resources.FontResources.miramob), Resources.GetString(Resources.StringResources.rpmLabel));
            _rpmLabel.HorizontalAlignment = HorizontalAlignment.Center;

            _rightMidPanel.Children.Add(_cadence);
            _rightMidPanel.Children.Add(_rpmLabel);

            _midPanel.Children.Add(_leftMidPanel);
            _midPanel.Children.Add(_midMidPanel);
            _midPanel.Children.Add(_rightMidPanel);

            ////////////////////////////////////////////////////////////////////////////////////////
            //  Bottom Area
            //
            StackPanel _bottomPanel = new StackPanel(Orientation.Horizontal);

            StackPanel _leftBottomPanel = new StackPanel(Orientation.Vertical);
            _leftBottomPanel.SetMargin(20, 0, 50, 0);

            _climbRate = new Text(Resources.GetFont(Resources.FontResources.nina48), _model.getHeight().ToString("F1"));
            _climbRate.HorizontalAlignment = HorizontalAlignment.Center;

            Text _ftmLabel = new Text(Resources.GetFont(Resources.FontResources.miramob), Resources.GetString(Resources.StringResources.ftmLabel));
            _ftmLabel.HorizontalAlignment = HorizontalAlignment.Center;

            _leftBottomPanel.Children.Add(_climbRate);
            _leftBottomPanel.Children.Add(_ftmLabel);

            StackPanel _rightBottomPanel = new StackPanel(Orientation.Vertical);
            _rightBottomPanel.SetMargin(60, 0, 0, 0);

            _distance = new Text(Resources.GetFont(Resources.FontResources.nina48), _model.getDistance().ToString());
            _distance.HorizontalAlignment = HorizontalAlignment.Center;
            if (_metric)
                _distanceLabel = new Text(Resources.GetFont(Resources.FontResources.miramob), Resources.GetString(Resources.StringResources.kilometerLabel));
            else
                _distanceLabel = new Text(Resources.GetFont(Resources.FontResources.miramob), Resources.GetString(Resources.StringResources.milesLabel));
            _distanceLabel.HorizontalAlignment = HorizontalAlignment.Center;

            _rightBottomPanel.Children.Add(_distance);
            _rightBottomPanel.Children.Add(_distanceLabel);

            _bottomPanel.Children.Add(_leftBottomPanel);
            _bottomPanel.Children.Add(_rightBottomPanel);

            ///////////////////////////////////////////////////////////////////////////////////////
            //  Paused Ride Panel
            //
            _pausedPanel = new StackPanel(Orientation.Vertical);
            _pausedPanel.Visibility = Visibility.Hidden;

            Text _pausedText = new Text(Resources.GetFont(Resources.FontResources.miramob), Resources.GetString(Resources.StringResources.PausedText));
            _pausedText.HorizontalAlignment = HorizontalAlignment.Center;
            _pausedText.SetMargin(0, 10, 0, 0);

            _pausedPanel.Children.Add(_pausedText);

            ///////////////////////////////////////////////////////////////////////////////////////
            //  panel Setup
            //

            ridePanel.Children.Add(_topPanel);
            ridePanel.Children.Add(_midPanel);
            ridePanel.Children.Add(_bottomPanel);
            ridePanel.Children.Add(_pausedPanel);

            Child = ridePanel;

         …

    }
}

WPF might seem a little ‘wordy’ with all the additional objects (mostly StackPanels) that we created but the view that we have is very flexible now. We can change text and fonts without having to adjust the layout. The runtime will do that for us.

clip_image006

And the results – ‘ok’ for a first approximation. The small font is too small and the layout is not very legible. First I need to get a slightly larger font and then let’s tweak the layout. We will continue to tweak this (and other) views through the project. But first, the sensors that I ordered have started arriving so I will take the next article to start looking at writing drivers in managed code and we will come back to the display.