Skip to content

15. Interface and Application Programming

Group Assignment - Compare Different Tools

There are a few easy options to quickly make an interface to show data and interact with a micro-controller. Processing and MIT App Inventor might be some of the most used but there are other options…

Processing

Processing appears to be quite popular. A quick search on youtube seems to suggest it’s mostly used for data visualization?

Quotting Processing’s wikipedia page:

Processing is a free graphical library and integrated development environment (IDE) built for the electronic arts, new media art, and visual design communities with the purpose of teaching non-programmers the fundamentals of computer programming in a visual context. Processing uses the Java language, with additional simplifications such as additional classes and aliased mathematical functions and operations. It also provides a graphical user interface for simplifying the compilation and execution stage. The Processing language and IDE have been the precursor to other projects including Arduino and Wiring.

MIT App Inventor

MIT App Inventor looks more friendly to an educational environment so within the context of Fab Academy it’s definitely a good option.

Wikipedia says it was originally built by Google and is now maintained by MIT.

It only supports mobile apps for iOS or Android.

Dabble

Whie searching Fab Academy past (and present) students I found someone using Dabble App. The repository hasn’t been updated in a while but it looks easy enough to setup. Seems to revolve around the ESP32-C3’s bluetooth capabilities so I don’t know if the code can be re-used with other bluetooth capable micro-controllers.

HTTP Web Server vs Websocket

Another popular option are just using micro-controllers with wi-fi to setup a small broadcast server within the actual micro-controller. ESP32’s are obviously the go-to choice. While the end-result is similar libraries appear to be split between regular Web Servers and Websocket. If I understand correctly Websocket is a specific two-way communication protocol so while all Websocket servrs are “Web Servers” not all Web Servers use Websocket… Something of the sort.

There are many many many search results for both… ESP-Dash seems nice enough. ESPUI.... Not to mention it’s possible to just build something from the ground up or close to it. A lot of libraries allow editing pure HTML/CSS, probably even letting some Java-script run.

One thing of note is that wi-fi network data in basic sketchs is always hard-coded into the micro-controller so if you move it somewhere else or change the network it can be an issue. There are tiny programs commonly called “Wifi Managers” that can run alongside other code to allow changing wifi credentials on the fly. Here’s a nice video showing how they work:

Direct Link

Arduino Cloud

Some time ago Arduino launched Arduino Cloud.

I actually did a small workshop at the last Lisbon Maker’s Faire about it hosted by a Fab Academy alumni: Marco Cassino. He’s in charge of the Fab Lab in Torino.

It allows the quick creation of interfaces to interact with a multitude of devices, not just Arduino equivalent. It seems focused on businesses but at least during the workshop it allowed adding 1-2x devices to the platform for free.

Individual Assignment - Building A GUI

This was the only assignment I didn’t actually do in 2022. By this time in the cycle I had turned my attentions to Final Project Presentations. In the end it might have been for the best since this year I could dedicate a lot more brain power to it.

For this assignment I wanted to try and build a Windows standalone app from scratch as I’d always been intimidated by a lot of projects that were released as source code only. I also wanted to focus on C/C++ to dive deeper into the language to learn/get comfortable with more advanced syntax.

In the end I made a full-blown Serial Monitor for no special reason, things just went in that general direction… Although waiting 5mins for Arduino IDE to open when we just need the serial monitor can be annoying.

I started to look at videos about GUI development and ended up picking Dear ImGUI after watching this clip:

Dear ImGUI (or just ImGUI for short) uses an Immediate Mode pipeline instead of Retained Mode.

With Immediate Mode the program controls all aspects of the scene and has direct control of what’s rendered. Retained mode as the name suggests has some data stored within the graphics library between frames and will only “intervene” if an element needs updating. Retained is easier to use but Immediate allows more freedom.

This page at learn.Microsoft helped me understand it somewhat.

Visual Studio and Vcpkg

Before getting into the ImGUI stuff we need to talk about Visual Studio. It’s what the tutorial I followed used so I stuck with it but there’s a plugin that we need and the tutorial doesn’t go a lot into it’s installation. Also: download the Community edition of Visual Studio. It wasn’t obvious when I downloaded it but the other versions are all trials so no point in getting used to functions that’ll go away after a while.

