10
\$\begingroup\$

Please review my serial port class written in C++. It is completely asynchronous, ie event driven. My idea for later is to inherit from this a sync_serial, where timeouts can be specified and it waits for responses. But that is not the purpose of this review. It is Windows only and I have tested on a Conexant voice modem. It uses the Microsoft overlapped IO model.

Header file, async_serial.hpp:

/*
asynchronous serial communications class
*/
#ifndef ASYNC_SERIAL_
#define ASYNC_SERIAL_

#include <thread>
#include <queue>
#include <unordered_map>
#include <stdint.h>

// forward declare pimpl
struct serialimpl;

// utility namespace
namespace utl {

    enum line_status {
        LS_CTS,   // clear to send signal changed state
        LS_DSR,   // data set ready signal changed
        LS_CD,    // carrier detect signal changed state
        LS_BREAK, // a break was detected in input
        LS_ERR,   // a line status error occurred
        LS_RING   // ring indicator detected
    }; 

struct datum
{
    datum(char* data, size_t length) : data_(data), length_(length) {}
    char* data_;
    size_t length_;
};

enum parity_type
{
    PARITYTYPE_NONE,  // most common
    PARITYTYPE_ODD,
    PARITYTYPE_EVEN
};

// bits per packet/frame
enum databits_type
{
    DATA_5 = 5,
    DATA_6 = 6,
    DATA_7 = 7,
    DATA_8 = 8       // most common
};

enum stopbits_type
{
    STOPBIT_1,       // most common
    STOPBIT_1_5,
    STOPBIT_2
};

enum flow_control_type
{
    FLOWCONTROL_OFF,              // no flow control - worth trying to get something working
    FLOWCONTROL_HARDWARE_RTSCTS,  // For best performance if supported by modem/cable
    FLOWCONTROL_HARDWARE_DTRDSR,  // not very common hardware flow control
    FLOWCONTROL_XONXOFF           // if cable has no flow control pins connected
};

struct port_settings
{
    port_settings(unsigned baud, databits_type databits = DATA_8, parity_type parity = PARITYTYPE_NONE,
        unsigned stopbits = STOPBIT_1, flow_control_type flowcontrol = FLOWCONTROL_HARDWARE_RTSCTS)
        : baud_rate_(baud), databits_(databits), parity_type_(parity), stopbits_(stopbits), flowcontrol_(flowcontrol) {}
    unsigned baud_rate_;       // speed in bits per second
    databits_type databits_;   // usually set to 8 (8 bits per packet/frame)
    parity_type parity_type_;  
    unsigned stopbits_;
    flow_control_type flowcontrol_;
};

class async_serial
{
public:
    // construct using port and baudrate
    async_serial(int port, unsigned baudrate);
    // construct using port and port_settings data
    async_serial(int port, port_settings settings);
    // close port related system resources
    virtual ~async_serial();
    // open serial port and setup handling for data
    virtual bool open();
    // check if serial port open
    bool is_open() const;
    // close serial port
    virtual bool close();
    // write data to port
    bool write(const char* data, size_t length);
    // override this function to handle received data
    virtual void on_read(char* data, size_t length);
    // override this function to identify when data successfully sent
    virtual void on_write();
    // override this function to indentify serial port state changes
    virtual void on_status(const unsigned statechange, bool set = true);
    // // override this function for diagnostic information
    virtual void on_error(const std::string& error);
    // get line status
    inline unsigned get_status() const { return status_; }
    // helper to convert status to a descriptive string
    std::string get_status_string();
    // helper function to get list of ports available and any available string description
    static long enumerate_ports(std::unordered_map <uint32_t, std::string>& ports);

    async_serial(const async_serial&) = delete;
    async_serial& operator=(const async_serial&) = delete;

private:
    // forward declare pimpl
    serialimpl *pimpl_;  // Handle object - pimpl

    std::thread reader_thread;  // thread to handle read events
    void reader_thread_func();  // read thread handler function
    bool closing_port_;        // to indicate port is in process of being closed
    int port_;                  // port indentifier
    unsigned status_;           // status indication

    void update_pin_states();   // updates internal pin status flags
    void update_error_states(unsigned long comm_event);  // reports errors if any

    port_settings settings_;
    bool configure_comport();         // initial port setup called in open

    std::thread writer_thread;  // thread to handle write events
    void writer_thread_func();  // write thread handler function

    std::queue<datum> write_queue_;  // data is queued for sending/(writing)

