16. Sending events - Part 3
In this final part of this Sending Events series, we'll put everything we learned so we can now send events from the device to the C# app, and then to Microsoft Flight Simulator.
Arduino Sketch Changes
Lets insert the rotary encoder code from part 2 with a small modification so we can send a more specific string
when the state changes:
#include "SPI.h"
#include "ILI9341_t3.h"
#define TFT_DC 9
#define TFT_CS 10
#define CHAR_WIDTH 6
#define CLK_PIN 14
#define DATA_PIN 15
#define BUTTON_PIN 16
ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC);
float currentAltitude = 0;
float previousAltitude = -1;
float currentBarometer = 0;
float previousBarometer = -1;
int lastEncoderState;
int lastEncoderButtonState;
void setup() {
pinMode(CLK_PIN, INPUT_PULLUP);
pinMode(DATA_PIN, INPUT_PULLUP);
pinMode(BUTTON_PIN, INPUT_PULLUP);
lastEncoderState = digitalRead(CLK_PIN);
// 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);
tft.begin();
tft.setRotation(1);
tft.fillScreen(ILI9341_BLACK);
// labels
drawTextCentered("Altitude (feet)", 0, 2, ILI9341_WHITE);
drawTextCentered("Barometer (inHg)", 120, 2, ILI9341_WHITE);
// refresh values
update();
// wait for a serial connection before beginning
while (!Serial) {}
}
void loop() {
// check if we have at least 8 bytes. our full message should be 9 bytes,
if (Serial.available() > 8) {
currentAltitude = readFloat();
currentBarometer = readFloat();
Serial.readStringUntil('\n');
update();
}
// process encoder
switch (readEncoder(CLK_PIN, DATA_PIN, lastEncoderState)) {
case -1:
Serial.println("ENCODER_DECREASED");
break;
case 1:
Serial.println("ENCODER_INCREASED");
break;
}
// check if the button was pressed
if (readButton(BUTTON_PIN, lastEncoderButtonState)) {
Serial.println("ENCODER_BUTTON_PRESSED");
}
// small delay to give it time for debouncing
delay(1);
}
// util method to read 4 bytes from the serial port and return them as a float
float readFloat() {
char buffer[4];
Serial.readBytes(buffer, 4);
float value;
memcpy(&value, buffer, sizeof(float));
return value;
}
// util method to redraw the values
void update() {
if (previousAltitude != currentAltitude) {
tft.fillRoundRect(30, 40, 260, 50, 10, ILI9341_GREEN);
tft.drawRoundRect(30, 40, 260, 50, 10, ILI9341_WHITE);
drawTextCentered(String(currentAltitude), 50, 4, ILI9341_BLACK);
previousAltitude = currentAltitude;
}
if (previousBarometer != currentBarometer) {
tft.fillRoundRect(30, 160, 260, 50, 10, ILI9341_GREEN);
tft.drawRoundRect(30, 160, 260, 50, 10, ILI9341_WHITE);
drawTextCentered(String(currentBarometer), 170, 4, ILI9341_BLACK);
previousBarometer = currentBarometer;
}
}
// util method to draw centered
void drawTextCentered(String text, int y, int size, uint16_t color) {
int offset = 160 - (text.length() * CHAR_WIDTH * size / 2);
tft.setTextSize(size);
tft.setCursor(offset, y);
tft.setTextColor(color);
tft.print(text);
}
// simple funciton that reads the state of a button connected to 'pin', with current and last states for determining if the button is being pressed or not.
bool readButton(uint8_t pin, int& lastState) {
int state = !digitalRead(pin);
if (state != lastState) {
lastState = state;
if (state == 0) {
return true;
}
}
return false;
}
// In my encoder, and depends on how you wired it, this is the sequence.
// If yours is inverted, you can invert the CLK and DATA connections
// cw 00,01,11,10,00
// ccw 00,10,11,01,00
int readEncoder(int clkPin, int dataPin, int& lastState) {
int state = digitalRead(clkPin);
if (state != lastState) {
lastState = state;
int dataState = digitalRead(dataPin);
if (dataState != state) {
return -1;
}
else {
return 1;
}
}
return 0;
}
Notice that we are now sending ENCODER_DECREASED
, ENCODER_INCREASED
and ENCODER_BUTTON_PRESSED
, but we really can send anything we like, 0,1,2, 'D','I','P', or even the original string messages as long as our C# code matches with what we sent. I opted for these human readable strings for demo and debugging purposes.
WPF Application Changes
Now let's look at the SerialPort_DataReceived(...)
method, which it's really the only thing we have to modify:
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();
switch (dataReceived)
{
case "ENCODER_DECREASED":
simconnect.TransmitClientEvent(
SimConnect.SIMCONNECT_OBJECT_ID_USER,
SimEventType.KOHLSMAN_DEC,
0,
GroupId.FLAG,
SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY
);
break;
case "ENCODER_INCREASED":
simconnect.TransmitClientEvent(
SimConnect.SIMCONNECT_OBJECT_ID_USER,
SimEventType.KOHLSMAN_INC,
0,
GroupId.FLAG,
SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY
);
break;
case "ENCODER_BUTTON_PRESSED":
simconnect.TransmitClientEvent_EX1(
SimConnect.SIMCONNECT_OBJECT_ID_USER,
SimEventType.KOHLSMAN_SET,
GroupId.FLAG,
SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY,
(uint)(STANDARD_PRESSURE * INHG_TO_MILLIBAR * 16), 0, 0, 0, 0
);
break;
default:
Debug.WriteLine($"Unsupported message received: {dataReceived}");
break;
}
}
catch (Exception ex)
{
Debug.WriteLine("Error while receiving data: " + ex.Message);
}
}
Since we are sending a string, we can still use serialPort.ReadLine()
to read a string
from the port and put that in a switch
statement for each one of the 3 message types that we are sending. We moved the code from the event handlers into each one of the corresponding case
blocks and added a default for debugging purposes.
At this point you can remove the additional 3 buttons we added in Part 1 for sending the events and remove the handlers, but that's optional. They both should still work independently.
Build and upload the Sketch, build and run the WPF app and launch the Sim and click on connect. Rotate the encoder clockwise and counterclockwise and you'll see the barometer setting increasing and decreasing, in addition to the altimeter getting updated because of the barometer setting change.
Demo
Here is a short video with a demo on how it should work. I'm using a dual rotary encoder hence the extra wires, but like I mentioned in the previous post, it works the same. And once again, ignore the red lines, it's my broken screen that I use for development purposes 😅
Wrapping up
In this 3 Part series about Sending Events, we learned how to use SimConnect SDK
to send events to Microsoft Flight Simulator
. We learned about the two methods available TransmitClientEvent()
and TransmitClientEvent_EX1()
and their differences. We learned about electromechanical rotational input devices like potentiometers and rotary encoders and we looked into rotary encoders deeper. Finally we combined the code we wrote in part 1 with what we learned about rotary encoders in part 2 so we could convert an action performed on a rotary encoder into an event in the sim.
If you want to have a look at the instruments I've designed drawn in the same way explained in this series, have a look at My Flight Instruments where I put screenshots and a description of each one of the ones I've designed.
Hope you found this series useful, and see you on the next one!