The plugin needed is Vcpkg Library Manager. It can take a bit to get it working so follow the instructions. They weren’t enough for me so I ended up watching a few clips:

This covers a lot of the setup: Direct Link

Just In case here’s another one: Direct Link

And one more, that should cover any problems you have: Direct Link

ImGUI Window

The UI had 3 development stages. First was getting a base ImGUI window onto an exe. file. Once that was ready I could start adding interface elements. And then it was about getting the interface elements to work as expected, intermediating a user to a connected board through the Windows API.

The video tutorial I followed to make my first ImGUI/c++ windows program was: Direct Link

The youtuber had an update posted, might as well: Direct Link

By the end of those we should have 4 files:

CMakeList.txt stores basic info for the compiler

cmake_minimum_required (VERSION 3.8)

add_executable (ImGUI-Test1 "ImGUI-Test1.cpp"  "UseImGUI.cpp"  "UseImGUI.h")

find_package(glad CONFIG REQUIRED)
target_link_libraries(ImGUI-Test1 PRIVATE glad::glad)

find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(ImGUI-Test1 PRIVATE glfw)

find_package(imgui CONFIG REQUIRED)
target_link_libraries(ImGUI-Test

UseImGUI.h stores the UseImGUI library and it’s list of functions.

#pragma once

#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>


class UseImGUI {
public:
    void Init(GLFWwindow* window, const char* glsl_version);
    void NewFrame();
    virtual void Update();
    void Render();
    void Shutdown();
};

UseImGUI.cpp stores the actual functions listed before. The interface code will be inside the Update() function.

#include "UseImGUI.h"

void UseImGUI::Init(GLFWwindow* window, const char* glsl_version) {
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(glsl_version);
    ImGui::StyleColorsDark();
}

void UseImGUI::NewFrame() {
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();
}

void UseImGUI::Update() {
    //ImGui::Begin();

    //ImGui::End();
}

void UseImGUI::Render() {
    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}

void UseImGUI::Shutdown() {
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();
}

Finally a ImGUI-Test1.cpp file that manages the ImGUI window plus everything else the program will do.

// ImGUI-Test1.cpp : Defines the entry point for the application.
//

#include "UseImGUI.h"
#include "glad/glad.h"
#include "GLFW/glfw3.h"

using namespace std;

int main()
{
    // Setup window
    if (!glfwInit())
        return 1;

    // GL 3.0 + GLSL 130
    const char* glsl_version = "#version 130";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
    //glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 3.2+ only
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // 3.0+ only

    // Create window with graphics context
    GLFWwindow* window = glfwCreateWindow(1280, 720, "Dear ImGui - FabAcademy", NULL, NULL);
    if (window == NULL)
        return 1;
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1); // Enable vsync

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) // Ties OpenGL to program window
        throw("unable to context to OpenGL");

    int screen_width, screen_height;
    glfwGetFramebufferSize(window, &screen_width, &screen_height);
    glViewport(0, 0, screen_width, screen_height);

    UseImGUI myimgui;
    myimgui.Init(window, glsl_version);
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
        glClearColor(0.45f, 0.55f, 0.60f, 1.00F);
        glClear(GL_COLOR_BUFFER_BIT);
        myimgui.NewFrame();
        myimgui.Update();
        //ImGui::ShowDemoWindow(); // This shows the demo menu
        myimgui.Render();
        glfwSwapBuffers(window);
    }
    myimgui.Shutdown();
    return 0;
}

When compiled they’ll create 2x files:

If You can read this an image didn't load - ImGui_exe

The program’s .exe executable file and a glfw3.dll related to the rendering engine used.

When the .exe is used it’ll open an empty window and the debug window. Think of it a bit like the serial monitor in IDEs, we can use it to show internal debug info to know what’s happening in the background.

If You can read this an image didn't load - Coms_Button

Once we close them it’ll generate an .ini file. Those tend to store settings from one session to another. In most cases it’s pointless and can be disabled by adding io.IniFilename = NULL; in the Main function.

If ImGui::ShowDemoWindow() is used it’ll make the Demo Window show up in the now not so empty window:

If You can read this an image didn't load - ImGui_Demo

Once you know the name of the UI feature you want to replicate go to the source code and check it out. For example if you need a drop-down menu, those are called Combo menus.

Designing The Interface

