9. Serial I/O - Part 2
Expanding the code we used to send data from C# to Arduino in Part 1 of this series, let's define a SerialPort
object and initialize it.
public partial class MainWindow : Window
{
...
private SerialPort serialPort;
...
private void InitializeSerialPort()
{
serialPort = new SerialPort("COM8", 115200);
}
}
Note: My Arduino is on COM8, so update the port to match yours.
Next, let's try to open and close the serial port connection when we press on the Connect/Disconnect buttons:
// Connect button event handler
private void connectButton_Click(object sender, RoutedEventArgs e)
{
if (Process.GetProcessesByName(MSFS_PROCESS_NAME).Any())
{
try
{
...
// open the COM port
InitializeSerialPort();
// start out DispatcherTimer to start polling from the game
simConnectDispatcherTimer.Start();
...
}
catch (Exception ex)
{
...
}
}
}
// Disconnect button event handler
private void disconnectButton_Click(object sender, RoutedEventArgs e)
{
...
// if port is open, close it
if (serialPort.IsOpen)
{
serialPort.Close();
}
}
Finally, let's expand the Simconnect_OnRecvSimobjectData
handler to send data:
// called whenever we receive a new set of SimVar data
private void Simconnect_OnRecvSimobjectData(SimConnect sender, SIMCONNECT_RECV_SIMOBJECT_DATA data)
{
switch ((RequestType)data.dwRequestID)
{
case RequestType.PerFrameData:
simvars = (SimVars)data.dwData[0];
// send data to serial port if it's open
if (serialPort.IsOpen)
{
try
{
serialPort.WriteLine($"{simvars.Altitude},{simvars.KohlsmanSettingHg}");
}
catch (Exception ex)
{
Debug.WriteLine($"Exception sending data to Arduino: {ex.Message}");
}
}
altimeterTextBox.Text = $"{simvars.Altitude:0.00}";
barometerTextBox.Text = $"{simvars.KohlsmanSettingHg:0.00}";
break;
default:
Debug.WriteLine($"Unsupported Request Type: {data.dwRequestID}");
break;
}
}
We can use the same Arduino Sketch we used before:
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');
// send the data back through the serial connection
Serial.println(data);
}
}
Arduino Serial Monitor
If we compile and upload this Sketch to our Arduino before running the Hello MSFS
app, we can open the Tools–>Serial Monitor window in the Arduino IDE, set the BAUD to 115200, and type anything and hit enter. The sketch should echo what we just we wrote back to us. Now, make sure MSFS is running and run the Hello MSFS
app and click on connect. As soon as it connects to the sim and tries to send data to the Arduino, we'll see the following exception:
22:36:04:570 Exception thrown: 'System.UnauthorizedAccessException' in System.IO.Ports.dll
22:36:04:570 Access to the path 'COM8' is denied.
So what happened? Since we still have the Arduino IDE Serial Monitor open, it already has COM8 opened and Windows won't let us open it a second time, so let's close the Serial Monitor window and try one more time. Click Disconnect and then Connect and now we don't get the exception but we don't have a way of knowing what was actually received on the Arduino. We are sending data back from the Arduino to the PC, but our app is not listening to data coming in from the COM8 port.
Note: If we would try to open the Serial Monitor again while our app is running and connected, we will get into a similar error in the Arduino IDE since, it won't be able to open the port because our app already has it opened.
DataReceived Event Handler
So how do we verify that the data received by the Arduino is correct? We also did this in the previous post. Let's add some code to subscribe to the DataReceived
event via a custom handler. By modifying the InitializeSerialPort
method we created earlier, we can define our custom event handler:
private void InitializeSerialPort()
{
serialPort = new SerialPort("COM8", 115200)
{
ReadTimeout = 1000,
WriteTimeout = 1000,
DtrEnable = true,
RtsEnable = true,
NewLine = Environment.NewLine,
};
// attach an event handler
serialPort.DataReceived += SerialPort_DataReceived;
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
// Read data from the serial port until NewLine is found and trim the newline character(s)
var dataReceived = serialPort.ReadLine().Trim();
Debug.WriteLine($"Received: {dataReceived}");
}
catch (Exception ex)
{
Debug.WriteLine("Error while receiving data: " + ex.Message);
}
}
Notice a few new extra parameters while creating the SerialPort
object:
- ReadTimeout/WriteTimeout – setting these to one second. The default is -1 which blocks forever and we don't want that
- DtrEnable/RtsEnable – Enabling Data Terminal Ready and Request To Send flags. For some reason the
DataReceived
Handler doesn't work without these on, and haven't found where it's documented that they are needed. - NewLine – We are setting this to '\r\n' on Windows to match Arduino's
Serial.print()
behavior. See the documentation for more details.
With all this in place, we can now run our app again. First make sure Arduino's Serial Monitor is not open, and Debug the app and click on Connect. This time we'll be both displaying the SimVar
values we are getting from the sim in the UI and sending them to the Arduino device as a string. The string with our SimVars
does the round robin trip back and the DataReceived
handler will read the string and print it on the Debug window:
A note on message format
One thing worth mentioning, for the sake of simplicity, our communications rely on sending the data as a string using the newline
character to determine when a "message" ends. We send from the C# app using serialPort.WriteLine()
, we read in the Arduino Sketch using Serial.readStringUntil('\n')
, then we send it back using Serial.println()
and read it in the C# app using serialPort.ReadLine()
. You really want to be able to send other data types besides string, since you don't want to have to do a lot of string parsing in Arduino when you can send floats
and ints
in byte format and convert them directly to their corresponding types on the Sketch. There are many existing standards and protocols used in Serial Communications that can do this, but they are out of scope for what I wanted to achieve here.
Conclusion
In part 2 of this 3 part series, we modified the code we created on part 1 that received data from Microsoft Flight Simulator and send it down to an Arduino device connected to the same PC where the Hello MSFS
app is running. We simplified the process by sending a newline
delimited string message and we verified that the data was received by implementing the DataReceived
handler to print out the same data we sent to the Arduino.
In the last part of this series, we'll modify the code to send the two SimVars
as floats
and then storing them on some variables in the Arduino Sketch.