    inline bool job_queue_empty() const { return write_queue_.empty(); }
};

}  // utl

#endif // ASYNC_SERIAL_

Implementation file, async_serial.cpp:

/*
 async_serial class implementation for Windows.  Inspired by MTTTY sample application.
*/
#include <cstdio>
#include <cctype>
#include <chrono>
#include <memory>
#include <string>
#include <unordered_map>
#include <iostream>
#include <sstream>

#include <Windows.h>
// WMI header for Windows
#include <Wbemcli.h>
// WMI library for Windows
#pragma comment(lib, "wbemuuid.lib")

#include "async_serial.hpp"


static const DWORD READ_BUFFER_SIZE = 512;
// milliseconds timeout for waiting for serial port events - read or status
static const DWORD OVERLAPPED_CHECK_TIMEOUT = 500;

using namespace utl;

// port handle - keep system specifics out of header
// *** PROBLEM - TODO - problem if user wants to create multiple instances of class. PIMPL?
// hide implementation details from header
struct serialimpl {
    HANDLE handle_ = INVALID_HANDLE_VALUE;
};


static std::string system_error() {
    // Retrieve, format, and print out a message from the last error.  
    char buf[256] = {};
    DWORD lasterror = GetLastError();
    DWORD chars = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, lasterror, 0, buf, 255, NULL);

    std::string error;
    if (chars) {
        error = buf;
    }
    else {
        error = "FormatMessage returned zero characters. Last error code: ";
        error += std::to_string(lasterror);
    }

    return error;
}

static std::string create_error_msg(const char* prepend) {
    std::string s(prepend);
    s += system_error();
    return s;
}

long async_serial::enumerate_ports(std::unordered_map <uint32_t, std::string>& ports) {

    // Initialize COM.
    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
    if (FAILED(hr))
        return hr;

    // Initialize security for application
    hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT,
        RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL);
    if (FAILED(hr)) {
        CoUninitialize();
        return hr;
    }

    //Create the WBEM locator (to WMI)
    std::shared_ptr<IWbemLocator> wben_locator;
    hr = CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,
        IID_IWbemLocator, reinterpret_cast<void**>(&wben_locator));
    if (FAILED(hr)) {
        CoUninitialize();
        return hr;
    }

    IWbemServices* wbem_services = NULL;
    hr = wben_locator->ConnectServer(L"ROOT\\CimV2", nullptr, nullptr, nullptr,
        0, nullptr, nullptr, &wbem_services);
    if (FAILED(hr)) {
        wben_locator->Release();
        CoUninitialize();
        return hr;
    }

    // WMI query of serial ports on this computer
    IEnumWbemClassObject* wbem_enumerate_object = NULL;
    hr = wbem_services->CreateInstanceEnum(L"Win32_SerialPort",
        WBEM_FLAG_RETURN_WBEM_COMPLETE, nullptr, &wbem_enumerate_object);
    if (FAILED(hr)) {
        wben_locator->Release();
        CoUninitialize();
        return hr;
    }

    // Now enumerate all the ports
    hr = WBEM_S_NO_ERROR;

    // when no Next object enumerated, WBEM_S_FALSE returned
    while (hr == WBEM_S_NO_ERROR) {
        ULONG numports = 0;
        IWbemClassObject** wbem_object = (IWbemClassObject**)malloc(10 * sizeof(IWbemClassObject));
        hr = wbem_enumerate_object->Next(WBEM_INFINITE, 10, reinterpret_cast<IWbemClassObject**>(wbem_object), &numports);
        if (SUCCEEDED(hr)) {
            // if > 10 objects returned have to re-allocate wbem_object
            if (numports > 10) {
                wbem_object = (IWbemClassObject**)realloc(wbem_object, numports * sizeof(IWbemClassObject));
            }

            for (ULONG i = 0; i < numports; ++i) {
                VARIANT device_property1;
                const HRESULT hrGet = wbem_object[i]->Get(L"DeviceID", 0, &device_property1, nullptr, nullptr);
                if (SUCCEEDED(hrGet) && (device_property1.vt == VT_BSTR) && (wcslen(device_property1.bstrVal) > 3)) {
                    // if deviceID is prefaced with "COM" add ports list
                    if (wcsncmp(device_property1.bstrVal, L"COM", 3) == 0) {
                        // get port number
                        std::wistringstream wstrm(std::wstring(&(device_property1.bstrVal[3])));
                        unsigned int port;
                        wstrm >> port;

                        VariantClear(&device_property1);
                        std::pair<UINT, std::string> pair;
                        pair.first = port;

                        // get the friendly name of the port
                        VARIANT device_property2;
                        if (SUCCEEDED(wbem_object[i]->Get(L"Name", 0, &device_property2, nullptr, nullptr)) && (device_property2.vt == VT_BSTR)) {
                            std::wstring ws(device_property2.bstrVal);
                            std::string name(ws.begin(), ws.end());
                            pair.second = name;
                            VariantClear(&device_property2);
                        }

                        ports.insert(pair);
                    }
                }
            }
        }
        free(wbem_object);
    }

    // cleanup
    wbem_enumerate_object->Release();
    wbem_services->Release();
    wben_locator->Release();
    CoUninitialize();

    return S_OK;
}



