Click here to Skip to main content
Click here to Skip to main content
Go to top

Robot Alarm Clock

, 15 Sep 2013
Rate this:
Please Sign up or sign in to vote.
Bluetooth robotic alarm clock using C#

Introduction

This article combines several useful concepts to make a user interface for a robotic alarm clock. The program can be modified to change the behavior of the robot when the alarm occurs. The article includes a solution to the surprisingly difficult task of extracting device names from Bluetooth devices paired to a PC. The article also shows how to easily use INI files to store settings and preferences. The article presents a robust, flexible communication structure between the robot and the PC. Visual Studio built in tools include ColorDialog, DateTimePicker, Timer, ToolStripMenuItem and StatusStrip.

Prerequisites

This article assumes that you know how to add a Bluetooth device to your PC. A familiarity with bytes and byte arrays is useful but can be learned by studying the code here.

Background

Bluetooth devices can use several different profiles (protocols) to talk to PCs. When you pair a device to the PC, Windows decides which profiles to support, based on information from the Bluetooth device. In this case, the profile used is SPP (Serial Port Profile). This creates an interface that is familiar to anyone who has ever used Visual Studio with a serial COM port. The send and receive details are all taken care of by Visual Studio. All you have to do is read the bytes from the port and then write bytes to the port. The hard part is making sense of those bytes as messages and building up your own messages that the robot will understand. For this article, I am using a robotic ball named Sphero. The communication interface (API) for Sphero is publicly available and well documented.

Save Settings in INI Files

Let's start with the INI file. You can easily store information in a text file. The values can be modified while the program is running and then reloaded the next time the program runs. A user could also modify values in the INI file while the program is not running, in order to change the behavior of the program the next time it runs. All you need to do is include the ini.cs file that is in this program. There is a read function and a save function. Typically, you would read the settings when the program starts and then write the settings when the program ends. In this program, I am saving two values, the com port of the robot and the alarm time. In IniReadValue(), the third parameter is a default value. If the file or setting in the file does not exist, then this value is used. The default values are used the first time you run the program and the INI file is automatically created. Then in the program UI, the user can change the alarm time and the new value gets saved in the file when you call SaveINISettings(), typically when the program closes.

public void ReadINISettings()
{
  commPort.PortName = config.IniReadValue
  ("Port Settings", "PortName", "COM1");
  // Reads last alarm time set
  dateTimePicker1.Value = Convert.ToDateTime(config.IniReadValue
  ("Alarm Settings", "Alarm Time", System.DateTime.Now.ToString("t")));
} 
public void SaveINISettings()
{
  config.IniWriteValue("Port Settings", "PortName", commPort.PortName);
  // Store alarm time setting.
  config.IniWriteValue("Alarm Settings", "Alarm Time", 
  dateTimePicker1.Value.ToString("t"));
}  

Bluetooth Device Names

Next let's look at getting Bluetooth device names. Some people will find this section extremely interesting and useful. But if you just want to get to the fun robot stuff, skip this section.

If all you need is a list of com ports, it's not too hard to get that list ("COM1", "COM2", etc.). But what if you have several Bluetooth devices and you want to show the user the names of the devices. The com port numbers don't really mean anything to the user, but they will know what the device name is. When a Bluetooth device is paired to a PC, an entry is created in the Windows Registry. The entry contains various information including the COM port number, MAC Address, and the device name.

First, the code below uses the Visual Studio functions ManagementObjectCollection and ManagementObjectSearcher to create a list of potential Bluetooth devices.

ManagementObjectCollection ManObjReturn;
ManagementObjectSearcher ManObjSearch;
ManObjSearch = new ManagementObjectSearcher("root\\CIMV2", 
"SELECT * FROM Win32_PnPEntity WHERE ConfigManagerErrorCode = 0");
ManObjReturn = ManObjSearch.Get(); 

Second, for each object in ManObjReturn, we check to see if it is a Bluetooth device. Look for PortType = "BTHENUM". From this, you also get the Bluetooth MAC Address.

foreach (ManagementObject ManObj in ManObjReturn)
{
  if ((PortType != null) && (PortType.Length > 0) && 
  PortType == "BTHENUM")  // BlueTooth?
  { 
    DeviceID = (string)(ManObj["DeviceID"]);
    SPEntry.BluetoothAddress = GetBTAfromDeviceID(DeviceID); // get BlueTooth MAC address 
    BluetoothPortList.Add(SPEntry);
  } 
} 