Now that we have a working, stand-alone but empty window it’s time to fill it all up. ImGui Wiki is a good resource to have bookmarked. That and ImGui Flags. Those flags help define and control how the UI elements behave.

When designing an interface one of the first steps will be figuring out what elements we need. Making a simple list will help make sure nothing is forgotten.

A serial monitor will need:

  • Refresh Button - So we can refresh the device list.
  • COM Port Drop-down - So we can pick which device we want to connect to.
  • Baud Rate Drop-Down - To set the connection’s speed.
  • Connect Button - For obvious reasons…
  • Disconnect Button - Just in case we want to change devices without disconnecting.
  • History Text Box - So we don’t lose track of sent and received messages.
  • Clear Button - Sometimes we do want to lose track of messages.
  • Send Text Box - Somewhere to write messages before they are sent.
  • End Character Drop-Down - This one will let us pick the character the MCU will recognize as message finished.
  • Send Button - A mostly redundant button since a good text box will send the message when pressing the Enter key.

Doesn’t hurt doing some research to get inspired…

If You can read this an image didn't load - SerialMonitor_Inspiration

I decided to give AI an ChatGPT a more serious test to see if it could help with what I was trying to do. And it did. But it wasn’t as straight forward as you might think. At least not yet.

First I asked for help in figuring out the names of the elements I wanted within ImGUI. Slowly I was getting there.

If You can read this an image didn't load - SerialMonitor_Slowly

Eventually I moved to making the UI functional. This involved a lot of back and forth. Maybe 70-85% of what it told me was wrong but caught by Visual Studio. I’d then ask for a correction; it wasn’t guaranteed to be any better. Sometimes Visual Studio didn’t catch an error during compilation but the code would still break or crash. The longer and more technical the line of questioning the less likely I was to get useful information/code and more likely it would start looping the same answer over and over. By the final bugs virtually everything was useless and I had to go back to regular search engines.

But in the end it not only save me a lot of time trying to understand some functions and connecting everything it also exposed me to much more advanced code than I would have otherwise found in such a short amount of time. I doubt I would have been able to build a fully functioning Serial Monitor for Windows within 7 days.

A lot of the initial elements added changed as I continued adding functionality as some weren’t compatible with what I had in mind. For example the Text Box used to store the message history took a few tries to get right because I wanted it to scroll automatically once messages got to the bottom of the text box’s viewport.

Eventually I got every element to do it’s thing but for some reason I still couldn’t create a stable connection.

If You can read this an image didn't load - SerialMonitor_DebugConsolePrints

Windows API serial interactions

Eventually I figured out the problem. ChatGPT was giving me the wrong advice on how to initialize a specific variable…

Understanding the DCB structure is an important step when trying to connect to a serial port with CreateFile(). Feed it the wrong parameters and nothing works.

The HANDLE data returned by the CreateFile function is also an encoded blob of data that has all information about a Serial COM connection, it will needed throughout the code so always make sure the contents of the variable that’s storing it is available everywhere it’s needed.

I learned this the hard way. It was the reason I coulnd’t get past making the initial connection when I was called up for random review the week of this assignment. The HANDLE data was being lost between the other functions to interact with the Serial Connection established previously. I’d connect but I couldn’t talk to a device or even disconnect without the correct HANDLE data as the program couldn’t identify the correct COM connection. Funny enough I pulled my first all-nighter in over 10 years before class just thinking about volunteering and talking 30 seconds about doing all this the hard way but random review had other thoughts so I ended up having to talk for way longer…

Since ChatGPT doesn’t know everything yet reading about how Serial Comunications work within the Windows API can help identify issues.

Final Interface working

Eventually I got everything working. Even made the list update on program execution to save clicking update that first time.

If You can read this an image didn't load - SerialMonitor_ImGui_RefreshList

Baur Rate drop-down.

If You can read this an image didn't load - SerialMonitor_ImGui_BaudRate

Go go and no go.

If You can read this an image didn't load - SerialMonitor_ImGui_ConnectDis

End character drop-down.

If You can read this an image didn't load - SerialMonitor_ImGui_EndOfLine

Connection made.

If You can read this an image didn't load - SerialMonitor_ImGui_Connection1

Message sent and received.

If You can read this an image didn't load - SerialMonitor_ImGui_Connection2

Restarting the loop.

If You can read this an image didn't load - SerialMonitor_ImGui_Connection3

