Content
Introduction
The Arduino boards provide an easy-to-use environment to control electrical components such as LEDs and motors. I have done this project with an Arduino Mega 2560 microcontroller board, but you can use nearly any official Arduino board.
To download and execute the source files attached to this article, you will need to have the following tools installed:
- An Arduino board, connected to your computer by USB
- The official Arduino IDE (download from here)
- The Arduino board driver must be installed as described here
- Microsoft Visual Studio Express 2010, 2012 or 2013
Background
I completely rewrote this article in fall 2013 and added several new examples, a better explanation and more background information.
What is a Serial Interface?
A serial interface is used for information exchange between computers and peripheral devices. When using a serial communication, the information is sent bit by bit (serial) over a cable. Modern serial interfaces are Ethernet, Firewire, USB, CAN-Bus or RS-485 even though they are not always called "a serial interface". The principle stays the same: Information is bit-by-bit transformed over short distances. Glenn Patton published a great article about the design patterns used by serial communication Ports - It is available on CodeProject.
About the Board
I use an Arduino Mega 2560 Board to test and execute my code, even though the code shall work with most of the available Arduino devices - The code is written using the Official Arduino IDE, which can be downloaded from the Arduino Project page for free, as mentioned at the start of the article.
Working Theory
To explain how a C# program and an Arduino Board may communicate, we are going to assume that the Arduino Board has a temperature sensor, collecting the environment temperatures three times a day: Once in the morning, once at noon and once in the evening. All these values are stored to the EEPROM on the Arduino board. The data is stored in the following format:
11 06 2013 06:00=6.6
11 06 2013 12:00=11.78
11 06 2013 23:00=8.9
11 07 2013 06:00=4.54
11 07 2013 12:00=15.3
11 07 2013 23:00=8.6
11 21 2013 06:00=-2.33
The raw data is formatted as [DATE TIME]=[temperature], and the temperature value is intended to be measured and stored in Celsius (C). The single sets of temperature data are separated by line breaks.
Now, in order to focus on the serial communication, I decided to leave out the EEPROM reading/writing and just define a set of dummy values which are sent whenever a request for the temperature data is coming to the Arduino board. I did a quick example of how a single set of data will look in the Arduino C++ code (I use Arduino's <a href="http://arduino.cc/de/Reference/StringObject" title="Arduino Reference - The String class">String</a>
class):
String SingleSetOfData = "11 21 2013 06:00=-2.33"
Given the fact that a date-time (for example "11 21 2013 06:00", which stands for November 21st 2013 at 06:00 AM) can be at most 15 characters long, the temperature will cover everything from -275.00 to 1999.999 degrees and the separator ('=') is another character we come to a total length of 23 characters the singleSetOfData
needs to take - Why is it in fact 24 characters long then?
Easy question, easy answer: The compiler adds a terminating character '\0' at the end which's space used need to be taken care of, too - 23 + 1 = 24
.
I will not go into detail about the thermometer implementation since there are plenty of instruction sets on how to build one available on the internet - We are going to send dummy values anyways
However, the Arduino board may receive a serial command telling it to read the collected temperature data. I assigned every command a single command code, and defined it to be terminated by a '#' character. So for example, the command to tell the Arduino board has to send the stored temperature data would come like this over the serial interface of the Arduino board, issued by the .NET application which is going to display the temperature data:
1#
Simples. That's it. After the Arduino Board receives this command, it is going to send our previously defined dummy data up to the .NET application, using the serial port, too (Of course, this requires the .NET application to stop sending any command down to the Arduino board):
[STX]
11 06 2013 12:00=11.78$
11 06 2013 23:00=8.9$
11 07 2013 06:00=4.54$
11 07 2013 12:00=15.3$
11 07 2013 23:00=8.6$
11 21 2013 06:00=-2.33[ETX]
Every transmission starts with the code [STX]
which is the official US ASCII code for the start of a transmission text (Code "2"). The transmission ends after sending the code [ETX]
which is the official US ASCII code for the end of a transmission text (Code "3"). The single datasets are separated by a dollar ($) sign.
Using the Code
The code presented in this chapter was completely rewritten during fall 2013 to provide a more valuable learning resource, and match a less theoretical working theory.
The Arduino Source Code
Error, Warning and Status codes
I use various Error, Warning and Status codes within the Arduino source code to indicate whether method calls were successful or ended up causing a warning or an error.
#define MSG_METHOD_SUCCESS 0 //Code which is used when an operation terminated successfully
#define WRG_NO_SERIAL_DATA_AVAILABLE 250 //Code indicates that no new data is available at the serial input buffer
#define ERR_SERIAL_IN_COMMAND_NOT_TERMINATED -1 //Code is used when a serial input commands' last char is not a '#'
Read a command from the Serial Input
I created a method which reads a command from the Arduino Board's serial input buffer and writes it into the pointer which points to an Arduino-String
- returned is an integer value indicating an operation success or an operation fail / warning. It returns either one of the three following codes:
MSG_METHOD_SUCCESS
- This code is returned if everything went fine and the command was written into the
command
pointer.
WRG_NO_SERIAL_DATA_AVAILABLE
- This code is returned if the serial input buffer doesn't contain any data at the time the method is executed.
ERR_SERIAL_IN_COMMAND_NOT_TERMINATED
- This code is returned if there is no data available in the serial input buffer before the end of a command was reached (which means it was not terminated by the '#' character)
Actually, this is the only method I left untouched in comparison to the article and source code in previous versions before I did the complete rewrite.
The code of this method is built up as follow:
The default return code operationStatus
is MSG_METHOD_SUCCESS
. If no data is available in the serial input buffer (checked by calling Serial.Available()
which returns true
if there is data in the serial input buffer), the said operationStatus
has the code WRG_NO_SERIAL_DATA_AVAILABLE
assigned.
int operationStatus = MSG_METHOD_SUCCESS;if (Serial.available()) {
}
else{ operationStatus = WRG_NO_SERIAL_DATA_AVAILABLE;
}
In the likely case that there is data in the serial input buffer, the program executes the following code block:
char serialInByte;
do{ serialInByte = Serial.read();
*command = *command + serialInByte;}while(serialInByte != '#' && Serial.available());
if(serialInByte != '#') {
operationStatus = ERR_SERIAL_IN_COMMAND_NOT_TERMINATED;
}
What now happens is not a big deal and easy to explain. I declared the variable serialInByte
in order to store a single byte which was read by the Serial.Read()
method (which reads exactly one byte from the serial input buffer). I call the said Serial.Read()
method, store the byte returned into serialInByte
and append serialInByte
to the command
pointer - This goes on and on until no data is available in the serial input buffer, or the character "#" is showing up, indicating the end of a command.
The last if
after the do
-while
-Loop checks whether the last read character was a "#" and sets the operationStatus
to ERR_SERIAL_IN_COMMAND_NOT_TERMINATED
if it wasn't because this would mean that a transmission error occurred.
Answer a Serial Command
As you may know, the Arduino source code has a loop()
method which is executing the source code in a forever-loop. I placed the code to handle a command directly in this method:
String command = ""; int serialResult = 0;
serialResult = readSerialInputCommand(&command);
As you can see, I call readSerialInputCommand
to read the command string from the serial input buffer. The return code of the method call is stored in the integer variable serialResult
, which is afterwards used to detect wether there was a command available to read:
if(serialResult == MSG_METHOD_SUCCESS){
if(command == "1#"){ WriteDummyWeatherData();
}
}
To keep this example easy and focused on serial communication, I left out any data storing/loading routines and just write dummy data back to the serial output by calling WriteDummyWeatherData()
:
void WriteDummyWeatherData(){
Serial.print(STX);
Serial.print("11 06 2013 12:00=11.78");
Serial.print(RS);
Serial.print("11 06 2013 23:00=8.9");
Serial.print(RS);
Serial.print("11 07 2013 06:00=4.54");
Serial.print(RS);
Serial.print("11 07 2013 12:00=15.3");
Serial.print(RS);
Serial.print("11 07 2013 23:00=10.3");
Serial.print(RS);
Serial.print("11 21 2013 06:00=-2.33");
Serial.print(ETX);
}
Sure that this method call would need to be replaced in case you really want to send actual weather / any other data to the serial output, but it's fair enough for demonstration purposes.
The .NET Source Code
I implemented a small demo WPF application which shows you how serial data can be handled using the .NET Framework.
The WeatherDataContainer class
I have two classes which help me to organize the communication with the Arduino Board: WeatherDataItem
and WeatherDataContainer
. While the WeatherDataItem
is a sole data-holding class (consisting of a property for the date and one for the temperature) WeatherDataContainer
is (amongst a List of WeatherDataItem
s) encapsulating the communication with the Arduino Board, too.
Since the GUI is only calling methods which can be found in the WeatherDataContainer
class, I'll focus on it rather than unrolling the boring GUI code.
The WeatherDataContainer
has two field variables:
SerialPort arduinoBoard = new SerialPort();
List<WeatherDataItem> weatherDataItems = new List<WeatherDataItem>();
It is important for you to keep these variables and the event in mind, since they'll be mentioned throughout the following code snippets.
Connect to an Arduino Board
The Serial communication capabilities for .NET are provided by the SerialPort
class from the System.IO.Ports
namespace. Before you can send or receive data with a SerialPort
instance, you need to open the Port:
public void OpenArduinoConnection()
{
if(!arduinoBoard.IsOpen)
{
arduinoBoard.DataReceived += arduinoBoard_DataReceived;
arduinoBoard.PortName = ConfigurationSettings.AppSettings["ArduinoPort"];
arduinoBoard.Open();
}
else
{
throw new InvalidOperationException("The Serial Port is already open!");
}
}
As you can see, we add a new Event handler which handles the DataReceived
event of the Serial port - This event is fired as soon as there is data available in the input buffer of the Serial Port.
Getting weather data
You can easily send a command to the Arduino board (I encapsulated the command to make the code better readable):
public void GetWeatherDataFromArduinoBoard()
{
if (arduinoBoard.IsOpen)
{
arduinoBoard.Write("1#");
}
else
{
throw new InvalidOperationException("Can't get weather data if the serial Port is closed!");
}
}
After this command is received by the Arduino Board, it will send back the weather data and trigger the DataReceived
event of the Serial Port.
Receive and parse serial data
As soon as there is data available at the input buffer of the Serial Port, it will fire the DataReceived
Event which is handled by the following event handler in the WeatherDataContainer
class:
void arduinoBoard_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string data = arduinoBoard.ReadTo("\x03");
string[] dataArray = data.Split(new string[]
{"\x02", "$" }, StringSplitOptions.RemoveEmptyEntries);
foreach (string dataItem in dataArray.ToList())
{
WeatherDataItem weatherDataItem = new WeatherDataItem();
weatherDataItem.FromString(dataItem);
weatherDataItems.Add(weatherDataItem);
}
if(NewWeatherDataReceived != null)
{
NewWeatherDataReceived(this, new EventArgs());
}
}
Closing a SerialPort
It is highly recommended to close a SerialPort
after it isn't in use anymore. This happens by calling the Close()
method on a SerialPort
object:
arduinoBoard.Close();
The GUI
I chose two ways of displaying the weather data: As DataGrid
, and as 3D Cylinder Chart. Either one of them has its advantages, even though I prefer the Chart since it looks a bit fancier.
Displaying the Data in a Data Grid
Displaying the weather data in a data grid has an advantage if you want to display a lot of data in a reasonable way - If you only have ten or less data items displaying the weather data as a Cylinder graph is rather reasonable.
The XAML code of the above grid looks somewhat like that:
<DataGrid Name="weatherDataGrid" AutoGenerateColumns="False" Grid.Column="1">
<DataGrid.Columns>
<DataGridTextColumn Header="Date"
Width="1*" Binding="{Binding Date}">
</DataGridTextColumn>
<DataGridTextColumn Header="temperature (Celsius)"
Width="1*" Binding="{Binding temperatureCelsius}">
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
The Grid has two columns from which one is bound to the Date
Property and one is bound to the temperatureCelsius
Property of the WeatherDataItem
class.
The Grid is filled as soon as the Event handler for the WeatherDataContainer
's NewWeatherDataReceived
is triggered, and is executed with Thread safety provided by the dispatcher:
void weatherData_NewWeatherDataReceived(object sender, EventArgs e)
{
Dispatcher.BeginInvoke(new ThreadStart(() =>
weatherDataGrid.ItemsSource = weatherData.WeatherDataItems));
Displaying the data in a 3D Chart
The 3D Chart is placed on a different tab on the application window and directly drawn on a WPF Canvas
by the DrawChart
method:
private void DrawChart()
{
List<double> temperatures = new List<double>();
List<string> captions = new List<string>();
foreach (WeatherDataItem item in weatherData.WeatherDataItems)
{
temperatures.Add(item.temperatureCelsius);
captions.Add(string.Format("{0}\n{1} C", item.Date.ToShortDateString(),
item.temperatureCelsius.ToString()));
}
ChartDataContainer container = new ChartDataContainer();
container.Data = temperatures;
container.DataCaptions = captions;
container.MaxSize = new Point(800, 300);
container.XAxisText = "Date / temperature (Celsius)";
container.YAxisText = "";
container.Offset = new Point(20, 20);
container.ChartElementColor = Colors.BurlyWood;
CylinderChartPainter.DrawChart(container, weatherChartCanvas);
}
The Chart is drawn as soon as the Event handler for the WeatherDataContainer
's NewWeatherDataReceived
is triggered, and is executed with Thread safety provided by the dispatcher:
void weatherData_NewWeatherDataReceived(object sender, EventArgs e)
{
Dispatcher.BeginInvoke(new ThreadStart(DrawChart));
The XAML for the Canvas
looks pretty ordinary, it is basically just a canvas filling a TabItem
:
<TabItem Name="weatherChartTab" Header="3D Chart">
<TabItem>
<Canvas Name="weatherChartCanvas">
</Canvas>
</TabItem>
</TabControl>
In the End, a Chart looking somewhat like the Chart shown in the picture below is drawn onto the Canvas
:
Points of Interest
Arduino provides a huge amount of possibilities on working with hardware and connection to a computer. Last but not least, at the official Arduino homepage is a large community support available. It is very interesting which possibilities are available when you use a serial interface: You can send a pretty huge amount of data and it is not even really complicated.
The best part (for me) was that I was able to do a basic bit of WPF. I played around with various methods of databinding and invoking before I got the .NET app to run.
History
- 10-October-2012: Created the article
- 10-October-2012: Updated
- 12-October-2012: Added the functionality to read different commands
- 11-September-2013: Changed License to LGPL 3
- 23-November-2013: Complete rewrite - New, structured approach to solve an example problem
- 26-November-2013: Updated to use the 3D Cylinder Chart to display weather data