Third, we take our list of Bluetooth devices and search the registry for more information, based on the MAC address. We are really looking for the Bluetooth port name ("ServiceName").

if ((string)hexString == BluetoothAddress)
{
  object Enabled = Registry.GetValue("HKEY_LOCAL_MACHINE\\" + 
  startKeyPath + "\\" + subkey, "Enabled", null);
  if ((int)Enabled == 1)
  {
    object ServiceName = Registry.GetValue("HKEY_LOCAL_MACHINE\\" + 
    startKeyPath + "\\" + subkey, "ServiceName", null);
    return (string)ServiceName;
  }
} 

Finally, we take our list of Bluetooth devices and add them to the drop down menu. Whew!

foreach (ComPortData cpl in serStat.BluetoothPortList)
  portToolStripMenuItem.DropDownItems.Add(cpl.ComName + " " + 
  cpl.BluetoothName, null, portToolStripMenuItem_Click); 

Alarm Clock Implementation

The implementation of the alarm clock is fairly simple. It gets a little more complicated when we add in different behaviors for the robot, based on how long the alarm has been occurring. We start out gentle, by just making the LEDs blink for a while. Then we create a little noise by doing a slow rotation. If you are still not awake, the motion increases until the ball is jumping and rolling around and hopefully making a lot of noise and, at the very least, scaring the cat.

The dateTimePicker tool provided by Visual Studio makes it very easy to create an interface for the user to select a time. All we have to do is add the dateTimePicker to the form and then set the format we want.

dateTimePicker1.CustomFormat = "h:mm tt";
dateTimePicker1.Format = DateTimePickerFormat.Custom; 

The current time is also displayed on the form in a simple text box, using the code below. This code is in the event for a Visual Studio timer, set to run once per second. The timer interval also controls how fast the robot behavior changes. At one second intervals, the behavior changes pretty fast. This lets you quickly see all the behaviors while testing the program. For the real alarm clock, the interval should be 10 or 20 seconds to give you more time to wake up in the gentle cycles.

tBxCurrentTime.Text = System.DateTime.Now.ToString("t"); 

If we are connected to the robot, then the code in the timer event will also check on the alarm status and see if it is time to start an alarm or time to change the way Sphero is behaving. Comparing the current time to the alarm time is easy.

if (dateTimePicker1.Value.ToString("t") == System.DateTime.Now.ToString("t")) 
  alarmState = 1; 

Then while alarmState = 1, each time our timer runs its event, we'll check to see if it is time to make the alarm more alarming.

Controlling the Robot

Now for the fun stuff! Sphero gives us LEDs and motors to play with by talking to it through Bluetooth with API commands. A command is made up of a series of bytes. Every command has the same structure. The first two bytes are a header of 0xFF 0xFF. The third and fourth bytes are a two byte command. The fifth byte is a sequence number which gets sent back from the robot with a response message (like an ACK message). The sixth byte is a data length which specifies how many bytes of data are in this message (including the checksum byte). Then the last byte is a checksum byte which is the checksum of all the bytes, except for the two byte header.

First let's look at a simple command. The Ping command is a command that doesn't really do anything. You can use it to check if the connection is working. I'm using it here to keep Sphero from going to sleep. Normally Sphero will go to sleep after 10 minutes of nothing happening. But in this case, I want Sphero to stay awake and connected so that I can control it when the alarm goes off. So each time the timer event occurs, I send a ping message, which keeps Sphero awake. A Ping message looks like:

0xFF 0xFF 0x00 0x01 0x17 0x01 0xE7

where 0x00 0x01 is the two byte command for Ping, 0x17 is the sequence number, 0x01 is the data length (which is one byte for the checksum), and 0xE7 is the checksum.

Sphero sends a response back in the form of 0xFF 0xFF 0x00 0x17 0x01 0xE7 where 0x00 is the response code (0 means no errors), 0x17 is the sequence number it received in the Ping message, 0x01 is the data length, and 0xE7 is the checksum. We could just create the byte array with those Ping message bytes above and always send that as our Ping message, but I wanted to create a more flexible system. First, I made a generic CommandPacket class that can be used with many different commands. Some common default values are assigned in the constructor. A checksum function is provided, as well as a function to convert the structure into a byte array, CreateDataStream().