Clearing history.

If You can read this an image didn't load - SerialMonitor_ImGui_Clear

Pictures don’t make it justice though. Pretty happy how it turned out:

Ended up using it again in the Networking And Communications group assignment to interact with my final project’s motherboard and turn some LED’s On and Off:

Finalized Code

It’s a bit too much code to talk about all of it so I’ll focus on the small loop I was running on a micro-controller to test sending out messages from the monitor and the actual Serial functions outside the context of the ImGUI interface. They’re on their own file so it should be easy to re-use them.

MCU Test loop

The test loop is just switching the BuiltIn LED ON/OFF and sending a serial message of the current state. If it receives STOP or START it’ll change the boolean variable acordingly and affect the ON/OFF loop. This is the code running on the MCU:

bool running = true; // flag to keep track if the loop should run or not

void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {

  if (Serial.available() > 0) {
    delay(100);
    Serial.println("DEBUG: waiting for input");
    String input = Serial.readStringUntil('\n');
    Serial.print("Received message: ");
    Serial.println(input);
    if (input == "STOP") {
      running = false;
      Serial.println("Loop stopped");
    }
    else if (input == "START") {
      running = true;
      Serial.println("Loop started");
    }
  }

  if (running) {
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("On");
    delay(1000);
    digitalWrite(LED_BUILTIN, LOW);
    Serial.println("Off");
    delay(1000);
  }
}

SerialFunctions.h

If I’m not mistaken this is usually called a class file.

#ifndef USE_SERIAL_H
#define USE_SERIAL_H

#pragma once

#include "vector"
#include "string"
#include "windows.h"

class useSerial {
  public:
    enum SerialError {
      SERIAL_ERROR_SUCCESS = 0,
      SERIAL_ERROR_PORT_NOT_FOUND = 1,
      SERIAL_ERROR_PORT_IN_USE = 2,
      SERIAL_ERROR_PERMISSION_DENIED = 3,
      SERIAL_ERROR_INVALID_BAUD_RATE = 4,
      SERIAL_ERROR_INVALID_DATA_BITS = 5,
      SERIAL_ERROR_INVALID_STOP_BITS = 6,
      SERIAL_ERROR_INVALID_PARITY = 7,
      SERIAL_ERROR_FAILED_TO_GET_STATE = 8,
      SERIAL_ERROR_FAILED_TO_SET_STATE = 9,
      SERIAL_ERROR_TIMEOUT = 10,
      SERIAL_ERROR_FAILED_TO_OPEN_PORT = 11
    };

    std::vector<std::string> getAvailableSerialPorts();
    bool useSerial::isPortAvailable(std::string portName);
    SerialError connectSerialPort(std::string portName, int baudRate);
    void closeSerialPort(int port);
    bool sendSerialData(const char* buffer, int SendbufferSize);
    bool useSerial::readSerialData(char* buffer, int ReadbufferSize = useSerial::readBufferSize);
    bool useSerial::isConfigured();


    static DWORD bytesRead;
    // Defines the variable with the size of the buffer of data read from serial
    static const int readBufferSize = 256;
    static char readBuffer[readBufferSize];

    static bool connectedCOM;

    static char useSerial::inputBuffer[1024 * 1]; // store the input data here
    static bool useSerial::inputEntered;

  private:

    // Defines 
    static HANDLE hSerial;

    static std::string f_name;


    // Define the default values for serial connection parameters
    uint8_t dataBits = 8;  // Default data bits is 8
    uint8_t parity = NOPARITY;    // Default parity is 0 - accepts "NOPARITY"
    uint8_t stopBits = ONESTOPBIT;  // Default stop bits is 0 - accepts "ONESTOPBIT"
    int readTimeout = 5000;  // in ms, 5 seconds
    int writeTimeout = 5000; // in ms, 5 seconds




};

#endif // USE_SERIAL_H

getAvailableSerialPorts()

Pretty obvious, this one checks for available Com Ports.

Also: those std::cout + std::endl and std::cerr + std::endl are the C versions of serial.print so those are all debug messages sent to the debug monitor.

