Sunday, October 26, 2014

Rewriting a Legacy App - Part 3: Accepting Commands

So, I'm continuing to work on my house automation software to replace my legacy system. In Part 1, we looked at existing functionality and figured out our minimum viable product (MVP). In Part 2, we got the basics of the hardware interaction working. Now we'll expand things to work with various devices and commands.

But before we move on, let's review the requirements:

Needed for MVP:
  1. Send commands through the serial dongle
    This is the purpose of the system.
  2. Send commands for 8 devices on House Code "A"
    These are the only devices that are actively used.
  3. Fire on/off/dim events on a schedule
    To maintain the air conditioner functionality (and current lighting schedule).
We've already got #1 in place, so let's expand things to #2 -- being able to work with all 8 devices on the system.

[Update: Code download and links to the entire series of articles: Rewriting a Legacy App]

Separation of Concerns
Now, I realize that the typical way of implementing a minimum viable product is to not really worry about the code organization (just get it working). I have trouble working that way. I like to clean code up as I go along.

When moving on to the next step of accepting commands for various devices, I really wanted to separate the concerns. One thing I didn't like in the test code was the mixing of building the message (specific to the X10 system) with sending the message (interacting with the serial port). So I created a new library and a couple of classes to handle this:

The first file is "DeviceCommands.cs". This simply has an enumeration to hold the "On" and "Off" values (and in the future, it will handle other commands as well).

The MessageGenerator is responsible for building the message that we'll send through the serial port, and the SerialCommander is responsible for sending those commands. Let's look at the SerialCommander first.

No Sleeping Allowed
When we were going through the test code in the previous article, it was littered with Thread.Sleep commands. These are generally not good because they block the current thread. But there's some good news for our code.

I did some additional tests and found that the Thread.Sleep commands were not necessary. When I removed them, the hardware interaction continued to work just fine. As mentioned, I brought these over because they were part of the legacy implementation. But they may have been needed on earlier systems just for timing issues. Perhaps (just a guess) modern computers run fast enough that they no longer run into issues here.

Creating and initializing the serial port is very similar to what we had in our test code:

The only difference here is that I added a configuration setting for the COM port. This is the one part of configuration that may need to be changed depending on the physical computer. The code also defaults to "COM3" (in case there is no configuration setting). This probably isn't a good way of handling a default, but we can loop back around to this later.

The method to send the command has a new parameter, but the bulk of the code is unchanged (except for the removal of the Sleep commands):

Just like before, it opens the serial port and initializes the DTR/RTS control signals. Then it loops through the message and sends the individual bits. And finally, it closes the serial port.

The biggest difference is that we're sending the message in as a parameter. We have to build this message somewhere. And that's the responsibility of the MessageGenerator.

Generating the Message
In order to fulfill requirement #2 for our MVP, we need to be able to interact with devices 1 through 8 on the "A" house code. To facilitate this, I created some static variables that we can work with.

I decided to go ahead and stick with strings for these values. I could have made them byte arrays and done shift left/shift right to process them, but the strings aren't causing any problems at this point, so we'll stick with them for now.

This code has the "header" and "footer" values that we saw earlier. In addition, I noticed that the first byte of the message (the middle 2 bytes) was the same for all of house code "A". So, I created a variable just for that. This means that our changing command portion is only 1 byte long.

To make things a bit easier, I created 2 lists: one for the "on" commands, and one for the "off" commands (I don't show the entire entry for the "offCommands" list, but you get the idea). This will give us a list that we can index into in order to find the value that we want. For example, if we want the "on" command for device #2, we can look at "onCommands[2]". I added the "dummy" values at the top of the lists since the indexers are 0-based. That way, we don't need to subtract 1 to get the right value.

With these static variables in place, we can now create a method that builds our message:

Notice that the "GetMessage" method takes 2 parameters: an integer for the device number and a DeviceCommands for the command. I spent a bit of time working on the parameters and the layout of the static variables so that this method would be fairly easy. Some of my other ideas included nested switch statements -- that seemed overly complex.

I start by validating that the device number is in a valid range. Since we're only supporting devices 1 though 8, this is important to validate.

Next, we start creating the message with the "header" and "codeA" parts.

Then we switch based on the command. If the command is "on", we look in the "onCommands" list from above; otherwise, we look in the "offCommands". As mentioned earlier, we can index into these lists based on the device number, and we'll get the right command string.

Finally, we add the "footer" to the message and return it.

Running the New Methods
Now that we have these classes in place, we can use them to send commands to any of our connected devices. Here's our updated console application (from Program.cs):

This uses our new classes to turn on device #5 and then turn off device #5. I was using this device since it was a lamp in the room that I was working in. This let me verify visually that the commands were working -- although I think my cats were getting kind of annoyed that the light kept going on and off.

And, of course, we can change the parameters of the "GetMessage" method to interact with any of the devices on the system.

Closer to MVP
Let's review our requirements again:

Needed for MVP:
  1. Send commands through the serial dongle
    This is the purpose of the system.
  2. Send commands for 8 devices on House Code "A"
    These are the only devices that are actively used.
  3. Fire on/off/dim events on a schedule
    To maintain the air conditioner functionality (and current lighting schedule).
By creating a parameterized method (along with the static variables for the various commands), we've fulfilled requirement #2. We can now interact with the 8 devices that are on house code "A".

I'm not completely happy with this implementation. In the console application, we need to create both the SerialCommander and the MessageGenerator objects. I don't want my calling application to have to know so much about the details. So at some point, I'll wrap these up into a class that is easier to work with.

What's left is to be able to process a schedule (requirement #3). This will be a little more involved. We'll need some sort of class that wakes up at particular times to fire events, and we'll need some way to persist the schedule to disk. We'll be looking at these in future articles.

So we can see that we've made quite a bit of progress toward our minimum viable product. And I like the direction this is going in so far. By eliminating the features that we don't really need, we've managed to come up with a system that is pretty quick to implement.

Stay tuned for more articles on this. The scheduling portion should be interesting (but I may swing off into some other articles in the meantime).

Happy Coding!

No comments:

Post a Comment