8. Serial I/O- Part 1

8. Serial I/O- Part 1
Photo by Vishnu Mohanan / Unsplash

Let's continue thinking about building our Altimeter device. Now that we have the altimeter and barometer data from the sim, lets see what we need to be able to write a sketch to send the two SimVars to an Arduino and display them somehow.

The obvious questions

So... how do we send data from Windows to an Arduino via USB? But more important, how do we know out of all the USB devices connected to the PC, which one's our Arduino? It's pretty simple and I already talked about in my post about Arduino Joystick Emulation. We can use the device's PID/VID to identify them.

What is a PID/VID?

The Product ID (or PID) and the Vendor ID (or VID) are unique identifiers used in USB devices to identify the manufacturer and their specific product. For Arduino devices, the VID is 2341, and the PID depends on the Arduino board. For example:

Product PID VID
Arduino UNO 0043 2341
Arduino Leonardo 8036 2341
Arduino Micro 8037 2341
TEENSY 0483 16C0
Note: these are the PID/VID values defined in C:\Program Files (x86)\Arduino\hardware\arduino\avr\boards.txt

How do we query USB devices in .NET?

Unfortunately, there is no "native" .NET way of querying that data for USB devices so you have to use some other API like a Windows Management Instrumentation (or WMI) query and parse the results. Here is an example code snippet with the general idea of how to do it:

using System.Management;

var searcher = new ManagementObjectSearcher("select DeviceID, PNPDeviceID, MaxBaudRate from win32_serialport");

var devices = searcher.Get().Cast<ManagementBaseObject>().ToList();

// print all devices
foreach (var d in devices)
{
  // this contains the COM port
  var deviceId = (string)d["DeviceID"];

  // this contains the PID?VID
  var pnpDeviceId = (string)d["PNPDeviceID"];

  // this contains the Max Baud Rate you can use to talk to the device
  var maxBaudRate = (uint)d["MaxBaudRate"];

  Debug.WriteLine($"DeviceID: {deviceId}, PNPDeviceId: {pnpDeviceId}, MaxBaudRate: {maxBaudRate}");
}

// you can write a lambda to filter by your supported devices based on the PNPDeviceID

Once we have built a list of connected devices that we can talk to, we can now write some C# code to open a Serial connection to a device and send our data.

About Serial Ports

A serial port is a communication interface where information is transmitted sequentially one bit at a time. This is the interface used historically for communications between computers and peripherals like modems, keyboards, mice, terminals, back in the day before high speed USB, Ethernet, etc. Devices that support serial port communications are typically associated with port name and their speed, plus other data integrity validation parameters like data/stop bits and parity.

Although all these parameters must match on both sides to be able to communicate properly, we are going to focus on two things, the port name and the speed. When connecting to an Arduino, these typically look like "COM3", "COM5" for the port name, and values like 115200, 9600, 38400 for the speed.

Sending data from C# using SerialPort

We can use the SerialPort class to send and receive data to/from an Arduino. We'll need two things, it's COM port and the speed. To find the COM port name manually, we can go to Device Manager and find our Arduino device and see what port it is using; alternatively we can open the Arduino IDE and find it there.

Let's look at some code:

// initialize the SerialObject, assuming our Arduino is on COM3 and we can send data at 115200. If not, try a lower setting like 9600 which is commonly seen in older Arduino example Sketches
var serialPort = new SerialPort("COM3", 115200);

// try to open the port
try
{
  serialPort.Open();

  // send some data
  serialPort.WriteLine("Hello Serial World!");
}
catch (Exception e)
{
  Debug.WriteLine(e.Message);
}
finally
{
  // close the serial port
  if (serialPort.IsOpen)
  {
    serialPort.Close();
  }
}

Receiving data in C# using SerialPort

For receiving data, we need to define a DataReceived event handler which is triggered whenever data comes to the serial port. Example:

// initialize the SerialObject, assuming our Arduino is on COM3 and we can send data at 115200. If not, try a lower setting like 9600 which is commonly seen in older Arduino example Sketches
var serialPort = new SerialPort("COM3", 115200);

// attach an event handler
serialPort.DataReceived += SerialPort_DataReceived;

// try to open the port
try
{
  serialPort.Open();

  while(true)
  {
    Thread.Sleep(1000);
  }
}
catch (Exception e)
{
  Debug.WriteLine(e.Message);
}
finally
{
  // close the serial port
  if (serialPort.IsOpen)
  {
    serialPort.Close();
  }
}

private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
  try
  {
    // Read data from the serial port
    var dataReceived = serialPort.ReadLine(); // Reads until a newline character is found
    // Alternatively, use serialPort.ReadExisting() to read all available data as a string
            
    Debug.WriteLine(dataReceived);
  }
  catch (Exception ex)
  {
    Debug.WriteLine("Error while receiving data: " + ex.Message);
  }
}

Sending and Receiving data from Arduino

Arduino has a Serial class used serial port communications which is pretty straightforward to use. All Arduinos have at least one Serial port, but some can have Serial2 and Serial3 for additional I/O. Example:

void setup() {
  // initialize the port and set the baud rate to 115200, change to 9600 if you are having communication issues, but make sure it's the same rate as in the C# code
  Serial.begin(115200);
}

void loop() {
  if (Serial.available() > 0) {
    // read all data until end of line
    String data = Serial.readStringUntil('\n');

    Serial.print("Received: ");
    Serial.println(data);
  }
}

Conclusion

.NET and Arduino both provide simple classes to communicate via serial port. In the examples above, we are sending and receiving simple string messages as a proof of concept, but we can easily expand these to send other data types. The catch is that we have to send and receive them in bytes and convert them to the corresponding types after receiving them.

Alternatively, there are existing standardized protocols that are supported in Arduino that we could use to implement more robust communications instead of simple raw byte communications, but with the added overhead that could impact performance, but it's worth looking into in the future.