std::vector<std::string> useSerial::getAvailableSerialPorts() {
    // Define a vector to store the available COM ports
    std::vector<std::string> com_ports_list;

    // Use the SetupDiGetClassDevs() function to retrieve a list of all connected serial devices
    HDEVINFO device_info_set = SetupDiGetClassDevs(&GUID_DEVCLASS_PORTS, NULL, NULL, DIGCF_PRESENT);
    if (device_info_set == INVALID_HANDLE_VALUE) {
        std::cerr << "Failed to get device info set: " << GetLastError() << std::endl;
        return com_ports_list;
    }

    // Define a SP_DEVINFO_DATA struct to store device info
    SP_DEVINFO_DATA device_info_data;
    device_info_data.cbSize = sizeof(SP_DEVINFO_DATA);

    // Loop through each connected device and check if it's a serial device
    for (DWORD i = 0; SetupDiEnumDeviceInfo(device_info_set, i, &device_info_data); i++) {
        // Define a buffer to store the friendly name of the device
        TCHAR friendly_name[MAX_PATH];
        DWORD friendly_name_size = MAX_PATH;

        // Retrieve the friendly name of the device
        if (SetupDiGetDeviceRegistryProperty(device_info_set, &device_info_data, SPDRP_FRIENDLYNAME, NULL, (PBYTE)friendly_name, friendly_name_size, NULL)) {
            // Check if the device is a serial device
            if (std::string(friendly_name).find("COM") != std::string::npos) {
                // Add the available COM port to the vector
                std::cout << "Adding device with friendly name: " << friendly_name << std::endl;
                com_ports_list.push_back(std::string(friendly_name));
            }
            else {
                std::cout << "Device with friendly name: " << friendly_name << " is not a serial device" << std::endl;
            }
        }
        else {
            std::cerr << "Failed to get friendly name: " << GetLastError() << std::endl;
        }
    }

    // Cleanup the device info set
    SetupDiDestroyDeviceInfoList(device_info_set);

    // Return the vector of available COM ports
    for (const auto& port : com_ports_list) {
        std::cout << "Available port: " << port << std::endl;
    }
    std::cout << "com_ports_list size: " << com_ports_list.size() << std::endl;
    return com_ports_list;
}

isPortAvailable()

This one makes sure a specific COM port is still available. This check is done before trying to connect to it.

bool useSerial::isPortAvailable(std::string portName) {
    HANDLE hComm = CreateFile(portName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    if (hComm == INVALID_HANDLE_VALUE) {
        // Port is not available, it's already in use
        std::cerr << "useSerial:: Selected COM port is already in use" << std::endl;
        return false;
    }
    else {
        // Port is available, close the handle and return true
        // std::cerr << "useSerial:: Selected COM port is Available " << std::endl;
        CloseHandle(hComm);
        return true;
    }
}

connectSerialPort()

This is the function that will create a connection if successful.

First there’s a logic check to make sure the COM port is still available and we’re not already connected to it. Then it’ll store the HANDLE data returned by CreateFile() inside the hSerial variable. Whenever we want to interact with the device in a given COM Port we need that hSerial and the HANDLE it contains. Don’t mess with the CreateFile() parameters unless you know what you’re doing.

Making sure those were set right also took me sometime. While their settings might not be too important for the end-product we’re aiming for they’ll affect how the connection is setup so the rest of the Connect function’s code will depend on how the paremeter’s are set. ChatGPT also burned some of my time here because while it was giving me technically viable code it didn’t interact correctly. Which is a recuring theme with ChatGPT.

useSerial::SerialError useSerial::connectSerialPort(std::string portName, int baudRate) {
    useSerial::f_name = portName;
    // std::cout << "useSerial:: Parameter input Data bits: " << +dataBits << std::endl; // small number in equivalent to CHAR datatypes are reserved characters on the print command, they must be explicitly noted as numerical values with static_cast or unary plus
    // std::cout << "useSerial:: Parameter input Parity: " << +parity << std::endl; // small number in equivalent to CHAR datatypes are reserved characters on the print command, they must be explicitly noted as numerical values with static_cast or unary plus
    // std::cout << "useSerial:: Parameter input Stop bits: " << +stopBits << std::endl; // small number in equivalent to CHAR datatypes are reserved characters on the print command, they must be explicitly noted as numerical values with static_cast or unary plus
    // std::cout << "useSerial:: Parameter input readTimeout: " << readTimeout << std::endl;
    // std::cout << "useSerial:: Parameter input writeTimeout bits: " << writeTimeout << std::endl;
    // std::cerr << "useSerial:: 1 COM port connection status " << useSerial::connectedCOM << std::endl;

    // it will only try to connect if the COM port is available and it's we're not already connected
    if (useSerial::isPortAvailable(portName) && useSerial::connectedCOM != true) {
        // Open the serial port
        hSerial = CreateFile(portName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

        if (hSerial == INVALID_HANDLE_VALUE) {

            DWORD lastError = GetLastError();
            LPSTR messageBuffer = nullptr;
            size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, lastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);
            std::cout << "userSerial::Failed to open serial port with error code " << lastError << ": " << messageBuffer << std::endl;
            LocalFree(messageBuffer);

            if (lastError == ERROR_FILE_NOT_FOUND) {
                std::cout << "userSerial::Failed to create handle to serial port: " << GetLastError() << std::endl;
                return SERIAL_ERROR_PORT_NOT_FOUND;
            }
            else if (lastError == ERROR_ACCESS_DENIED) {
                return SERIAL_ERROR_PERMISSION_DENIED;
            }
            else if (lastError == ERROR_ALREADY_EXISTS) {
                return SERIAL_ERROR_PORT_IN_USE;
            }
            else {
                return SERIAL_ERROR_FAILED_TO_OPEN_PORT;
            }
        }

        if (hSerial == INVALID_HANDLE_VALUE) {
            DWORD lastError = GetLastError();
            if (lastError == ERROR_FILE_NOT_FOUND) {
                return SERIAL_ERROR_PORT_NOT_FOUND;
            }
            else if (lastError == ERROR_ACCESS_DENIED) {
                return SERIAL_ERROR_PERMISSION_DENIED;
            }
            else if (lastError == ERROR_ALREADY_EXISTS) {
                return SERIAL_ERROR_PORT_IN_USE;
            }
            else {
                return SERIAL_ERROR_FAILED_TO_OPEN_PORT;
            }
        }

        // Set the serial port parameters
        DCB dcbSerialParams = { 0 };
        dcbSerialParams.DCBlength = sizeof(dcbSerialParams);

        if (!GetCommState(hSerial, &dcbSerialParams)) {
            std::cerr << "useSerial:: Failed to get serial port parameters for port " << portName << std::endl;
            return SERIAL_ERROR_FAILED_TO_GET_STATE;
        }

        dcbSerialParams.BaudRate = baudRate;
        dcbSerialParams.ByteSize = useSerial::dataBits;
        dcbSerialParams.StopBits = useSerial::stopBits;
        dcbSerialParams.Parity = useSerial::parity;

        if (!SetCommState(hSerial, &dcbSerialParams)) {

            // Failed to set serial port parameters
            std::cerr << "useSerial:: Failed to set serial port parameters for port " << portName << std::endl;
            CloseHandle(hSerial);
            return SERIAL_ERROR_FAILED_TO_SET_STATE;
        }
        else {
            // Set read and write timeouts

            COMMTIMEOUTS timeouts = { 0 };
            timeouts.ReadIntervalTimeout = readTimeout;
            timeouts.ReadTotalTimeoutConstant = readTimeout;
            timeouts.ReadTotalTimeoutMultiplier = 0;
            timeouts.WriteTotalTimeoutConstant = writeTimeout;
            timeouts.WriteTotalTimeoutMultiplier = 0;
            if (!SetCommTimeouts(hSerial, &timeouts)) {

                // Failed to set serial port timeouts
                std::cerr << "useSerial:: Failed to set serial port timeouts for port " << portName << std::endl;
                CloseHandle(hSerial);
                return SERIAL_ERROR_FAILED_TO_SET_STATE;
            }

            // std::cout << "useSerial:: Data bits: " << +dataBits << std::endl; // small number in equivalent to CHAR datatypes are reserved characters, they must be explicitly noted as numerical values with static_cast or unary plus
            // std::cout << "useSerial:: Parity: " << +parity << std::endl; // small number in equivalent to CHAR datatypes are reserved characters, they must be explicitly noted as numerical values with static_cast or unary plus
            // std::cout << "useSerial:: Stop bits: " << +stopBits << std::endl; // small number in equivalent to CHAR datatypes are reserved characters, they must be explicitly noted as numerical values with static_cast or unary plus
            // std::cout << "useSerial:: readTimeout: " << readTimeout << std::endl;
            // std::cout << "useSerial:: writeTimeout: " << writeTimeout << std::endl;

            // Return the error code
            // std::cerr << "useSerial::2 COM port connection status " << useSerial::connectedCOM << std::endl;
            useSerial::connectedCOM = true;
            // std::cerr << "useSerial::2 COM port connection status " << useSerial::connectedCOM << std::endl;
            std::cerr << "useSerial:: Successfully connected to " << portName << std::endl;

            PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR);
            Sleep(1000);
        }

    }
    return SERIAL_ERROR_SUCCESS;
}