// comport settings based on configuration requested
bool async_serial::configure_comport()
{
    DCB dcb = { 0 };
    dcb.DCBlength = sizeof(dcb);

    // get current DCB settings
    if (!GetCommState(pimpl_->handle_, &dcb)) {
        on_error(create_error_msg("GetCommState error "));
        return false;
    }

    // update DCB rate, byte size, parity, and stop bits size
    dcb.BaudRate = settings_.baud_rate_;
    dcb.ByteSize = settings_.databits_;
    dcb.Parity = settings_.parity_type_;
    dcb.StopBits = settings_.stopbits_;

    // event flag
    dcb.EvtChar = '\0';

    // set suitable flow control settings
    switch (settings_.flowcontrol_) {
        case FLOWCONTROL_HARDWARE_RTSCTS:  // most common hardware flow control
            dcb.fDtrControl = DTR_CONTROL_ENABLE;
            dcb.fRtsControl = RTS_CONTROL_HANDSHAKE;
            dcb.fOutxCtsFlow = 1;
            dcb.fOutxDsrFlow = 0;
            dcb.fOutX = 0;
            dcb.fInX = 0;
        break;
        case FLOWCONTROL_HARDWARE_DTRDSR:  // not very common hw flow control
            dcb.fDtrControl = DTR_CONTROL_HANDSHAKE;
            dcb.fRtsControl = RTS_CONTROL_ENABLE;
            dcb.fOutxCtsFlow = 0;
            dcb.fOutxDsrFlow = 1;
            dcb.fOutX = 0;
            dcb.fInX = 0;
            break;
        case FLOWCONTROL_XONXOFF:  // software flow control
            dcb.fDtrControl = DTR_CONTROL_ENABLE;
            dcb.fRtsControl = RTS_CONTROL_ENABLE;
            dcb.fOutxCtsFlow = 0;
            dcb.fOutxDsrFlow = 0;
            dcb.fOutX = 1;
            dcb.fInX = 1;
            break;
        case FLOWCONTROL_OFF:  // no flow control
            dcb.fDtrControl = DTR_CONTROL_ENABLE;
            dcb.fRtsControl = RTS_CONTROL_ENABLE;
            dcb.fOutxCtsFlow = 0;
            dcb.fOutxDsrFlow = 0;
            dcb.fOutX = 0;
            dcb.fInX = 0;
            break;
    }

    // set the rest to suitable defaults
    dcb.fDsrSensitivity = 0;
    dcb.fTXContinueOnXoff = 0;
    dcb.XonChar = 0x11;     // Tx and Rx XON character
    dcb.XoffChar = 0x13;    // Tx and Rx XOFF character
    dcb.XonLim = 0;         // transmit XON threshold
    dcb.XoffLim = 0;        // transmit XOFF threshold
    dcb.fAbortOnError = 0;  // abort reads/writes on error
    dcb.ErrorChar = 0;      // error replacement character
    dcb.EofChar = 0;        // end of input character

    // Windows does not support non-binary mode so this must be TRUE
    dcb.fBinary = TRUE;

    // DCB settings not in the user's control
    dcb.fParity = TRUE;

    // set new state
    if (!SetCommState(pimpl_->handle_, &dcb)) {
        on_error(create_error_msg("SetCommState error "));
        return false;
    }

    COMMTIMEOUTS ctout = { 0x01, 0, 0, 0, 0 };
    if (!SetCommTimeouts(pimpl_->handle_, &ctout)) {
        on_error(create_error_msg("SetCommTimeouts error "));
    }

    return true;
}

