Wednesday, July 30, 2014

Unity 3D and Phidgets: Weather Underground app

Looking to have a little fun with my Phidgets displays, and provide some relatively useful functionality, I decided to make a little weather app. This app grabs weather data from Weather Underground at regular intervals for as many ZIP codes as you'd like, then displays the data on a Phidgets Text LCD device. It's not terribly complicated but it is a nice way to show the Text LCD functionality as well as using the .NET WebClient class in Unity and some simple XML parsing.

First things first: do your basic Phidgets setup in a Unity project. If you'd like to peruse the Phidgets Text LCD tutorial that I wrote a while back, that will give you a little background on the hardware commands, although we'll touch on that here as well.

Also, you'll need to set up a Weather Underground developer account to get access to your personalized API key.

So, you should at this point have a Weather Underground API key and a basic Unity project ready to go. We're going to keep this all very simple and you can expand on it as you like.

First, I'm going to set up a script to handle communication with my display. Create this script and drop it on to a GameObject in your scene (I called mine "DisplayController").

using Phidgets;
using Phidgets.Events;
using UnityEngine;
using System.Collections;

public class DisplayController : MonoBehaviour
{
    private TextLCD lcdDisplay= new TextLCD ();
    private bool lcdDisplayIsAttached = false;
  
    void Start ()
    {
        lcdDisplay.Attach += new AttachEventHandler (lcdControl_Attach);
        lcdDisplay.Detach += new DetachEventHandler (lcdControl_Detach);
        lcdDisplay.Error += new ErrorEventHandler (lcdControl_Error);
      
        lcdDisplay.open ();
    }
  
    void lcdControl_Attach(object sender, AttachEventArgs e)
    {
        Debug.Log ("Display attached.");
        lcdDisplay.open ();
        lcdDisplay.screens[0].Backlight = true;
        lcdDisplay.screens[1].Backlight = true;
        lcdDisplay.screens[0].ScreenSize = TextLCD.ScreenSizes._2x20;
        lcdDisplay.screens[1].ScreenSize = TextLCD.ScreenSizes._2x20;
        lcdDisplay.screens[0].initialize ();
        lcdDisplay.screens[1].initialize ();
        lcdDisplayIsAttached = true;
        WriteMessageToDisplay ("*** INITIALIZING ***", "***   PROGRAM    ***", false);
    }
  
    void lcdControl_Detach(object sender, DetachEventArgs e)
    {
        Debug.Log ("Detached.");
        lcdDisplayIsAttached = false;
    }
  
    void lcdControl_Error(object sender, ErrorEventArgs e)
    {
        Debug.Log ("Phidgets display error: " + e.ToString ());
    }

    public void WriteMessageToDisplay (string topLine, string bottomLine, bool centerText)
    {
        string topPadding = "";
        string bottomPadding = "";

        // Make sure strings are not too long.
        if (topLine.Length > 20)
        {
            topLine = topLine.Remove (19, topLine.Length - 20);
        }

        if (bottomLine.Length > 20)
        {
            bottomLine = bottomLine.Remove (19, bottomLine.Length - 20);
        }

        if (centerText)
        {
            if (topLine.Length <= 20)
            {
                int amountOfPaddingToAdd = (20 - topLine.Length) / 2;
                for (int i = 0; i < amountOfPaddingToAdd; i++)
                {
                    topPadding += " ";
                }
            }

            if (bottomLine.Length <= 20)
            {
                int amountOfPaddingToAdd = (20 - bottomLine.Length) / 2;
                for (int i = 0; i < amountOfPaddingToAdd; i++)
                {
                    bottomPadding += " ";
                }
            }
        }

        string formattedTopLine = topPadding + topLine;
        string formattedBottomLine = bottomPadding + bottomLine;

        if (lcdDisplayIsAttached)
        {
            lcdDisplay.screens[0].Backlight = true;
            lcdDisplay.screens[0].rows[0].DisplayString = formattedTopLine;
            lcdDisplay.screens[0].rows[1].DisplayString = formattedBottomLine;
            lcdDisplay.screens[1].Backlight = true;
            lcdDisplay.screens[1].rows[0].DisplayString = formattedTopLine;
            lcdDisplay.screens[1].rows[1].DisplayString = formattedBottomLine;
        }
    }
  
    void CloseDisplay ()
    {
        lcdDisplay.close ();
        lcdDisplay = null;
    }
  