closeSerialPort()

Don’t think I need to explain this one either.

void useSerial::closeSerialPort(int port) {
    // std::cout << "useSerial::closeSerialPort Start" << std::endl;
    PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR);
    Sleep(100);
    // std::cerr << "useSerial:: COM port connection status " << useSerial::connectedCOM << std::endl;
    if (useSerial::connectedCOM == true) {
        if (hSerial != INVALID_HANDLE_VALUE) {
            DWORD error = GetLastError();
            // std::cout << "Error after (hSerial != INVALID_HANDLE_VALUE): " << error << std::endl;
            if (CloseHandle(hSerial)) {
                DWORD error = GetLastError();
                // std::cout << "Error after CloseHandle(hSerial): " << error << std::endl;

                std::cout << "useSerial:: Successfully disconnected from " << useSerial::f_name << std::endl;
                useSerial::connectedCOM = false;
            }
            else {
                std::cout << "Error closing serial port: " << GetLastError() << std::endl;
            }
            hSerial = INVALID_HANDLE_VALUE;
        }
    } else {
        std::cout << "useSerial:: Can't disconnect if not connected" << std::endl;
    }
    // std::cout << "useSerial::closeSerialPort End" << std::endl;
}

sendSerialData()

Data goes.

bool useSerial::sendSerialData(const char* buffer, int SendbufferSize) {

    DWORD bytesWritten;

    if (connectedCOM) {
        if (!WriteFile(hSerial, buffer, SendbufferSize, &bytesWritten, NULL)) {
            std::cerr << "useSerial:: Failed to write to serial port" << std::endl;
            return false;
        }
        return true;
    }
}

readSerialData()

Data comes.

This one was also somewhat challenging as it interacted with UI elements differently. It had to run in the background unlike the others that relied on user input. Thankfully ChatGPT had me covered.

bool useSerial::readSerialData(char* buffer, int readBufferSize) {
    COMSTAT status = { 0 };
    unsigned int toRead = 0;

    if (connectedCOM) {
        if (!ClearCommError(hSerial, NULL, &status)) {
            DWORD errorCode = GetLastError();
            std::cerr << "useSerial:: Failed to clear comm error. Error code: " << errorCode << std::endl;
            Sleep(500);
            return false;
        }


        if (status.cbInQue > 0) {
            if (status.cbInQue > readBufferSize) {
                toRead = useSerial::readBufferSize;
                readBufferSize = 0;
                // std::cout << "userSerial::toRead was set to " << toRead << " After if (status.cbInQue > bufferSize)" << std::endl;
                // Sleep(500);
            }
            else {
                toRead = status.cbInQue;
                // std::cout << "userSerial::toRead was set to " << toRead << " After else" << std::endl;
                // Sleep(500);
            }

            if (!ReadFile(hSerial, buffer, toRead, &useSerial::bytesRead, NULL)) {
                DWORD errorCode = GetLastError();
                std::cerr << "useSerialData:: Failed to read from serial port. Error code: " << errorCode << std::endl;
                return false;
            }

            buffer[useSerial::bytesRead] = '\0'; // null-terminate received data
            // std::cout << "userSerial::Read " << useSerial::bytesRead << " bytes from serial port." << std::endl;
            // std::cout << "userSerial::toRead was set to " << toRead << " After else" << std::endl;
            // Sleep(500);
        }
    }

    return true;
}

isConfigured()

Just to check if there’s a COM Port already setup. Actually as I document this I don’t think this function is still used… But this isn’t the time to go deleting random functions…

bool useSerial::isConfigured() {
    return hSerial != INVALID_HANDLE_VALUE;
}

Files

Serial Monitor .exe ready for use

Serial Monitor’s Source Code


Last update: July 16, 2023
Back to top