public class CommandPacket
{
  [StructLayout(LayoutKind.Sequential, Pack = 1)]
  public class CommandPacketData
  {
    public byte SOP1;
    public byte SOP2;
    public byte DID;
    public byte CID;
    public byte Seq;  // echoed by client in response packet
    public byte Dlen;
    public byte Chk;
  }
  public CommandPacketData pkt;
  public CommandPacket()
  {
    pkt = new CommandPacketData();
    pkt.SOP1 = 0xff;
    pkt.SOP2 = 0xff;
    pkt.Dlen = 0x01; // always 1 because of the Chksum field
  }
  public byte GetChecksum()
  {
    return (byte)(~((pkt.DID + pkt.CID + pkt.Seq + pkt.Dlen) % 256));
  }
  public byte[] CreateDataStream()
  {
    return Form1.StructureToByteArray(pkt);
  }
}

Then, to construct a Ping message, I created a Ping function. The values specific to the Ping command are added into the CommandPacket. squenceNum is a global variable used by all commands so that each message gets a unique sequence number (until it wraps around at 255, and then starts over at 0). GetChecksum() is used to calculate the checksum and CreateDataStream() is used to construct a byte array that can be sent to the serial port. If the serial port write fails, I assume the robot is not connected and update the UI to show a disconnected state. If the write succeeds, then I look for a response. The response function, AnalyzeResult(), has a timeout parameter in milliseconds (300 here) and a message type. Further, debugging can be done by putting in Visual Studio "MessageBox.Show" commands to give more information for the failure cases. Here I am just returning without doing anything extra even if Sphero sends back an error code or the checksum in the response message is wrong.

private void Ping(SerialPort sp)
{
  ResponseResults result;
  CommandPacket packet = new CommandPacket();
  packet.pkt.DID = (byte)0x00;
  packet.pkt.CID = (byte)0x01;
  packet.pkt.Seq = (byte)sequenceNum++;	// increment the sequence number for the next packet
  packet.pkt.Chk = packet.GetChecksum();	// calculate the checksum
  if (SerialPortWrite(sp, packet.CreateDataStream(), packet.pkt.Seq, "Ping") == false)
  {
    UpdateCommPortState(false);	// No response so Sphero must not be connected
    return;
  }
  // block here until a response
  result = AnalyzeResult(300, "Ping");
  if (result != ResponseResults.GOOD)
    return;
  if (response.pkt.ChksumError) // Look for checksum error
    return;
} 

If you understand the process of creating the Ping message, then the other commands are easy. The command for setting the LED color is similar to the Ping message but it has 4 data bytes (one for red, one for green, one for blue, and one to say whether you want the color to be temporary or come back the next time you use Sphero). I made a different packet class just for the set LED command to handle the additional data bytes. Then to create the message, there is a function just like the Ping command that looks like:

private void SetRGBLED(SerialPort sp, byte Red, byte Green, byte Blue, byte flag) 

It is the same as the Ping function above except that the color values get stuffed into their specific bytes in the packet. In the user interface, I use the ColorDialog tool to let the user pick what color they want to use as a night light (black is an option if they don't want any light to disturb the long cold night).

Next is the raw motor command. It is about the same as the other two. There is L and R for left and right motors. For the mode, 0 = stop, 1 = forward, and 2 = backwards. Power can be 0 to 255.

private void SetRawMotors(SerialPort sp, byte LMode, byte LPower, byte Rmode, byte RPower) 

The last commands I'm using are the get and set option flag commands. Normally when you put Sphero in the charger, it goes to sleep. In this case, I want Sphero to stay awake so it can function as a night light and be awake to do the alarm when it is time for the alarm. In order to achieve this, the "Stay Awake in Charger" option bit needs to be set. But first, I get the existing bits from Sphero so that I don't mess up whatever is already there. Then I just set the awake bit without disturbing the other bits. I do this every time the alarm is enabled. Every time the alarm is disabled, I do the same thing, but turn off the awake bit so that Sphero will go to sleep in the charger.

The End

That's about it. Let me know if you have any ideas for improvement!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jim Atwell

Unknown
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 PinmemberAnthony Daly15-Sep-13 0:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140926.1 | Last Updated 15 Sep 2013
Article Copyright 2013 by Jim Atwell
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid