diff --git a/CMakeLists.txt b/CMakeLists.txt index cf413d0..1044f5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,11 @@ mark_as_advanced(serial_cpp_INSTALL) option(serial_cpp_FORCE_RESPECT_BUILD_TESTING "If also BUILD_TESTING is ON, enable tests even if project was included via add_subdirectory/FetchContent" OFF) mark_as_advanced(serial_cpp_FORCE_RESPECT_BUILD_TESTING) +# Windows async (overlapped) I/O option +# When ON, uses FILE_FLAG_OVERLAPPED allowing concurrent read/write from different threads. +# When OFF (default), uses the original blocking synchronous implementation. +option(SERIAL_CPP_WIN_ASYNC "Use overlapped (async) I/O on Windows for non-blocking concurrent read/write" OFF) + # Detect if project is top level or not (fall back for CMake < 3.21) if(CMAKE_VERSION VERSION_LESS "3.21.0") if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) @@ -54,7 +59,11 @@ elseif(UNIX) list(APPEND serial_cpp_SRCS src/impl/list_ports/list_ports_linux.cc) else() # If windows - list(APPEND serial_cpp_SRCS src/impl/win.cc) + if(SERIAL_CPP_WIN_ASYNC) + list(APPEND serial_cpp_SRCS src/impl/win_async.cc) + else() + list(APPEND serial_cpp_SRCS src/impl/win.cc) + endif() list(APPEND serial_cpp_SRCS src/impl/list_ports/list_ports_win.cc) endif() @@ -81,11 +90,27 @@ if(WIN32 AND BUILD_SHARED_LIBS) set_target_properties(${PROJECT_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() -## Uncomment for example +## Examples add_executable(serial_cpp_example examples/serial_cpp_example.cc) add_dependencies(serial_cpp_example ${PROJECT_NAME}::${PROJECT_NAME}) target_link_libraries(serial_cpp_example ${PROJECT_NAME}::${PROJECT_NAME}) +## Async test example (demonstrates concurrent read/write) +add_executable(serial_cpp_async_test examples/serial_cpp_async_test.cc) +add_dependencies(serial_cpp_async_test ${PROJECT_NAME}::${PROJECT_NAME}) +target_link_libraries(serial_cpp_async_test ${PROJECT_NAME}::${PROJECT_NAME}) +if(WIN32) + + + if(SERIAL_CPP_WIN_ASYNC) + target_compile_definitions(${PROJECT_NAME} PRIVATE -DSERIAL_CPP_WIN_ASYNC) + endif() + + target_link_libraries(serial_cpp_async_test Threads::Threads) +endif() +find_package(Threads REQUIRED) +target_link_libraries(serial_cpp_async_test Threads::Threads) + ## Install library if(serial_cpp_INSTALL) install(TARGETS ${PROJECT_NAME} diff --git a/examples/serial_cpp_async_test.cc b/examples/serial_cpp_async_test.cc new file mode 100644 index 0000000..e6445e8 --- /dev/null +++ b/examples/serial_cpp_async_test.cc @@ -0,0 +1,144 @@ +#include +#include +#include +#include +#include +#include + +#include "serial_cpp/serial.h" + +// Test program to demonstrate concurrent read/write with the async Windows implementation. +// Build with: cmake -B build -DSERIAL_CPP_WIN_ASYNC=ON && cmake --build build +// +// Usage: serial_cpp_async_test [baudrate] +// Example: serial_cpp_async_test COM3 115200 +// +// This test spawns a reader thread and a writer thread that operate simultaneously. +// With the blocking implementation, the writer would stall while the reader is +// waiting for data. With the async implementation, both proceed independently. + +std::atomic running{true}; + +void reader_thread(serial_cpp::Serial &serial) +{ + std::cout << "[Reader] Started. Waiting for incoming data...\n"; + while (running) { + try { + std::string data = serial.read(256); + if (!data.empty()) { + std::cout << "[Reader] Received " << data.size() << " bytes: "; + // Print as hex for binary data, or as string if printable + bool printable = true; + for (char c : data) { + if (c < 0x20 && c != '\n' && c != '\r' && c != '\t') { + printable = false; + break; + } + } + if (printable) { + std::cout << "\"" << data << "\""; + } else { + for (unsigned char c : data) { + printf("%02X ", c); + } + } + std::cout << "\n"; + } + } catch (const serial_cpp::IOException &e) { + std::cerr << "[Reader] IOException: " << e.what() << "\n"; + } catch (const serial_cpp::PortNotOpenedException &e) { + std::cerr << "[Reader] Port not open: " << e.what() << "\n"; + break; + } + } + std::cout << "[Reader] Stopped.\n"; +} + +void writer_thread(serial_cpp::Serial &serial) +{ + std::cout << "[Writer] Started. Sending periodic messages...\n"; + int counter = 0; + while (running) { + try { + std::string msg = "Hello #" + std::to_string(counter++) + "\r\n"; + size_t written = serial.write(msg); + std::cout << "[Writer] Sent " << written << " bytes: \"" << msg.substr(0, msg.size() - 2) << "\"\n"; + } catch (const serial_cpp::IOException &e) { + std::cerr << "[Writer] IOException: " << e.what() << "\n"; + } catch (const serial_cpp::PortNotOpenedException &e) { + std::cerr << "[Writer] Port not open: " << e.what() << "\n"; + break; + } + + // Send every 1 second + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + std::cout << "[Writer] Stopped.\n"; +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " [baudrate]\n"; + std::cerr << "Example: " << argv[0] << " COM3 115200\n"; + std::cerr << "\nAvailable ports:\n"; + std::vector ports = serial_cpp::list_ports(); + for (const auto &port : ports) { + std::cout << " " << port.port << " - " << port.description << "\n"; + } + return 1; + } + + std::string port_name = argv[1]; + unsigned long baudrate = 115200; + if (argc >= 3) { + baudrate = std::stoul(argv[2]); + } + + std::cout << "=== serial_cpp Async I/O Test ===\n"; + std::cout << "Port: " << port_name << "\n"; + std::cout << "Baudrate: " << baudrate << "\n"; + std::cout << "Press Enter to stop...\n\n"; + + try { + // Set a short read timeout so the reader doesn't block forever + serial_cpp::Timeout timeout = serial_cpp::Timeout::simpleTimeout(100); + + serial_cpp::Serial serial(port_name, baudrate, timeout); + + if (!serial.isOpen()) { + std::cerr << "Error: Failed to open port " << port_name << "\n"; + return 1; + } + + std::cout << "Port opened successfully.\n\n"; + + // Launch reader and writer threads concurrently + // With the blocking implementation, the writer would be stuck waiting + // for the reader's ReadFile to complete/timeout before it can WriteFile. + // With SERIAL_CPP_WIN_ASYNC=ON, both operate independently. + std::thread reader(reader_thread, std::ref(serial)); + std::thread writer(writer_thread, std::ref(serial)); + + // Wait for Enter key to stop + std::cin.get(); + running = false; + + std::cout << "\nStopping...\n"; + + reader.join(); + writer.join(); + + serial.close(); + std::cout << "Port closed. Done.\n"; + + } catch (const serial_cpp::IOException &e) { + std::cerr << "IOException: " << e.what() << "\n"; + return 1; + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; + } + + return 0; +} diff --git a/include/serial_cpp/impl/win_async.h b/include/serial_cpp/impl/win_async.h new file mode 100644 index 0000000..04774b6 --- /dev/null +++ b/include/serial_cpp/impl/win_async.h @@ -0,0 +1,220 @@ +/*! + * \file serial/impl/win_async.h + * \author William Woodall + * \author John Harrison + * \version 0.1 + * + * \section LICENSE + * + * The MIT License + * + * Copyright (c) 2012 William Woodall, John Harrison + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * \section DESCRIPTION + * + * This provides a Windows implementation of the Serial class interface using + * overlapped (asynchronous) I/O. This allows concurrent read/write operations + * from different threads without blocking each other. + * + * Enable by setting SERIAL_CPP_WIN_ASYNC=ON in CMake. + * + */ + +#if defined(_WIN32) + +#ifndef SERIAL_IMPL_WINDOWS_ASYNC_H +#define SERIAL_IMPL_WINDOWS_ASYNC_H + +#include "serial_cpp/serial.h" + +#include "windows.h" + +namespace serial_cpp { + +using std::string; +using std::wstring; +using std::invalid_argument; + +using serial_cpp::SerialException; +using serial_cpp::IOException; + +class serial_cpp::Serial::SerialImpl { +public: + SerialImpl (const string &port, + unsigned long baudrate, + bytesize_t bytesize, + parity_t parity, + stopbits_t stopbits, + flowcontrol_t flowcontrol); + + virtual ~SerialImpl (); + + void + open (); + + void + close (); + + bool + isOpen () const; + + size_t + available (); + + bool + waitReadable (uint32_t timeout); + + void + waitByteTimes (size_t count); + + size_t + read (uint8_t *buf, size_t size = 1); + + size_t + write (const uint8_t *data, size_t length); + + void + flush (); + + void + flushInput (); + + void + flushOutput (); + + void + sendBreak (int duration); + + void + setBreak (bool level); + + void + setRTS (bool level); + + void + setDTR (bool level); + + bool + waitForChange (); + + bool + getCTS (); + + bool + getDSR (); + + bool + getRI (); + + bool + getCD (); + + void + setPort (const string &port); + + string + getPort () const; + + void + setTimeout (const Timeout &timeout); + + Timeout + getTimeout () const; + + void + setBaudrate (unsigned long baudrate); + + unsigned long + getBaudrate () const; + + void + setBytesize (bytesize_t bytesize); + + bytesize_t + getBytesize () const; + + void + setParity (parity_t parity); + + parity_t + getParity () const; + + void + setStopbits (stopbits_t stopbits); + + stopbits_t + getStopbits () const; + + void + setFlowcontrol (flowcontrol_t flowcontrol); + + flowcontrol_t + getFlowcontrol () const; + + void + readLock (); + + void + readUnlock (); + + void + writeLock (); + + void + writeUnlock (); + +protected: + void reconfigurePort (); + +private: + wstring port_; // Path to the file descriptor + HANDLE fd_; + + bool is_open_; + + Timeout timeout_; // Timeout for read operations + unsigned long baudrate_; // Baudrate + + parity_t parity_; // Parity + bytesize_t bytesize_; // Size of the bytes + stopbits_t stopbits_; // Stop Bits + flowcontrol_t flowcontrol_; // Flow Control + + // Mutex used to lock the read functions + HANDLE read_mutex; + // Mutex used to lock the write functions + HANDLE write_mutex; + + // Events for overlapped I/O (persistent, one per operation type) + HANDLE read_event_; + HANDLE write_event_; + HANDLE wait_event_; + + // WaitCommEvent writes here asynchronously - MUST be a class member, + // NOT a stack variable, because the kernel writes to it after the call returns. + DWORD wait_comm_event_mask_; +}; + +} + +#endif // SERIAL_IMPL_WINDOWS_ASYNC_H + +#endif // if defined(_WIN32) diff --git a/src/impl/win.cc b/src/impl/win.cc index 7e158ab..198c9a1 100644 --- a/src/impl/win.cc +++ b/src/impl/win.cc @@ -643,4 +643,3 @@ Serial::SerialImpl::writeUnlock() } #endif // #if defined(_WIN32) - diff --git a/src/impl/win_async.cc b/src/impl/win_async.cc new file mode 100644 index 0000000..f2b7453 --- /dev/null +++ b/src/impl/win_async.cc @@ -0,0 +1,873 @@ +#if defined(_WIN32) + +/* Copyright 2012 William Woodall and John Harrison */ +/* Overlapped (async) I/O implementation for Windows */ + +#include +#include + +#include "serial_cpp/impl/win_async.h" + +using std::string; +using std::wstring; +using std::stringstream; +using std::invalid_argument; +using serial_cpp::Serial; +using serial_cpp::Timeout; +using serial_cpp::bytesize_t; +using serial_cpp::parity_t; +using serial_cpp::stopbits_t; +using serial_cpp::flowcontrol_t; +using serial_cpp::SerialException; +using serial_cpp::PortNotOpenedException; +using serial_cpp::IOException; + +inline wstring +_prefix_port_if_needed_async(const wstring &input) +{ + static wstring windows_com_port_prefix = L"\\\\.\\"; + if (input.compare(0, windows_com_port_prefix.size(), windows_com_port_prefix) != 0) + { + return windows_com_port_prefix + input; + } + return input; +} + +Serial::SerialImpl::SerialImpl (const string &port, unsigned long baudrate, + bytesize_t bytesize, + parity_t parity, stopbits_t stopbits, + flowcontrol_t flowcontrol) + : port_ (port.begin(), port.end()), fd_ (INVALID_HANDLE_VALUE), is_open_ (false), + baudrate_ (baudrate), parity_ (parity), + bytesize_ (bytesize), stopbits_ (stopbits), flowcontrol_ (flowcontrol), + read_event_(NULL), write_event_(NULL), wait_event_(NULL), + wait_comm_event_mask_(0) +{ + // Create manual-reset events for overlapped I/O + read_event_ = CreateEvent(NULL, TRUE, FALSE, NULL); + write_event_ = CreateEvent(NULL, TRUE, FALSE, NULL); + wait_event_ = CreateEvent(NULL, TRUE, FALSE, NULL); + + if (!read_event_ || !write_event_ || !wait_event_) { + if (read_event_) CloseHandle(read_event_); + if (write_event_) CloseHandle(write_event_); + if (wait_event_) CloseHandle(wait_event_); + throw SerialException("Failed to create overlapped events."); + } + + read_mutex = CreateMutex(NULL, false, NULL); + write_mutex = CreateMutex(NULL, false, NULL); + + if (port_.empty () == false) + open (); +} + +Serial::SerialImpl::~SerialImpl () +{ + this->close(); + CloseHandle(read_mutex); + CloseHandle(write_mutex); + if (read_event_) CloseHandle(read_event_); + if (write_event_) CloseHandle(write_event_); + if (wait_event_) CloseHandle(wait_event_); +} + +void +Serial::SerialImpl::open () +{ + if (port_.empty ()) { + throw invalid_argument ("Empty port is invalid."); + } + if (is_open_ == true) { + throw SerialException ("Serial port already open."); + } + + // See: https://github.com/wjwwood/serial/issues/84 + wstring port_with_prefix = _prefix_port_if_needed_async(port_); + LPCWSTR lp_port = port_with_prefix.c_str(); + fd_ = CreateFileW(lp_port, + GENERIC_READ | GENERIC_WRITE, + 0, + 0, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + 0); + + if (fd_ == INVALID_HANDLE_VALUE) { + DWORD create_file_err = GetLastError(); + stringstream ss; + switch (create_file_err) { + case ERROR_FILE_NOT_FOUND: + ss << "Specified port, " << this->getPort() << ", does not exist."; + THROW (IOException, ss.str().c_str()); + default: + ss << "Unknown error opening the serial port: " << create_file_err; + THROW (IOException, ss.str().c_str()); + } + } + + reconfigurePort(); + is_open_ = true; +} + +void +Serial::SerialImpl::reconfigurePort () +{ + if (fd_ == INVALID_HANDLE_VALUE) { + THROW (IOException, "Invalid file descriptor, is the serial port open?"); + } + + DCB dcbSerialParams = {0}; + + dcbSerialParams.DCBlength=sizeof(dcbSerialParams); + + if (!GetCommState(fd_, &dcbSerialParams)) { + THROW (IOException, "Error getting the serial port state."); + } + + // setup baud rate + switch (baudrate_) { +#ifdef CBR_0 + case 0: dcbSerialParams.BaudRate = CBR_0; break; +#endif +#ifdef CBR_50 + case 50: dcbSerialParams.BaudRate = CBR_50; break; +#endif +#ifdef CBR_75 + case 75: dcbSerialParams.BaudRate = CBR_75; break; +#endif +#ifdef CBR_110 + case 110: dcbSerialParams.BaudRate = CBR_110; break; +#endif +#ifdef CBR_134 + case 134: dcbSerialParams.BaudRate = CBR_134; break; +#endif +#ifdef CBR_150 + case 150: dcbSerialParams.BaudRate = CBR_150; break; +#endif +#ifdef CBR_200 + case 200: dcbSerialParams.BaudRate = CBR_200; break; +#endif +#ifdef CBR_300 + case 300: dcbSerialParams.BaudRate = CBR_300; break; +#endif +#ifdef CBR_600 + case 600: dcbSerialParams.BaudRate = CBR_600; break; +#endif +#ifdef CBR_1200 + case 1200: dcbSerialParams.BaudRate = CBR_1200; break; +#endif +#ifdef CBR_1800 + case 1800: dcbSerialParams.BaudRate = CBR_1800; break; +#endif +#ifdef CBR_2400 + case 2400: dcbSerialParams.BaudRate = CBR_2400; break; +#endif +#ifdef CBR_4800 + case 4800: dcbSerialParams.BaudRate = CBR_4800; break; +#endif +#ifdef CBR_7200 + case 7200: dcbSerialParams.BaudRate = CBR_7200; break; +#endif +#ifdef CBR_9600 + case 9600: dcbSerialParams.BaudRate = CBR_9600; break; +#endif +#ifdef CBR_14400 + case 14400: dcbSerialParams.BaudRate = CBR_14400; break; +#endif +#ifdef CBR_19200 + case 19200: dcbSerialParams.BaudRate = CBR_19200; break; +#endif +#ifdef CBR_28800 + case 28800: dcbSerialParams.BaudRate = CBR_28800; break; +#endif +#ifdef CBR_57600 + case 57600: dcbSerialParams.BaudRate = CBR_57600; break; +#endif +#ifdef CBR_76800 + case 76800: dcbSerialParams.BaudRate = CBR_76800; break; +#endif +#ifdef CBR_38400 + case 38400: dcbSerialParams.BaudRate = CBR_38400; break; +#endif +#ifdef CBR_115200 + case 115200: dcbSerialParams.BaudRate = CBR_115200; break; +#endif +#ifdef CBR_128000 + case 128000: dcbSerialParams.BaudRate = CBR_128000; break; +#endif +#ifdef CBR_153600 + case 153600: dcbSerialParams.BaudRate = CBR_153600; break; +#endif +#ifdef CBR_230400 + case 230400: dcbSerialParams.BaudRate = CBR_230400; break; +#endif +#ifdef CBR_256000 + case 256000: dcbSerialParams.BaudRate = CBR_256000; break; +#endif +#ifdef CBR_460800 + case 460800: dcbSerialParams.BaudRate = CBR_460800; break; +#endif +#ifdef CBR_921600 + case 921600: dcbSerialParams.BaudRate = CBR_921600; break; +#endif + default: + dcbSerialParams.BaudRate = baudrate_; + } + + // setup char len + if (bytesize_ == eightbits) + dcbSerialParams.ByteSize = 8; + else if (bytesize_ == sevenbits) + dcbSerialParams.ByteSize = 7; + else if (bytesize_ == sixbits) + dcbSerialParams.ByteSize = 6; + else if (bytesize_ == fivebits) + dcbSerialParams.ByteSize = 5; + else + throw invalid_argument ("invalid char len"); + + // setup stopbits + if (stopbits_ == stopbits_one) + dcbSerialParams.StopBits = ONESTOPBIT; + else if (stopbits_ == stopbits_one_point_five) + dcbSerialParams.StopBits = ONE5STOPBITS; + else if (stopbits_ == stopbits_two) + dcbSerialParams.StopBits = TWOSTOPBITS; + else + throw invalid_argument ("invalid stop bit"); + + // setup parity + if (parity_ == parity_none) { + dcbSerialParams.Parity = NOPARITY; + } else if (parity_ == parity_even) { + dcbSerialParams.Parity = EVENPARITY; + } else if (parity_ == parity_odd) { + dcbSerialParams.Parity = ODDPARITY; + } else if (parity_ == parity_mark) { + dcbSerialParams.Parity = MARKPARITY; + } else if (parity_ == parity_space) { + dcbSerialParams.Parity = SPACEPARITY; + } else { + throw invalid_argument ("invalid parity"); + } + + // setup flowcontrol + if (flowcontrol_ == flowcontrol_none) { + dcbSerialParams.fOutxCtsFlow = false; + dcbSerialParams.fRtsControl = RTS_CONTROL_DISABLE; + dcbSerialParams.fOutX = false; + dcbSerialParams.fInX = false; + } + if (flowcontrol_ == flowcontrol_software) { + dcbSerialParams.fOutxCtsFlow = false; + dcbSerialParams.fRtsControl = RTS_CONTROL_DISABLE; + dcbSerialParams.fOutX = true; + dcbSerialParams.fInX = true; + } + if (flowcontrol_ == flowcontrol_hardware) { + dcbSerialParams.fOutxCtsFlow = true; + dcbSerialParams.fRtsControl = RTS_CONTROL_HANDSHAKE; + dcbSerialParams.fOutX = false; + dcbSerialParams.fInX = false; + } + + // activate settings + if (!SetCommState(fd_, &dcbSerialParams)){ + CloseHandle(fd_); + THROW (IOException, "Error setting serial port settings."); + } + + // Setup timeouts + // With overlapped I/O, COMMTIMEOUTS still apply to the underlying driver. + COMMTIMEOUTS timeouts = {0}; + timeouts.ReadIntervalTimeout = timeout_.inter_byte_timeout; + timeouts.ReadTotalTimeoutConstant = timeout_.read_timeout_constant; + timeouts.ReadTotalTimeoutMultiplier = timeout_.read_timeout_multiplier; + timeouts.WriteTotalTimeoutConstant = timeout_.write_timeout_constant; + timeouts.WriteTotalTimeoutMultiplier = timeout_.write_timeout_multiplier; + if (!SetCommTimeouts(fd_, &timeouts)) { + THROW (IOException, "Error setting timeouts."); + } +} + +void +Serial::SerialImpl::close () +{ + if (is_open_ == true) { + if (fd_ != INVALID_HANDLE_VALUE) { + // Cancel any pending overlapped I/O operations before closing + CancelIoEx(fd_, NULL); + // Wait a bit for cancellations to complete + Sleep(10); + int ret; + ret = CloseHandle(fd_); + if (ret == 0) { + stringstream ss; + ss << "Error while closing serial port: " << GetLastError(); + THROW (IOException, ss.str().c_str()); + } else { + fd_ = INVALID_HANDLE_VALUE; + } + } + is_open_ = false; + } +} + +bool +Serial::SerialImpl::isOpen () const +{ + return is_open_; +} + +size_t +Serial::SerialImpl::available () +{ + if (!is_open_) { + return 0; + } + COMSTAT cs; + DWORD errors; + if (!ClearCommError(fd_, &errors, &cs)) { + stringstream ss; + ss << "Error while checking status of the serial port: " << GetLastError(); + THROW (IOException, ss.str().c_str()); + } + return static_cast(cs.cbInQue); +} + +bool +Serial::SerialImpl::waitReadable (uint32_t timeout) +{ + if (!is_open_) { + throw PortNotOpenedException ("Serial::waitReadable"); + } + + // Check if data is already available + if (available() > 0) { + return true; + } + + // Use WaitCommEvent with overlapped I/O to wait for incoming data + ResetEvent(wait_event_); + + if (!SetCommMask(fd_, EV_RXCHAR)) { + THROW (IOException, "Error setting comm mask for waitReadable."); + } + + OVERLAPPED ov; + memset(&ov, 0, sizeof(ov)); + ov.hEvent = wait_event_; + + wait_comm_event_mask_ = 0; + + if (!WaitCommEvent(fd_, &wait_comm_event_mask_, &ov)) { + DWORD err = GetLastError(); + if (err != ERROR_IO_PENDING) { + // If err == ERROR_INVALID_PARAMETER, the comm mask might have been + // changed by another thread. This is not fatal. + if (err == ERROR_INVALID_PARAMETER) { + return available() > 0; + } + stringstream ss; + ss << "Error in WaitCommEvent: " << err; + THROW (IOException, ss.str().c_str()); + } + // Wait for the event or timeout + DWORD wait_result = WaitForSingleObject(wait_event_, timeout); + if (wait_result == WAIT_OBJECT_0) { + // Check if there's actually data (the event could have fired for other reasons) + return available() > 0; + } else if (wait_result == WAIT_TIMEOUT) { + // Cancel the pending wait + CancelIoEx(fd_, &ov); + // Must wait for cancellation to complete to ensure ov is no longer in use + DWORD dummy; + GetOverlappedResult(fd_, &ov, &dummy, TRUE); + return false; + } else { + CancelIoEx(fd_, &ov); + DWORD dummy; + GetOverlappedResult(fd_, &ov, &dummy, TRUE); + THROW (IOException, "Error waiting for readable event."); + } + } + + // WaitCommEvent completed immediately + return (wait_comm_event_mask_ & EV_RXCHAR) != 0; +} + +void +Serial::SerialImpl::waitByteTimes (size_t count) +{ + if (!is_open_) { + throw PortNotOpenedException ("Serial::waitByteTimes"); + } + // Calculate the time to wait based on baud rate + uint32_t bits_per_byte = 1; // start bit + switch (bytesize_) { + case fivebits: bits_per_byte += 5; break; + case sixbits: bits_per_byte += 6; break; + case sevenbits: bits_per_byte += 7; break; + case eightbits: bits_per_byte += 8; break; + } + if (parity_ != parity_none) + bits_per_byte += 1; + switch (stopbits_) { + case stopbits_one: bits_per_byte += 1; break; + case stopbits_one_point_five: bits_per_byte += 2; break; + case stopbits_two: bits_per_byte += 2; break; + } + + if (baudrate_ > 0) { + DWORD ms = static_cast( + (1000.0 * bits_per_byte * count) / baudrate_ + 0.5); + if (ms > 0) { + Sleep(ms); + } + } +} + +size_t +Serial::SerialImpl::read (uint8_t *buf, size_t size) +{ + if (!is_open_) { + throw PortNotOpenedException ("Serial::read"); + } + + OVERLAPPED ov; + memset(&ov, 0, sizeof(ov)); + ov.hEvent = read_event_; + ResetEvent(read_event_); + + DWORD bytes_read = 0; + if (!ReadFile(fd_, buf, static_cast(size), &bytes_read, &ov)) { + DWORD err = GetLastError(); + if (err != ERROR_IO_PENDING) { + stringstream ss; + ss << "Error while reading from the serial port: " << err; + THROW (IOException, ss.str().c_str()); + } + + // I/O is pending - wait with timeout + DWORD timeout_ms = timeout_.read_timeout_constant + + timeout_.read_timeout_multiplier * static_cast(size); + if (timeout_ms == 0) { + timeout_ms = INFINITE; + } + + DWORD wait_result = WaitForSingleObject(read_event_, timeout_ms); + switch (wait_result) { + case WAIT_OBJECT_0: + // Operation completed + if (!GetOverlappedResult(fd_, &ov, &bytes_read, FALSE)) { + DWORD err2 = GetLastError(); + if (err2 == ERROR_OPERATION_ABORTED) { + // Operation was cancelled (e.g., port closing) + return 0; + } + stringstream ss; + ss << "Error getting overlapped read result: " << err2; + THROW (IOException, ss.str().c_str()); + } + break; + case WAIT_TIMEOUT: + // Timeout - cancel the pending operation and get partial result + CancelIoEx(fd_, &ov); + // Must wait for cancellation to fully complete + if (!GetOverlappedResult(fd_, &ov, &bytes_read, TRUE)) { + DWORD err2 = GetLastError(); + if (err2 != ERROR_OPERATION_ABORTED) { + bytes_read = 0; + } + } + break; + default: + { + CancelIoEx(fd_, &ov); + GetOverlappedResult(fd_, &ov, &bytes_read, TRUE); + stringstream ss; + ss << "Error waiting for read completion: " << GetLastError(); + THROW (IOException, ss.str().c_str()); + } + } + } + + return static_cast(bytes_read); +} + +size_t +Serial::SerialImpl::write (const uint8_t *data, size_t length) +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::write"); + } + + OVERLAPPED ov; + memset(&ov, 0, sizeof(ov)); + ov.hEvent = write_event_; + ResetEvent(write_event_); + + DWORD bytes_written = 0; + if (!WriteFile(fd_, data, static_cast(length), &bytes_written, &ov)) { + DWORD err = GetLastError(); + if (err != ERROR_IO_PENDING) { + stringstream ss; + ss << "Error while writing to the serial port: " << err; + THROW (IOException, ss.str().c_str()); + } + + // I/O is pending - wait with timeout + DWORD timeout_ms = timeout_.write_timeout_constant + + timeout_.write_timeout_multiplier * static_cast(length); + if (timeout_ms == 0) { + timeout_ms = INFINITE; + } + + DWORD wait_result = WaitForSingleObject(write_event_, timeout_ms); + switch (wait_result) { + case WAIT_OBJECT_0: + // Operation completed + if (!GetOverlappedResult(fd_, &ov, &bytes_written, FALSE)) { + DWORD err2 = GetLastError(); + if (err2 == ERROR_OPERATION_ABORTED) { + return 0; + } + stringstream ss; + ss << "Error getting overlapped write result: " << err2; + THROW (IOException, ss.str().c_str()); + } + break; + case WAIT_TIMEOUT: + // Timeout - cancel the pending operation + CancelIoEx(fd_, &ov); + if (!GetOverlappedResult(fd_, &ov, &bytes_written, TRUE)) { + DWORD err2 = GetLastError(); + if (err2 != ERROR_OPERATION_ABORTED) { + bytes_written = 0; + } + } + break; + default: + { + CancelIoEx(fd_, &ov); + GetOverlappedResult(fd_, &ov, &bytes_written, TRUE); + stringstream ss; + ss << "Error waiting for write completion: " << GetLastError(); + THROW (IOException, ss.str().c_str()); + } + } + } + + return static_cast(bytes_written); +} + +void +Serial::SerialImpl::setPort (const string &port) +{ + port_ = wstring(port.begin(), port.end()); +} + +string +Serial::SerialImpl::getPort () const +{ + return string(port_.begin(), port_.end()); +} + +void +Serial::SerialImpl::setTimeout (const serial_cpp::Timeout &timeout) +{ + timeout_ = timeout; + if (is_open_) { + reconfigurePort (); + } +} + +serial_cpp::Timeout +Serial::SerialImpl::getTimeout () const +{ + return timeout_; +} + +void +Serial::SerialImpl::setBaudrate (unsigned long baudrate) +{ + baudrate_ = baudrate; + if (is_open_) { + reconfigurePort (); + } +} + +unsigned long +Serial::SerialImpl::getBaudrate () const +{ + return baudrate_; +} + +void +Serial::SerialImpl::setBytesize (serial_cpp::bytesize_t bytesize) +{ + bytesize_ = bytesize; + if (is_open_) { + reconfigurePort (); + } +} + +serial_cpp::bytesize_t +Serial::SerialImpl::getBytesize () const +{ + return bytesize_; +} + +void +Serial::SerialImpl::setParity (serial_cpp::parity_t parity) +{ + parity_ = parity; + if (is_open_) { + reconfigurePort (); + } +} + +serial_cpp::parity_t +Serial::SerialImpl::getParity () const +{ + return parity_; +} + +void +Serial::SerialImpl::setStopbits (serial_cpp::stopbits_t stopbits) +{ + stopbits_ = stopbits; + if (is_open_) { + reconfigurePort (); + } +} + +serial_cpp::stopbits_t +Serial::SerialImpl::getStopbits () const +{ + return stopbits_; +} + +void +Serial::SerialImpl::setFlowcontrol (serial_cpp::flowcontrol_t flowcontrol) +{ + flowcontrol_ = flowcontrol; + if (is_open_) { + reconfigurePort (); + } +} + +serial_cpp::flowcontrol_t +Serial::SerialImpl::getFlowcontrol () const +{ + return flowcontrol_; +} + +void +Serial::SerialImpl::flush () +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::flush"); + } + FlushFileBuffers (fd_); +} + +void +Serial::SerialImpl::flushInput () +{ + if (is_open_ == false) { + throw PortNotOpenedException("Serial::flushInput"); + } + PurgeComm(fd_, PURGE_RXCLEAR); +} + +void +Serial::SerialImpl::flushOutput () +{ + if (is_open_ == false) { + throw PortNotOpenedException("Serial::flushOutput"); + } + PurgeComm(fd_, PURGE_TXCLEAR); +} + +void +Serial::SerialImpl::sendBreak (int /*duration*/) +{ + THROW (IOException, "sendBreak is not supported on Windows."); +} + +void +Serial::SerialImpl::setBreak (bool level) +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::setBreak"); + } + if (level) { + EscapeCommFunction (fd_, SETBREAK); + } else { + EscapeCommFunction (fd_, CLRBREAK); + } +} + +void +Serial::SerialImpl::setRTS (bool level) +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::setRTS"); + } + if (level) { + EscapeCommFunction (fd_, SETRTS); + } else { + EscapeCommFunction (fd_, CLRRTS); + } +} + +void +Serial::SerialImpl::setDTR (bool level) +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::setDTR"); + } + if (level) { + EscapeCommFunction (fd_, SETDTR); + } else { + EscapeCommFunction (fd_, CLRDTR); + } +} + +bool +Serial::SerialImpl::waitForChange () +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::waitForChange"); + } + + if (!SetCommMask(fd_, EV_CTS | EV_DSR | EV_RING | EV_RLSD)) { + return false; + } + + // Use a local OVERLAPPED with its own event for this blocking call + HANDLE local_event = CreateEvent(NULL, TRUE, FALSE, NULL); + if (local_event == NULL) { + return false; + } + + OVERLAPPED ov; + memset(&ov, 0, sizeof(ov)); + ov.hEvent = local_event; + + DWORD dwCommEvent = 0; + + if (!WaitCommEvent(fd_, &dwCommEvent, &ov)) { + if (GetLastError() != ERROR_IO_PENDING) { + CloseHandle(local_event); + return false; + } + // Wait indefinitely for the event + DWORD wait_result = WaitForSingleObject(local_event, INFINITE); + if (wait_result != WAIT_OBJECT_0) { + CancelIoEx(fd_, &ov); + DWORD dummy; + GetOverlappedResult(fd_, &ov, &dummy, TRUE); + CloseHandle(local_event); + return false; + } + // Get result to ensure ov is complete + DWORD dummy; + GetOverlappedResult(fd_, &ov, &dummy, TRUE); + } + + CloseHandle(local_event); + return true; +} + +bool +Serial::SerialImpl::getCTS () +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::getCTS"); + } + DWORD dwModemStatus; + if (!GetCommModemStatus(fd_, &dwModemStatus)) { + THROW (IOException, "Error getting the status of the CTS line."); + } + + return (MS_CTS_ON & dwModemStatus) != 0; +} + +bool +Serial::SerialImpl::getDSR () +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::getDSR"); + } + DWORD dwModemStatus; + if (!GetCommModemStatus(fd_, &dwModemStatus)) { + THROW (IOException, "Error getting the status of the DSR line."); + } + + return (MS_DSR_ON & dwModemStatus) != 0; +} + +bool +Serial::SerialImpl::getRI() +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::getRI"); + } + DWORD dwModemStatus; + if (!GetCommModemStatus(fd_, &dwModemStatus)) { + THROW (IOException, "Error getting the status of the RI line."); + } + + return (MS_RING_ON & dwModemStatus) != 0; +} + +bool +Serial::SerialImpl::getCD() +{ + if (is_open_ == false) { + throw PortNotOpenedException ("Serial::getCD"); + } + DWORD dwModemStatus; + if (!GetCommModemStatus(fd_, &dwModemStatus)) { + THROW (IOException, "Error getting the status of the CD line."); + } + + return (MS_RLSD_ON & dwModemStatus) != 0; +} + +void +Serial::SerialImpl::readLock() +{ + if (WaitForSingleObject(read_mutex, INFINITE) != WAIT_OBJECT_0) { + THROW (IOException, "Error claiming read mutex."); + } +} + +void +Serial::SerialImpl::readUnlock() +{ + if (!ReleaseMutex(read_mutex)) { + THROW (IOException, "Error releasing read mutex."); + } +} + +void +Serial::SerialImpl::writeLock() +{ + if (WaitForSingleObject(write_mutex, INFINITE) != WAIT_OBJECT_0) { + THROW (IOException, "Error claiming write mutex."); + } +} + +void +Serial::SerialImpl::writeUnlock() +{ + if (!ReleaseMutex(write_mutex)) { + THROW (IOException, "Error releasing write mutex."); + } +} + +#endif // #if defined(_WIN32) diff --git a/src/serial.cc b/src/serial.cc index ded7dfe..bb9135f 100755 --- a/src/serial.cc +++ b/src/serial.cc @@ -12,7 +12,11 @@ #include "serial_cpp/serial.h" #ifdef _WIN32 +#ifdef SERIAL_CPP_WIN_ASYNC +#include "serial_cpp/impl/win_async.h" +#else #include "serial_cpp/impl/win.h" +#endif #else #include "serial_cpp/impl/unix.h" #endif