16. Sending events - Part 3

16. Sending events - Part 3
Photo by Sean Foster / Unsplash

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!