    void OnApplicationQuit ()
    {
        CloseDisplay ();
    }
}


Let's go through this quickly; if you've followed the Unity Text LCD tutorial there isn't much new here.

We create a new Phidgets TextLCD () device to be able to communicate with the display, and a bool to keep track of when it is attached. We could instead check TextLCD.Attached before we try to communicate with it; this is just another way to go about it.

In Start (), we attach our event handlers and open the device for communication. In our event handlers, we're doing some simple initialization when it's attached (turning on the backlight, clearing the display, etc.), setting the attached status when it's detached, and logging errors as needed. Note that in lcdControl_Attach (), we set the screen size (number of lines by number of columns) -- this is critical. This size must be set before you send any text to the display. We are using a 2x20 display, so you'll need to change this if you have a different size.

WriteMessageToDisplay () is a public method we'll use from other methods to send text to the display. We've allowed for two lines, and a bool to decide if we want to center the text or not. We limit the text size to 20 characters, because if we go over, we'll throw an exception; again, change your text size to accomodate the size you need. If we do want the text centered, we look at the max text length minus the current length, and pad half that length to the beginning of the string. Then, as long as the display is attached, we set our backlight and shoot the text over to the display. Note also that we are talking to both displays on the controller; if you'd like to have identical output on two displays, this is already taken care of.

Finally, CloseDisplay () gets rid of the TextLCD object cleanly, and it gets called when the application is exited. If we don't do this (and, in my experience, even sometimes when we do), the application will hang when you exit.

So now we have a script that is capable of talking to the display. We need a script that can grab this weather data for us. Again, I made an object in my scene (this time called "WeatherController") and attached the following script to it.

using UnityEngine;
using System.Collections;
using System.Net;
using System.Xml;
using System.IO;

public class WeatherController : MonoBehaviour
{
    // Weather Underground API settings
    private const string WEATHER_UNDERGROUND_API_KEY = "abc123";

    public string[] ParseWUndergroundData (string zipCode)
    {
        // Form the proper URL.
        string inputXML = "http://api.wunderground.com/api/" + WEATHER_UNDERGROUND_API_KEY + "/conditions/q/" + zipCode + ".xml";

        string place = "";
        string weather = "";
        string temperature = "";
        string relativeHumidity = "";
        string windDirection = "";
        string windMPH = "";

        bool fullNameRetrieved = false;    // Need to use a flag. There are two "full" XML elements; we just want the first.

        WebClient webClient = new WebClient ();
        string weatherData = "";

        try
        {
            weatherData = webClient.DownloadString (inputXML);
        }
        catch
        {
            Debug.Log ("Could not receive XML document from Weather Underground.");
            return null;
        }

        using (XmlReader reader = XmlReader.Create (new StringReader (weatherData)))
        {
            while (reader.Read ())
            {
                switch (reader.NodeType)
                {
                case XmlNodeType.Element:
                    if (reader.Name.Equals ("full") && !fullNameRetrieved)
                    {
                        reader.Read ();
                        place = reader.Value;
                        fullNameRetrieved = true;
                    }
                    else if (reader.Name.Equals ("weather"))
                    {
                        reader.Read ();
                        weather = reader.Value;
                    }
                    else if (reader.Name.Equals ("temperature_string"))
                    {
                        reader.Read ();
                        temperature = reader.Value;
                    }
                    else if (reader.Name.Equals ("relative_humidity"))
                    {
                        reader.Read ();
                        relativeHumidity = reader.Value;
                    }
                    else if (reader.Name.Equals ("wind_dir"))
                    {
                        reader.Read ();
                        windDirection = reader.Value;
                    }
                    else if (reader.Name.Equals ("wind_mph"))
                    {
                        reader.Read ();
                        windMPH = reader.Value;
                    }

                    break;
                }
            }
        }

        string[] formattedData = { "Weather for " + zipCode, place, weather, temperature, "Rel. humidity: " + relativeHumidity, "Wind " + windDirection + " at " + windMPH + " MPH" };

        return formattedData;
    }
}


We'll go over this in a bit more detail, as we haven't touched the WebClient or XML parsing in any other tutorials.

First, we make a string constant to hold our API key. We're going to use this in the URL we build to process a request from the Weather Underground server.

We've got a string array method ParseWUndergroundData () that takes a ZIP code and returns certain weather data. There are lots of fields that it will return. If you'd like to just poke at the API and see everything you can get back, form a URL like this, substituting your own API key and ZIP code:

http://api.wunderground.com/api/API_KEY_GOES_HERE/conditions/q/60601.xml

If you plug in your own API key there, and pop that URL into a browser, you'll get an XML file with a bunch of weather info. I only parse a few fields for the sake of brevity; you can parse any others you want (I'll detail how below).

So, we form our URL using the API key and supplied ZIP code. We get a few variables ready to hold the data that we want. The fullNameRetrieved is a workaround to make our XML parsing similar, and again, there's a couple of ways we could have done this. The problem is that there are two "full" fields that get sent in the XML document. Rather than parsing multiple nodes, I just flag when the first one (the brief name) has been parsed, ignoring the second one. If you wanted, you could structure your XML parsing to be a bit more detailed on node and element names, but again, I wanted to illustrate with simplicity in mind.

Next, we instantiate a WebClient and a string to hold the XML data. You can check out the WebClient class in more detail here if you're interested, but the bottom line is it gets data from a URL. We put that data in a string -- in this case, it's the XML Weather Underground returned to us -- and now it's ready to parse.

We make a new XMLReader, which is used to read data in XML format. We need to give it XML data, and since we're holding that XML data in a string, we'll have to supply the string to the XMLReader in a StringReader. A StringReader is just like a TextReader, which reads text from a file, except it reads a string. We put this XMLReader in a using statement so that once it has finished processing, it will be taken care of by the garbage collector.

Next, we get the XMLReader reading the file. Any time we run across an XML element, we check the name. If it's the name of an element we want to parse, we assign its value to the appropriate variable and continue reading. You can see the element names that I'm grabbing. If you wanted to grab others, you would simply add a string of your choice, then create another "else if" statement with the element's name, and assign the variable properly. Once the reader has finished processing all elements, it will be disposed of (thanks to our using statement).

Finally, we create a new string array containing the data formatted how we want it. Each string in the array will be on a separate line. I've added a little formatting (such as prepending the ZIP code with "Weather for "). Again, however you want your data to display, go ahead and make changes accordingly. We return this string array and we're done here.

Now, we're going to need something to make requests to the web server and collect weather data, then send that data to the display at appropriate times. This is where my MasterController script comes into play. One final time, create an empty GameObject and assign this script to it.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MasterController : MonoBehaviour
{
    private const float TIME_BETWEEN_UPDATES = 3600f;
    private const float SCROLLING_TIME = 5f;

    private List<string> dataToScrollThrough = new List<string>();

    public WeatherController weatherController;
    public DisplayController displayController;

    private float updateTimer = 0;
    private float scrollingTime = 0;
    private int scrollingIndex = 0;
    private bool applicationPaused = false;

    public string[] weatherZipCodes = { "10001", "60601", "90010" };

    private void UpdateAllData ()
    {
        dataToScrollThrough.Clear ();

        foreach (string currentZipCode in weatherZipCodes)
        {
            foreach (string stringToAdd in weatherController.ParseWUndergroundData (currentZipCode))
            {
                dataToScrollThrough.Add (stringToAdd);
            }
        }
    }

    void Start ()
    {
        UpdateAllData ();
    }

    private void UpdateDataOnDisplay (int indexOfData)
    {
        displayController.WriteMessageToDisplay (dataToScrollThrough[indexOfData], dataToScrollThrough[scrollingIndex + 1], true);
    }

    void Update ()
    {
        if (!applicationPaused)
        {
            updateTimer += Time.deltaTime;

            if (updateTimer >= TIME_BETWEEN_UPDATES)
            {
                UpdateAllData ();
                updateTimer = 0;
            }

            scrollingTime += Time.deltaTime;

            if (scrollingTime >= SCROLLING_TIME)
            {
                scrollingTime = 0;

                if (scrollingIndex > dataToScrollThrough.Count - 1)
                {
                    scrollingIndex = 0;
                }

                UpdateDataOnDisplay (scrollingIndex);

                scrollingIndex += 2;
            }
        }

        if (Input.GetKeyDown (KeyCode.UpArrow))
        {
            scrollingTime = 0;
           
            if (scrollingIndex > dataToScrollThrough.Count - 1)
            {
                scrollingIndex = 0;
            }
           
            UpdateDataOnDisplay (scrollingIndex);
           
            scrollingIndex += 2;
        }

        if (Input.GetKeyDown (KeyCode.Space))
        {
            if (applicationPaused)
            {
                updateTimer = 0;
                scrollingTime = 0;
                applicationPaused = false;
            }
            else
            {
                applicationPaused = true;
            }
        }
    }
}