async_serial::async_serial(int port, port_settings settings) :
  port_(port),
  settings_(settings),
  closing_port_(false),
  status_(0), 
  pimpl_(new serialimpl) {}

async_serial::async_serial(int port, unsigned baudrate) 
    : port_(port), 
      settings_(9600), 
      closing_port_(false), 
      status_(0), 
      pimpl_(new serialimpl) {}


void async_serial::writer_thread_func() {
    OVERLAPPED overlapped_write = { 0 };
    DWORD bytes_written;

    // create this writes overlapped structure hEvent
    overlapped_write.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (overlapped_write.hEvent == NULL)
    {
        on_error("Error creating writer overlapped event");
        return;
    }

    // keep thread running all the time that we are not shutting down
    while (!closing_port_) {
        if (!is_open())
            std::this_thread::sleep_for(std::chrono::seconds(5));
        else if (job_queue_empty())
            std::this_thread::sleep_for(std::chrono::seconds(1));
        else {
            // get next job
            datum d = write_queue_.front();
            write_queue_.pop();

            // issue write
            if (!WriteFile(pimpl_->handle_, d.data_, d.length_, &bytes_written, &overlapped_write)) {
                if (GetLastError() == ERROR_IO_PENDING) {

                    // write is delayed
                    DWORD wait_result = WaitForSingleObject(overlapped_write.hEvent, OVERLAPPED_CHECK_TIMEOUT);
                    switch (wait_result)
                    {
                        // write event set
                    case WAIT_OBJECT_0:
                        SetLastError(ERROR_SUCCESS);
                        if (!GetOverlappedResult(pimpl_->handle_, &overlapped_write, &bytes_written, FALSE)) {
                            if (GetLastError() != ERROR_OPERATION_ABORTED) {
                                on_error(system_error());
                            }
                        }

                        if (bytes_written != d.length_) {
                            on_error(system_error());
                        }
                        else {
                            // success - we have written data ok
                            on_write();
                        }
                        break;

                        // wait timed out
                    case WAIT_TIMEOUT:
                        // timeout expected
                        break;

                    case WAIT_FAILED:
                    default:
                        on_error("WaitForSingleObject WAIT_FAILED in writer_thread_function");
                        break;
                    }
                    // deallocate data memory
                    delete[] d.data_;
                }
                else
                    // writefile failed - but not dut to ERROR_OPERATION_ABORTED
                    on_error(system_error());
            }
            else {
                // writefile returned immediately
                if (bytes_written != d.length_) {
                    on_error(system_error());
                }
                else {
                    // success - we have written data ok
                    on_write();
                }
            }
        }
    }
    // don't leak event handle
    CloseHandle(overlapped_write.hEvent);
}

std::string async_serial::get_status_string() {

    std::string state;
    if (status_ & (1UL << LS_CTS)) {
        state += "CTS,";
    }
    if (status_ & (1UL << LS_DSR)) {
        state += "DSR,";
    }
    if (status_ & (1UL << LS_CD)) {
        state += "CD,";
    }
    if (status_ & (1UL << LS_BREAK)) {
        state += "BREAK,";
    }
    if (status_ & (1UL << LS_ERR)) {
        state += "ERROR,";
    }
    if (status_ & (1UL << LS_RING)) {
        state += "RING,";
    }
    if (state.size() > 0 && state[state.size() - 1] == ',') {
        state.erase(state.rfind(','));
    }
    return state;
}

void async_serial::update_pin_states() {
    // GetCommModemStatus provides cts, dsr, ring and cd states
    // if status checks are not allowed, then don't issue the
    // modem status check nor the com stat check
    DWORD modem_status;
    if (!GetCommModemStatus(pimpl_->handle_, &modem_status))
    {
        on_error(create_error_msg("serial::update_pin_states GetCommModemStatus error "));
    }
    else {

        const std::pair<unsigned, unsigned> mapping_list[] = { 
            { MS_CTS_ON, LS_CTS }, { MS_DSR_ON, LS_DSR }, { MS_RING_ON, LS_RING }, { MS_RLSD_ON, LS_CD } };

        for (const auto& item : mapping_list) {
            bool is_set = (item.first & modem_status) == item.first;
            if (is_set != ((status_ >> item.second) & 1U)) {
                // update status_
                is_set != 0 ? status_ |= (1 << item.second) : status_ &= ~(1 << item.second);
                on_status(item.second, is_set != 0 ? true : false);  // notify state change
            }
        }
    }
}

void async_serial::reader_thread_func() {
    DWORD bytes_read;
    // flip between waiting on read and waiting on status events
    BOOL wait_on_read = FALSE;    // waiting on read
    BOOL wait_on_status = FALSE;  // waiting on line status events
    OVERLAPPED overlapped_reader = { 0 };  // overlapped structure for read operations
    OVERLAPPED overlapped_status = { 0 };  // overlapped structure for status operations
    HANDLE event_array[2];  // list of events that WaitForxxx functions wait on
    char read_buffer[READ_BUFFER_SIZE];  // temp storage of read data
    DWORD comms_event; // result from WaitCommEvent

    // Create the overlapped event. Must be closed before exiting
    // to avoid a handle leak.
    overlapped_reader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (overlapped_reader.hEvent == NULL)
    {
        on_error("Error creating reader overlapped event");
        return;
    }
    overlapped_status.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (overlapped_status.hEvent == NULL)
    {
        on_error("Error creating status overlapped event");
        return;
    }
    // array of handles to wait for signals - we wait on read and status events
    event_array[0] = overlapped_reader.hEvent;
    event_array[1] = overlapped_status.hEvent;

    // keep thread running until shutting down flag set
    while (!closing_port_) {
        if (!is_open())
            std::this_thread::sleep_for(std::chrono::seconds(1));
        else {

            if (!wait_on_read) {
                // issue read operation
                if (!ReadFile(pimpl_->handle_, read_buffer, READ_BUFFER_SIZE, &bytes_read, &overlapped_reader)) {
                    if (GetLastError() != ERROR_IO_PENDING) { // read not delayed
                        on_error(system_error());
                        break;
                    } 
                    else
                        wait_on_read = TRUE;
                }
                else {
                    on_read(read_buffer, bytes_read);
                }
            }

            // if no status check is outstanding, then issue another one
            if (!wait_on_status) {
                if (!WaitCommEvent(pimpl_->handle_, &comms_event, &overlapped_status)) {
                    if (GetLastError() == ERROR_IO_PENDING) {
                        wait_on_status = TRUE;  // have to wait for status event
                    } else {
                        on_error(create_error_msg("WaitCommEvent error "));
                    }
                } else {
                    // WaitCommEvent returned immediately
                    update_error_states(comms_event);
                }
            }

            // wait for pending operations to complete
            if (wait_on_status || wait_on_read) {
                DWORD wait_result = WaitForMultipleObjects(2, event_array, FALSE, OVERLAPPED_CHECK_TIMEOUT);
                switch (wait_result)
                {
                // read completed
                case WAIT_OBJECT_0:
                    if (!GetOverlappedResult(pimpl_->handle_, &overlapped_reader, &bytes_read, FALSE)) {
                        if (GetLastError() == ERROR_OPERATION_ABORTED) {
                            on_error("Read aborted");
                            wait_on_read = FALSE;
                        }
                        else {
                            on_error(create_error_msg("GetOverlappedResult (in reader_thread_func) error "));
                        }
                    }
                    else {      
                        // read completed successfully
                        if (bytes_read) {
                            on_read(read_buffer, bytes_read);
                        }
                    }

                    wait_on_read = FALSE;

                break;

                // status completed
                case WAIT_OBJECT_0 + 1:
                {   
                    DWORD overlapped_result;
                    if (!GetOverlappedResult(pimpl_->handle_, &overlapped_status, &overlapped_result, FALSE)) {
                        if (GetLastError() == ERROR_OPERATION_ABORTED) {
                            on_error("WaitCommEvent aborted");
                        } else {
                            on_error(create_error_msg("GetOverlappedResult for status wait returned "));
                        }
                }
                else {      
                    // status check completed successfully
                    update_error_states(comms_event);
                    update_pin_states();
                }

                wait_on_status = FALSE;
                }
                break;
                case WAIT_TIMEOUT:
                    update_pin_states();  // periodically update pin states if changed
                    break;

                default:
                    break;
                }
            }
        }
    }
    // close reader event handle - otherwise will get resource leak
    CloseHandle(overlapped_reader.hEvent);
}

async_serial::~async_serial() {

    // if port is not open then reader and writer threads not running
    close();

    delete pimpl_;
}

bool async_serial::open() {

    // in case we are already open
    close();

    closing_port_ = false;

    // The "\\.\" prefix form to access the Win32 device namespace must be used for com ports above COM9
    char portname[100] = { 0 };
    _snprintf_s(portname, sizeof(portname), sizeof(portname) - 1, "\\\\.\\COM%d", port_);
    pimpl_->handle_ = CreateFileA(portname, GENERIC_READ | GENERIC_WRITE, 0, 0,
        OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0);

    if (pimpl_->handle_ == INVALID_HANDLE_VALUE)
        return false;

    // Save original comm timeouts and set new ones
    COMMTIMEOUTS origtimeouts;
    if (!GetCommTimeouts(pimpl_->handle_, &origtimeouts)) {
        on_error(create_error_msg("GetCommTimeouts error "));
    }

    if (!configure_comport()) {
        on_error("failed to configure serial port");
    }

    // set size of modem driver read and write buffers
    if (!SetupComm(pimpl_->handle_, READ_BUFFER_SIZE, READ_BUFFER_SIZE)) {
        on_error(create_error_msg("SetupComm error: "));
    }

    // Sends the DTR (data-terminal-ready) signal
    if (!EscapeCommFunction(pimpl_->handle_, SETDTR)) {
        on_error(create_error_msg("EscapeCommFunction(SETDTR) error: "));
    }

    // WaitCommEvent, in reader_thread_func, will monitor for these events
    if (!SetCommMask(pimpl_->handle_, EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING | EV_RLSD)) {
        on_error(create_error_msg("SetCommMask error: "));
    }

    reader_thread = std::thread(&async_serial::reader_thread_func, this);
    writer_thread = std::thread(&async_serial::writer_thread_func, this);

    std::this_thread::yield();

    return pimpl_->handle_ != INVALID_HANDLE_VALUE;
}

inline bool async_serial::is_open() const {
    return pimpl_->handle_ != INVALID_HANDLE_VALUE;
}

bool async_serial::close() {

    if (is_open()) {

        closing_port_ = true;

        // give the reader and writer threads time to finish
        std::this_thread::sleep_for(std::chrono::seconds(2));

        if (reader_thread.joinable())
            reader_thread.join();  // prevents crash - due to terminate being called on running thread still 'alive'

        if (writer_thread.joinable())
            writer_thread.join();

        BOOL b = CloseHandle(pimpl_->handle_);
        pimpl_->handle_ = INVALID_HANDLE_VALUE;
        return b == TRUE ? true : false;
    }
    else {
        return false;
    }
}

bool async_serial::write(const char* data, size_t length) {

    // add to writer queue
    char* cpy = new char[length]();
    memcpy(cpy, data, length);
    write_queue_.push(datum(cpy, length));
    return true;
}

void async_serial::on_read(char* data, size_t length) {
    // override this function to handle received data
}

void async_serial::on_write() {
    // override this function to identify when data successfully sent
}

void async_serial::on_status(const unsigned statechange, bool set) {
    // override this function to see line status changes
}

void async_serial::on_error(const std::string& error) {
    // override this function for diagnostic information
}

void async_serial::update_error_states(unsigned long comm_event) {

    // cache previous status for comparison
    unsigned prev_status = status_;
    if (comm_event & EV_BREAK) {
        on_error("BREAK signal received");

        status_ |= (1UL << LS_BREAK);
        if (!((prev_status >> LS_BREAK) & 1U)) {
            on_status(LS_BREAK, true);
        }
    }
    else {
        on_error("BREAK signal reset");
        status_ &= ~(1UL << LS_BREAK);

        if (((prev_status >> LS_BREAK) & 1U)) {
            on_status(LS_BREAK, false);
        }
    }

    // A line-status error occurred. Line-status errors are CE_FRAME, CE_OVERRUN, and CE_RXPARITY.
    if (comm_event & EV_ERR) {
        on_error("A line status error has occurred");

        status_ |= (1UL << LS_ERR);

        if (!((prev_status >> LS_ERR) & 1U)) {
            on_status(LS_ERR, true);
        }

        COMSTAT ComStatNew;
        DWORD dwErrors;
        if (!ClearCommError(pimpl_->handle_, &dwErrors, &ComStatNew))
            on_error("ErrorReporter(\"ClearCommError\")");

        if (ComStatNew.fCtsHold)
            on_error("waiting for the CTS(clear - to - send) signal to be sent.");

        if (ComStatNew.fDsrHold)
            on_error("waiting for the DSR (data-set-ready) signal to be sent.");
        if (ComStatNew.fRlsdHold)
            on_error("waiting for the  RLSD (receive-line-signal-detect) aka CD (Carrier Detect) signal to be sent.");

        if (ComStatNew.fXoffHold)
            on_error("waiting because an XOFF character was received.");

        if (ComStatNew.fXoffSent)
            on_error("Transmission waiting because an XOFF character was sent.");

        if (ComStatNew.fEof)
            on_error("An end-of-file (EOF) character has been received.");
    }
    else {
        on_error("A line status error cleared");

        status_ &= ~(1UL << LS_ERR);

        if (((prev_status >> LS_ERR) & 1U)) {
            on_status(LS_ERR, false);
        }
    }
}

Test program, main.cpp:

#include <iostream>
#include <unordered_map>
#include <string>
#include <chrono>
#include <thread>

#include "async_serial.hpp"

static void print_byte_as_binary(unsigned n, unsigned numbits) {
    for (int i = numbits; i >= 0; i--) {
        std::cout << (n & (1 << i) ? '1' : '0');
    }
}

class modem_tester : public utl::async_serial {
public:
    modem_tester(int port, unsigned baudrate) : utl::async_serial(port, baudrate) {

    }
    modem_tester(int port, utl::port_settings settings) : utl::async_serial(port, settings) {

    }

    virtual bool open() {
        return utl::async_serial::open();
    }

    // bytes read
    virtual void on_read(char* data, size_t length) {
        std::string s(data, length);
        std::cout << "data: " << s;
    }
    // bytes written
    virtual void on_write() {
        std::cout << "on_write()\n";
    }
    // Line status change detected
    virtual void on_status(const unsigned statechange, bool set = true) {
        std::cout << "on_status bit: " << statechange << " " << (set ? "set" : "not set") << std::endl;
    }

    virtual void on_error(const std::string& error) {
        std::cout << "on_error: " << error << std::endl;
    }
};

int main() {

    {
    // get list of ports available on system
    std::unordered_map <uint32_t, std::string> ports;
    utl::async_serial::enumerate_ports(ports);

    for (const auto& item : ports) {
        std::cout << item.first << "->" << item.second << std::endl;
    }

    std::cout << "From list above select port number (number before arrow)\n";
    int port;
    std::cin >> port;
    std::cin.ignore(1, '\n');  // ignore newline received after number entered
    // verify a valid port selected
    if (ports.find(port) == ports.end()) {
        std::cout << "You have selected a port that doesn't exist on this system. Aborting...\n";
        return 0;
    }

    utl::port_settings ps(1200);
    ps.databits_ = utl::DATA_8;
    ps.flowcontrol_ = utl::FLOWCONTROL_HARDWARE_RTSCTS;
    //ps.flowcontrol = utl::FLOWCONTROL_XONXOFF;  // select if hardware flow control not possible
    ps.parity_type_ = utl::PARITYTYPE_NONE;
    ps.stopbits_ = utl::STOPBIT_1;

    modem_tester s(port, 1200);

    bool ret = s.open();
    std::cout << "open(" << port << ") returned " << std::boolalpha << ret << std::endl;

    const char* commands[] = { 
        "ATQ0V1E0\r\n",  // initialise the query, Q0=enable result codes, V1=verbal result codes, E0=disable command echo
        "AT+GMM\r\n",  // Request Model Indentification
        "AT+FCLASS=?\r\n", // list of supported modes (data, fax, voice)
        "AT#CLS=?\r\n",   // not sure, I think similar to above command (CLS being shorthand for class?)
        "AT+GCI?\r\n", // currently selected country code (US=B5)
        "AT+GCI=?\r\n", // list of supported country codes
        "ATI0\r\n", "ATI1\r\n", "ATI2\r\n", "ATI3\r\n", "ATI4\r\n", "ATI5\r\n", "ATI6\r\n", "ATI7\r\n", // Identification requests
        "AT+GMI\r\n",  // Request Manufacturer Identification
        "AT+GMI9\r\n",  // Request Conexant Identification
        "AT+GCAP\r\n" }; // Request complete capabilities list

    for (const auto& cmd : commands) {
        std::cout << "Sending: " << cmd;
        s.write(cmd, strlen(cmd));
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }

    // Give tester 10 seconds to ring modem - check for RING indicator - LS_RING
    std::this_thread::sleep_for(std::chrono::seconds(10));

    print_byte_as_binary(s.get_status(), 6);
    std::cout << '\n' << s.get_status_string() << '\n';

    ret = s.close();

    std::cout << "close() returned " << std::boolalpha << ret << std::endl;

    } // out of scope - so can check destructor
    std::cout << "finished\n";
}

Example output using a Conexant voice modem:

1->Communications Port (COM1)
4->SAMSUNG Mobile USB Modem
12->Conexant USB CX93010 ACF Modem
From list above select port number (number before arrow)
12
open(12) returned true
on_status bit: 0 set
on_status bit: 1 set
on_status bit: 5 set
Sending: ATQ0V1E0
on_write()
data:
OK
Sending: AT+GMM
on_write()
data:
+GMM: V90

OK
Sending: AT+FCLASS=?
on_write()
data:
0,1,1.0,2,8

OK
Sending: AT#CLS=?
on_write()
data:
ERROR
Sending: AT+GCI?
on_write()
data:
+GCI: B4

OK
Sending: AT+GCI=?
on_write()
data:
+GCI: (00,07,09,0A,0F,16,1B,20,25,26,27,2D,2E,31,36,3C,3D,42,46,50,51,52,53,54,5
7,58,59,61,62,64,69,6C,73,77,7B,7E,82,84,89,8A,8B,8E,98,99,9C,9F,A0,A1,A5,A6,A9,
AD,AE,B3,B4,B5,B7,B8,C1,F9,FA,FB,FC,FD,FE)

OK
Sending: ATI0
on_write()
data:
56000

OK
Sending: ATI1
on_write()
data:
OK
Sending: ATI2
on_write()
data:
OK
Sending: ATI3
on_write()
data:
CX93001-EIS_V0.2013-V92

OK
Sending: ATI4
on_write()
data:
OK
Sending: ATI5
on_write()
data:
B4

OK
Sending: ATI6
on_write()
data:
OK
Sending: ATI7
on_write()
data:
OK
Sending: AT+GMI
on_write()
data:
+GMI: CONEXANT

OK
Sending: AT+GMI9
on_write()
data:
+GMI9: CONEXANT ACF

OK
Sending: AT+GCAP
on_write()
data:
+GCAP: +FCLASS,+MS,+ES,+DS

OK
0100011
CTS,DSR,RING
close() returned true
on_error: Read aborted
finished
\$\endgroup\$
  • \$\begingroup\$ this is not asynchronous code at all. asynchronous code not used dedicated threads for io operations, loops and main wait for io complete just after it begin. for real asynchronous code you need or use apc completion. if you work with single thread and wait in alertable state. or bind your file handle to iocp. use WMI extremally not efficient and here no sense. use config api for enumerate serial ports. not use hardcoded "\\\\.\\COM%d". many code simply not multithreaded safe like is_open. all program logic very complex and not clean, due wrong asynchronous io implementation \$\endgroup\$ – RbMm Mar 27 at 19:51
  • \$\begingroup\$ @RbMm the way serial comms is handled in the code seems to be asynchronous according to this definition: stackify.com/when-to-use-asynchronous-programming How do you think this code is not asynchronous? \$\endgroup\$ – arcomber Mar 28 at 11:29
  • \$\begingroup\$ @RbMm Are you suggesting to use iocp? Do you have an example for serial comms? Ideally a code review. \$\endgroup\$ – arcomber Mar 28 at 11:35
  • \$\begingroup\$ if you begin asynchronous I/O and then just call WaitForSingleObject - this is already not asynchronous I/O by sense. by fact synchronous I/O - when your request sent to driver and then if pending returned - code is wait in kernel for complete. you simply move wait from kernel to user mode. if you use dedicated threads and loops - this is already not asynchronous I/O by sense, even if you formally use it. asynchronous I/O - this is when you begin it - and not wait after, and not do any loops. all program structure must be absolute another. \$\endgroup\$ – RbMm Mar 28 at 11:41
  • \$\begingroup\$ asynchronous I/O - this is when you do i/o request and continue do another tasks. asynchronous I/O is event driven. you must have loop for got events - from user GUI, console, iocp. only this loop. why you say decide write to port ? you got some event - from user, another src, etc. and on this event - you write data to port. and after this - you return to code which wait for new event, instead wait write complete. after you open port - you call read and return to your tasks. not wait in any place. when read complete - you begin new read, and return. this is asynchronous I/O \$\endgroup\$ – RbMm Mar 28 at 11:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.