Wednesday, June 27, 2012

Submitting Fake Tracks

Now that we have generated track data and saved it to our cloud storage. We can stub out the form that will post the fake GPS data to our (not yet implemented) API.

The Goal

We want to the user to be able to select a track, set a delay interval and indicate whether or not to randomly vary that interval (for realism). Then start the process of sending the data to our API. We also want to give the user some visual feedback of the data as it is being processed.

Track Faker

Here is a mock up of the track faker tab:


As you can see, we are capturing the device, track, delay and randomization flag at the top of the screen. There is also a Start/Cancel button at the top right. The bottom portion is a rolling log of events once the track has been started. We will make a small variation to this screen as we will make the log add events at the top of the log instead of the bottom.

Multi-Threading and Tasks

This feature is a great example of a long running task that will need to be broken out into its own thread in order to keep the application from becoming unresponsive. We will use the new Task Parallel Library to kick off a background thread that will read each GPS point, build the proper API post and update the UI with its progress. The task is simple to set up, we instantiate a cancellation token source and use it when we create the task like so:

_cancellationTokenSource = new CancellationTokenSource();
            CancellationToken cancellationToken = _cancellationTokenSource.Token; 
            
            Task.Factory.StartNew(() =>
            {
            }, cancellationToken);

This allows the task to be cancelled with a simple call from anywhere outside the task:
_cancellationTokenSource.Cancel();

Retrieving Files From Storage

So far we have gone through saving a file to blob storage, now lets have a look at retrieving a file. We can have the file retrieved as a memory stream or as text. Since we are going to deserialize it into an class we will use the memory stream.

public static MemoryStream RetrieveBlobStream(string containerName, string fileName)
        {
            try
            {
                var stream = new MemoryStream();
                var storageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");

                CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);

                CloudBlob blob = container.GetBlobReference(fileName);
                blob.DownloadToStream(stream);
                return stream;
            }
            catch (Exception)
            {
                //TODO: Log error here...
                return null;
            }
        }

GPX POCO Generation

Wait? What? Deserialize it into a class? What class you say? That's easy the gpx.cs class we created using the xds.exe tool. Simply give it the proper parameters and the uri to your xsd file and it will generate a class for you.
//------------------------------------------------------------------------------
// 
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.17379
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// 
//------------------------------------------------------------------------------

// 
// This source code was auto-generated by xsd, Version=4.0.30319.17379.
// 
namespace iGOR.App.GPX
{
    /// 
    [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.17379")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://www.topografix.com/GPX/1/1")]
    [System.Xml.Serialization.XmlRootAttribute("gpx", Namespace="http://www.topografix.com/GPX/1/1", IsNullable=false)]
    public partial class gpxType {
        
        private metadataType metadataField;
        
        private wptType[] wptField;
        
        private rteType[] rteField;
        
        private trkType[] trkField;
        
        private extensionsType extensionsField;
        
        private string versionField;
        
        private string creatorField;
        
        public gpxType() {
            this.versionField = "1.1";
        }
        
        /// 
        public metadataType metadata {
            get {
                return this.metadataField;
            }
            set {
                this.metadataField = value;
            }
        }
        
        /// 
        [System.Xml.Serialization.XmlElementAttribute("wpt")]
        public wptType[] wpt {
            get {
                return this.wptField;
            }
            set {
                this.wptField = value;
            }
        }
        
        /// 
        [System.Xml.Serialization.XmlElementAttribute("rte")]
        public rteType[] rte {
            get {
                return this.rteField;
            }
            set {
                this.rteField = value;
            }
        }

        //Snip a ton of code...
    }
}

Putting It All Together

Now we have all of the pieces, lets look at the end result. Remember this is a stub and not the complete solution for this tab yet. We still need to get our list of devices from somewhere and do the actual calls to our API.
private void cmdStart_Click(object sender, EventArgs e)
        {
            if (cboTracks.Text.Length == 0)
            {
                MessageBox.Show(
                    "Please select a demo track",
                    "Demo Track",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return;
            }

            _cancellationTokenSource = new CancellationTokenSource();
            CancellationToken cancellationToken = _cancellationTokenSource.Token; 
            
            Task.Factory.StartNew(() =>
            {
                decimal lowerTimer;
                decimal upperTimer;

                cmdStart.Invoke((Action)delegate { cmdStart.Visible = false; });
                cmdCancel.Invoke((Action)delegate { cmdCancel.Visible = true; });

                txtLog.Invoke((Action)delegate { txtLog.Text = "Loading track " + cboTracks.Text + "..." + Environment.NewLine; });

                var timeString = "Interval between location submits will be ";

                if (chkRandomize.Checked)
                {
                    decimal randomizerPercentage = Properties.Settings.Default.IntervalRandomizerPercentage;
                    decimal variance = spnInterval.Value * (randomizerPercentage / 100);
                    lowerTimer = spnInterval.Value - variance;
                    upperTimer = spnInterval.Value + variance;

                    timeString += "between " + lowerTimer + " and " + upperTimer + " seconds." + Environment.NewLine;
                }
                else
                {
                    lowerTimer = upperTimer = spnInterval.Value;
                    timeString += "exactly " + lowerTimer + " seconds." + Environment.NewLine;
                }

                txtLog.Invoke((Action)delegate { txtLog.Text = timeString + txtLog.Text; });

                if (cancellationToken.IsCancellationRequested)
                {
                    // another thread decided to cancel
                    txtLog.Invoke((Action)delegate { txtLog.Text = "Simulation Cancelled..." + Environment.NewLine + txtLog.Text; });
                    return;
                } 

                var containerName = ConfigurationManager.AppSettings["DemoTrackContainer"];
                var stream = Blob.RetrieveBlobStream(containerName, cboTracks.Text);
                stream.Position = 0;

                var mySerializer = new XmlSerializer(typeof(GPX.gpxType));
                var track = (GPX.gpxType)mySerializer.Deserialize(stream);

                var rdm = new Random();
                int min = Convert.ToInt32(lowerTimer * 1000);
                int max = Convert.ToInt32(upperTimer * 1000);

                foreach (var point in track.rte[0].rtept)
                {
                    if (cancellationToken.IsCancellationRequested)
                    {
                        // another thread decided to cancel
                        txtLog.Invoke((Action)delegate { txtLog.Text = "Simulation Cancelled..." + Environment.NewLine + txtLog.Text; });
                        return;
                    }

                    int waitTime = rdm.Next(min, max);
                    txtLog.Invoke((Action)delegate { txtLog.Text = "Waiting " + waitTime + " milliseconds..." + Environment.NewLine + txtLog.Text; });
                    Thread.Sleep(waitTime);
                    txtLog.Invoke((Action)delegate { txtLog.Text = "Sending Lat: " + point.lat + ", Lon: " + point.lon + Environment.NewLine + txtLog.Text; });
                    // TODO: Actually send the data to the API...
                }

                txtLog.Invoke((Action)delegate { txtLog.Text = "Simulation complete..." + Environment.NewLine + txtLog.Text; });

                cmdStart.Visible = true;
                cmdCancel.Visible = false;
            }, cancellationToken);
        }



No comments:

Post a Comment