Personally, I like to create autonomous scripts with easy-to-use interfaces, then create a master object that talks to them. Here, we've got the weather and display controllers, which don't do anything on their own, and the master controller, which tells them what to do. This is nice because it keeps game logic in one place, and lets each script handle only what it needs to handle. There are multiple ways to handle these types of things, but I prefer this for simple programs.

First, we make a couple of const floats that represent times to update the weather and how quickly to scroll through the data on the display. Using these numbers, we're checking weather once per hour, and showing each two lines of weather data every five seconds. We make a generic list of strings to represent the full set of data that will end up getting sent to the displays.

We also make a couple of public declarations for the controllers that we're going to access (the weather and display controllers). We're doing this the simplest possible way here -- we declare an instance of each script as a public variable. In the Editor, we're going to drag the object that has that type of script right on to the appropriate field in the Inspector, so that our Master Controller looks like this:


When we declare a public instance of a script we wrote, we just assign that script to an object, then drag the object right on to our public field. Now, when we reference that script in our code, we are talking directly to the script on that object.

Keep in mind that we could have also looked for the appropriate objects in the scene programmatically (for instance, using GameObject.Find ()), but since these objects are static in the scene, this is easy. If you're going to try to talk to prefabs that you instantiate on-the-fly, you won't be able to do this.

Now, we've got some variables for features that we'll explain later, some timers to control when to get weather data and scrolling speed, and an array of ZIP codes we want to check weather data for.

UpdateAllData () clears the current set of weather data strings and gets new ones. For every ZIP code in weatherZipCodes[], we parse the data from Weather Underground. All of these strings get added to our generic list, which we'll scroll through on the display.

In Start (), we call UpdateAllData () to get the balling rolling.

UpdateDataOnDisplay sends two lines of data to the display from our generic list. It accepts the index of the line we want to start at, and displays that line of text plus the next, since we have a two-line display. You could easily modify this to show more lines, in the case that you have a 4-line display.

Update () is where we're doing the bulk of our processing, as is usually the case. First, we update the timer that determines when it's time to get new weather data, provided that the application isn't paused, which we'll deal with later. If the timer gets above our TIME_BETWEEN_UPDATES float, we'll reset the timer and get our new data.

We now do the same for scrolling text. If our timer gets above SCROLLING_TIME, we reset the timer and increment the line of data we want to display next. If we get to the end of the data, we're going to reset our index to 0. We call UpdateDataOnDisplay () with the index of the data we want, then we increment the index for next time -- remember, we're displaying two lines of data at a time, so we want to increment the index by two. So the first time we display data, we'll get lines 0 and 1, then 2 and 3, and so on, until the end of the list, at which point we'll start back over at 0.

Finally, we've got a couple of keyboard commands for usability's sake. Pressing the up arrow will move to the next two lines of data. Pressing the space bar will pause on the current lines of data, or if the application is already paused, it will un-pause it.

We omitted lots of good programming practices to create the simplest example we could. Here are just a handful of changes that could be made, some of which you'd certainly need to do if you were going to use something like this in a released product:

- Make a GUI to accept/parse ZIP codes, control update/scrolling speeds, etc.
- Add error handling (try/catches around hardware communication would be a good place to start)
- Get rid of all magic numbers, such as the max number of characters in WriteMessageToDisplay, and replace with consts or user-configurable values

If you do want to use this, you could also compile it and run it with the "-batchmode" command line switch. This would open the program without any graphical display, letting it just plug away in the background. One nice thing about that is you won't have a crash due to losing the graphics device if you lock your screen.

2 comments:

  1. Great post! I was looking to do exactly this with Weather Undergound and Unity. There's also another way you could get the info that may be a little more precise, perhaps even simpler and also helps if you wanted to pull multiple fields or fields (i.e sunset/sunrise hour) from the Weather Underground XML. You could use read.ReadtoDescendant, which would look like the following:

    switch (reader.NodeType) {
    case XmlNodeType.Element:
    if (reader.Name.Equals ("display_location")) {
    reader.ReadToDescendant("full");
    reader.Read();
    location = reader.Value;
    }

    Thanks for a great tutorial! Steve

    ReplyDelete
    Replies
    1. Wow, I apologize for the length of time in replying, but thanks Steve!

      Delete