diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e36f4a..93a772e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,6 +125,7 @@ endif() option(BUILD_TESTING "Build tests" OFF) option(BUILD_COVERAGE "Build with code coverage support" OFF) option(BUILD_EXAMPLES "Build examples" OFF) +option(BUILD_APP "Build CLI application installed with the library" OFF) # Coverage configuration if(BUILD_COVERAGE) @@ -255,6 +256,18 @@ if(BUILD_EXAMPLES) message(STATUS "Examples will be built - use 'make examples' to build them") endif() +if(BUILD_APP) + # Create a small CLI that ships with the library + add_executable(serialctl tools/serialctl.cpp) + target_link_libraries(serialctl PRIVATE ${PROJECT_NAME}) + target_include_directories(serialctl PRIVATE include) + + install(TARGETS serialctl + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ) + message(STATUS "Building and installing CLI 'serialctl'") +endif() + # Enable generation of compile_commands.json for tooling set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 6ab8365..9b3679e 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -93,51 +93,72 @@ void close(); /** * @brief Writes data to the serial port * - * Sends the provided string data to the serial port. A carriage return - * character ('\\r') is automatically appended to the data. + * Sends the provided string data to the serial port. The string is sent as-is without any + * additional formatting or terminators. * - * @param data Shared pointer to the string data to write - * @throws SerialException if write operation fails - * @throws std::invalid_argument if data pointer is null + * @param data String view containing the data to write + * @throws libserial::IOException if the write operation fails * - * @note The original string is not modified; a copy is made with the - * terminator appended. */ -void write(std::shared_ptr data); +void write(std::string_view data); /** - * @brief Reads data from serial port into a shared pointer buffer + * @brief Writes raw byte data to the serial port + * + * Sends the provided byte data to the serial port without any modification. + * + * @param data Pointer to the byte data to write + * @param size Number of bytes to write from the buffer pointed to by data + * @return Number of bytes actually written + * @throws libserial::IOException if the write operation fails + */ +ssize_t writeRaw(const uint8_t* data, size_t size); + +/** + * @brief Writes raw byte data to the serial port + * + * Overloaded version that accepts a vector of bytes. This is a convenience + * method that simply calls the pointer-based writeRaw after checking for + * an empty vector. + * + * @param data Vector containing the byte data to write + * @return Number of bytes actually written + * @throws libserial::IOException if the write operation fails + */ +ssize_t writeRaw(const std::vector& data); + +/** + * @brief Reads data from serial port into a pointer buffer * * Reads up to max_length bytes from the serial port and stores them - * in the provided shared string buffer. This version provides better + * in the provided string buffer. This version provides better * memory management and avoids unnecessary string copies. Just works * in canonical mode. * - * @param buffer Shared pointer to string where data will be stored + * @param buffer String where data will be stored * @return Number of bytes actually read - * @throws SerialException if read operation fails - * @throws SerialException if buffer is null + * @throws libserial::IOException if the read operation fails * * @note The buffer will be resized to contain exactly the read data */ -size_t read(std::shared_ptr buffer); +size_t read(std::string & buffer); /** - * @brief Reads a specific number of bytes from the serial port + * @brief Reads a number of bytes from the serial port * - * Reads exactly num_bytes from the serial port and stores them - * in the provided shared string buffer. Just works in non-canonical mode. + * Reads up to num_bytes from the serial port and stores them + * in the provided string buffer. Just works in non-canonical mode. * - * @param buffer Shared pointer to string where data will be stored + * @param buffer String where data will be stored * @param num_bytes Number of bytes to read * @return Number of bytes actually read - * @throws SerialException if read operation fails - * @throws SerialException if buffer is null - * @throws SerialException if num_bytes is zero + * @throws libserial::IOException if the read operation fails + * @throws std::invalid_argument if buffer is null + * @throws std::invalid_argument if num_bytes is zero * * @note The buffer will be resized to contain exactly the read data */ -size_t readBytes(std::shared_ptr buffer, size_t num_bytes); +size_t readBytes(std::string & buffer, size_t num_bytes); /** * @brief Reads data until a specific terminator character is found @@ -146,14 +167,30 @@ size_t readBytes(std::shared_ptr buffer, size_t num_bytes); * character is encountered. The terminator is included in the result. * Works in both canonical and non-canonical modes. * + * @param buffer String where data will be stored * @param terminator The character to stop reading at * @return String containing all read data including the terminator - * @throws SerialException if read operation fails + * @throws libserial::IOException if the read operation fails + * @throws std::invalid_argument if buffer is null * * @warning This method reads one byte at a time and may be slower * for large amounts of data */ -size_t readUntil(std::shared_ptr buffer, char terminator); +size_t readUntil(std::string & buffer, char terminator); + +/** + * @brief Reads raw byte data from the serial port + * + * Reads up to size bytes of raw data from the serial port into the + * provided buffer. This method is intended for non-canonical mode. + * + * @param buffer Byte array where data will be stored + * @param size Maximum number of bytes to read + * @return Number of bytes actually read + * @throws SerialException if read operation fails + * @throws std::invalid_argument if buffer pointer is null + */ +ssize_t readRaw(uint8_t* buffer, size_t size); /** * @brief Flushes the input buffer @@ -369,24 +406,23 @@ void setFdForTest(int fd) { // used in production code. void setPollSystemFunction( std::function poll_func) { - poll_ = [poll_func](struct pollfd* f, nfds_t n, int t) { - return poll_func(f, n, t); - }; + poll_ = poll_func; } void setReadSystemFunction( std::function read_func) { - read_ = [read_func](int fd, void* buf, size_t sz) { - return read_func(fd, buf, sz); - }; + read_ = read_func; +} + +void setWriteSystemFunction( + std::function write_func) { + write_ = write_func; } /* *INDENT-OFF* */ void setIoctlSystemFunction( std::function ioctl_func) { // NOLINT - ioctl_ = [ioctl_func](int fd, unsigned long request, void* arg) { // NOLINT - return ioctl_func(fd, request, arg); - }; + ioctl_ = ioctl_func; } /* *INDENT-ON* */ #endif @@ -424,6 +460,16 @@ std::function read_ = return ::read(fd, buf, sz); }; +/** + * @brief Write system call function wrapper + * + * Allows injection of custom write function for testing. + */ +std::function write_ = + [](int fd, const void* buf, size_t sz) { + return ::write(fd, buf, sz); + }; + /** * @brief Applies terminal settings to the port * diff --git a/src/serial.cpp b/src/serial.cpp index efe97d9..52ce523 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -3,9 +3,10 @@ #include "libserial/serial.hpp" #include -#include #include #include +#include +#include namespace libserial { @@ -22,13 +23,28 @@ Serial::~Serial() { } void Serial::open(const std::string& port) { - fd_serial_port_ = ::open(port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK); + // Open the serial port with read/write access, no controlling terminal, and non-blocking mode. + // On many serial devices, opening a port without O_NONBLOCK can block waiting for modem + // control lines/carrier detect, which is a behavior change that can hang callers. By opening + // in non-blocking mode and then immediately clearing that flag, we can avoid this issue while + // still allowing blocking reads/writes as expected. + fd_serial_port_ = ::open(port.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd_serial_port_ == -1) { throw SerialException("Error opening port " + port + ": " + strerror(errno)); } - else { - fcntl(fd_serial_port_, F_SETFL, 0); + + int flags = ::fcntl(fd_serial_port_, F_GETFL); + + if (flags == -1) { + int saved_errno = errno; + this->close(); + throw SerialException("Error configuring port " + port + ": " + strerror(saved_errno)); + } + if (::fcntl(fd_serial_port_, F_SETFL, flags & ~O_NONBLOCK) == -1) { + int saved_errno = errno; + this->close(); + throw SerialException("Error configuring port " + port + ": " + strerror(saved_errno)); } } @@ -42,96 +58,155 @@ void Serial::close() { } } -void Serial::write(std::shared_ptr data) { - if (!data) { - throw IOException("Null pointer passed to write function"); +void Serial::write(std::string_view data) { + if (data.empty()) { + throw IOException("Empty string passed to write function"); } - ssize_t bytes_written = ::write(fd_serial_port_, data->c_str(), data->size()); + ssize_t bytes_written = write_(fd_serial_port_, data.data(), data.size()); if (bytes_written < 0) { throw IOException("Error writing to serial port: " + std::string(strerror(errno))); } } -size_t Serial::read(std::shared_ptr buffer) { - if (canonical_mode_ == CanonicalMode::DISABLE) { +ssize_t Serial::writeRaw(const uint8_t* data, size_t size) { + if (canonical_mode_ == CanonicalMode::ENABLE) { throw IOException( - "read() is not supported in non-canonical mode; use readBytes() or readUntil() instead"); + "writeRaw() is not supported in canonical mode; use write() instead"); } - if (!buffer) { - throw IOException("Null pointer passed to read function"); + if (!data || size == 0) { + throw IOException("Invalid buffer passed to writeRaw"); } - buffer->clear(); - buffer->resize(max_safe_read_size_); + size_t total_written = 0; + auto start_time = std::chrono::steady_clock::now(); + + while (total_written < size) { + int timeout_ms = -1; + if (write_timeout_ms_.count() > 0) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - start_time); + + if (elapsed >= write_timeout_ms_) { + break; + } + + timeout_ms = static_cast((write_timeout_ms_ - elapsed).count()); + } + + struct pollfd pfd; + pfd.fd = fd_serial_port_; + pfd.events = POLLOUT; + + int pool_result = poll_(&pfd, 1, timeout_ms); + + if (pool_result < 0) { + if (errno == EINTR) continue; + throw IOException("Error in poll(): " + std::string(strerror(errno))); + } + + if (pool_result == 0) { + break; + } + + // Check for error conditions signaled by poll + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + throw IOException("Serial port not writable (poll error state)"); + } + + ssize_t ret = write_(fd_serial_port_, + data + total_written, + size - total_written); + + if (ret < 0) { + if (errno == EINTR) continue; + // Defensive: if fd was toggled non-blocking somewhere + if (errno == EAGAIN || errno == EWOULDBLOCK) { + continue; + } + throw IOException("Error writing raw data: " + std::string(strerror(errno))); + } + + if (ret == 0) { + // No progress even though POLLOUT said writable. + // Avoid tight spin: re-poll (or optionally sleep a tiny bit). + continue; + } + total_written += static_cast(ret); + } + return static_cast(total_written); +} + +ssize_t Serial::writeRaw(const std::vector& data) { + if (data.empty()) { + throw IOException("Data vector is empty"); + } + return writeRaw(data.data(), data.size()); +} + +size_t Serial::read(std::string & buffer) { + if (canonical_mode_ == CanonicalMode::DISABLE) { + throw IOException( + "read() is not supported in non-canonical mode; use readBytes(), readUntil() or readRaw() instead"); + } struct pollfd fd_poll; fd_poll.fd = fd_serial_port_; fd_poll.events = POLLIN; - // 0 => no wait (immediate return), -1 => block forever, positive => wait specified milliseconds int timeout_ms = static_cast(read_timeout_ms_.count()); - int pr = poll_(&fd_poll, 1, timeout_ms); - if (pr < 0) { + int poll_result = poll_(&fd_poll, 1, timeout_ms); + if (poll_result < 0) { throw IOException(std::string("Error in poll(): ") + strerror(errno)); } - if (pr == 0) { + if (poll_result == 0) { throw IOException("Read operation timed out after " + std::to_string(timeout_ms) + " milliseconds"); } - // Data available: do the read - ssize_t bytes_read = read_(fd_serial_port_, const_cast(buffer->data()), - max_safe_read_size_); + buffer.resize(max_safe_read_size_); + + ssize_t bytes_read = read_(fd_serial_port_, buffer.data(), max_safe_read_size_); if (bytes_read < 0) { throw IOException(std::string("Error reading from serial port: ") + strerror(errno)); } - buffer->resize(static_cast(bytes_read)); + buffer.resize(static_cast(bytes_read)); return static_cast(bytes_read); } -size_t Serial::readBytes(std::shared_ptr buffer, size_t num_bytes) { +size_t Serial::readBytes(std::string & buffer, size_t num_bytes) { if (canonical_mode_ == CanonicalMode::ENABLE) { throw IOException( "readBytes() is not supported in canonical mode; use read() or readUntil() instead"); } - if (!buffer) { - throw IOException("Null pointer passed to readBytes function"); - } - if (num_bytes == 0) { throw IOException("Number of bytes requested must be greater than zero"); } - buffer->clear(); - buffer->resize(num_bytes); + buffer.clear(); + buffer.resize(num_bytes); - ssize_t bytes_read = read_(fd_serial_port_, buffer->data(), num_bytes); // codacy-ignore[buffer-boundary] + ssize_t bytes_read = read_(fd_serial_port_, buffer.data(), num_bytes); // codacy-ignore[buffer-boundary] if (bytes_read < 0) { throw IOException("Error reading from serial port: " + std::string(strerror(errno))); } - buffer->resize(static_cast(bytes_read)); + buffer.resize(static_cast(bytes_read)); return static_cast(bytes_read); } -size_t Serial::readUntil(std::shared_ptr buffer, char terminator) { - if (!buffer) { - throw IOException("Null pointer passed to readUntil function"); - } - - buffer->clear(); +size_t Serial::readUntil(std::string & buffer, char terminator) { + buffer.clear(); char temp_char = '\0'; auto start_time = std::chrono::steady_clock::now(); while (temp_char != terminator) { - // Check buffer size limit to prevent excessive memory usage - if (buffer->size() >= max_safe_read_size_) { + if (buffer.size() >= max_safe_read_size_) { throw IOException("Read buffer exceeded maximum size limit of " + std::to_string(max_safe_read_size_) + " bytes without finding terminator"); @@ -147,8 +222,6 @@ size_t Serial::readUntil(std::shared_ptr buffer, char terminator) { } // Use poll() to check if data is available with remaining timeout. - // poll() does not have the FD_SETSIZE limitation that select() has - // and is more robust for larger file descriptor values. struct pollfd pfd; pfd.fd = fd_serial_port_; pfd.events = POLLIN; @@ -165,27 +238,88 @@ size_t Serial::readUntil(std::shared_ptr buffer, char terminator) { } } - // Data is available, perform the read ssize_t bytes_read = read_(fd_serial_port_, &temp_char, 1); if (bytes_read < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { - // Non-blocking read, no data available right now std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } throw IOException("Error reading from serial port: " + std::string(strerror(errno))); } else if (bytes_read == 0) { - // End of file or connection closed throw IOException("Connection closed while reading: no terminator found"); } - // Add the character to buffer (including terminator) - buffer->push_back(temp_char); + buffer.push_back(temp_char); + } + + return buffer.size(); +} + +ssize_t Serial::readRaw(uint8_t* buffer, size_t size) { + if (canonical_mode_ == CanonicalMode::ENABLE) { + throw IOException( + "readRaw() is not supported in canonical mode; use read() or readUntil() instead"); + } + + if (!buffer || size == 0) { + throw IOException("Invalid buffer passed to readRaw"); + } + + size_t total_read = 0; + + auto start_time = std::chrono::steady_clock::now(); + + while (total_read < size) { + int timeout_ms = -1; + if (read_timeout_ms_.count() > 0) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - start_time); + + if (elapsed >= read_timeout_ms_) { + break; // timeout reached → return what we have + } + + timeout_ms = static_cast((read_timeout_ms_ - elapsed).count()); + } + + struct pollfd pfd; + pfd.fd = fd_serial_port_; + pfd.events = POLLIN; + + int pr = poll_(&pfd, 1, timeout_ms); + + if (pr < 0) { + if (errno == EINTR) continue; + throw IOException("Error in poll(): " + std::string(strerror(errno))); + } + + if (pr == 0) { + break; + } + + ssize_t ret = read_(fd_serial_port_, + buffer + total_read, + size - total_read); + + if (ret < 0) { + if (errno == EINTR) continue; + + if (errno == EAGAIN || errno == EWOULDBLOCK) { + continue; + } + throw IOException("Error reading raw data: " + std::string(strerror(errno))); + } + + if (ret == 0) { + break; + } + + total_read += static_cast(ret); } - return buffer->size(); + return static_cast(total_read); } void Serial::flushInputBuffer() { diff --git a/test/test_ports.cpp b/test/test_ports.cpp index c12f2ed..445d07b 100644 --- a/test/test_ports.cpp +++ b/test/test_ports.cpp @@ -110,9 +110,15 @@ TEST_F(PortsTest, GetDevicesPopulatesList) { ports.getDevices(devices); }); + std::vector names; + for (const auto& device : devices) { + names.push_back(device.getName()); + } + EXPECT_EQ(devices.size(), 2); - EXPECT_EQ(devices[0].getName(), "usb-Device_Two_0002"); - EXPECT_EQ(devices[1].getName(), "usb-Device_One_0001"); + + EXPECT_NE(std::find(names.begin(), names.end(), "usb-Device_One_0001"), names.end()); + EXPECT_NE(std::find(names.begin(), names.end(), "usb-Device_Two_0002"), names.end()); } diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 457fd57..3496d20 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -2,20 +2,21 @@ #include -#include -#include +#include +#include #include +#include #include #include -#include #include +#include -#include -#include #include -#include #include #include +#include +#include +#include #include "libserial/serial.hpp" #include "libserial/serial_exception.hpp" @@ -102,31 +103,6 @@ TEST_F(PseudoTerminalTest, ParameterizedConstructor) { libserial::Serial serial_port(slave_port_); } -// TEST_F(PseudoTerminalTest, SetTermios2WithFail) { -// libserial::Serial serial_port; - -// serial_port.open(slave_port_); - -// // Inject failure into ioctl for setTermios2 -// serial_port.setIoctlSystemFunction( -// [](int, unsigned long, void*) -> int { // NOLINT -// errno = EIO; -// return -1; -// }); - -// EXPECT_THROW({ -// serial_port.setBaudRate(9600); -// }, libserial::SerialException); - -// // Restore ioctl function for cleanup -// serial_port.setIoctlSystemFunction( -// [](int fd, unsigned long request, void* arg) -> int { // NOLINT -// return ::ioctl(fd, request, arg); -// }); - -// serial_port.close(); -// } - TEST_F(PseudoTerminalTest, SetAndGetBaudRate) { libserial::Serial serial_port; @@ -150,45 +126,6 @@ TEST_F(PseudoTerminalTest, SetAndGetBaudRate) { serial_port.close(); } -// TEST_F(PseudoTerminalTest, SetAndGetDataLength) { -// libserial::Serial serial_port; - -// serial_port.open(slave_port_); - -// // Test multiple data lengths to be more thorough -// std::vector test_lengths = { -// libserial::DataLength::FIVE, -// libserial::DataLength::SIX, -// libserial::DataLength::SEVEN, -// libserial::DataLength::EIGHT -// }; - -// for (const auto& expected_length : test_lengths) { -// // Set data length -// EXPECT_NO_THROW({ -// serial_port.setDataLength(expected_length); -// }); - -// // Add a small delay and flush -// std::this_thread::sleep_for(std::chrono::milliseconds(50)); - -// // Force a re-read of the current settings -// serial_port.close(); -// serial_port.open(slave_port_); - -// // Get data length and verify -// libserial::DataLength actual_length; -// EXPECT_NO_THROW({ -// actual_length = serial_port.getDataLength(); -// }); - -// EXPECT_EQ(actual_length, expected_length) -// << "Failed for data length: " << static_cast(expected_length); -// } - -// serial_port.close(); -// } - TEST_F(PseudoTerminalTest, SetGetReadTimeout) { libserial::Serial serial_port; @@ -282,10 +219,8 @@ TEST_F(PseudoTerminalTest, WriteTest) { serial_port.open(slave_port_); serial_port.setBaudRate(115200); - // Create test data using smart pointer - auto test_data = std::make_shared("Test Write Data"); + std::string_view test_data("Test Write Data"); - // Write using our Serial class EXPECT_NO_THROW({ serial_port.write(test_data); }); // Give time for data to propagate @@ -297,53 +232,194 @@ TEST_F(PseudoTerminalTest, WriteTest) { std::string received(buffer, bytes_read); - EXPECT_EQ(received, *test_data); + EXPECT_EQ(received, std::string(test_data)); } -TEST_F(PseudoTerminalTest, ReadCanonicalMode) { +TEST_F(PseudoTerminalTest, WriteRawBasic) { libserial::Serial serial_port; serial_port.open(slave_port_); - serial_port.setBaudRate(9600); + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial_port.setBaudRate(115200); - const std::string test_message{"Smart Pointer Test!\n"}; + std::vector data = {0x00, 0xFF, 0x10, 0x41, 0x00}; - ssize_t bytes_written = write(master_fd_, test_message.c_str(), test_message.length()); - ASSERT_GT(bytes_written, 0) << "Failed to write to master end"; + EXPECT_NO_THROW({ + ssize_t written = serial_port.writeRaw(data.data(), data.size()); + EXPECT_EQ(written, data.size()); + }); - // Give time for data to propagate - fsync(master_fd_); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); - // Test reading with shared pointer - auto read_buffer = std::make_shared(); - size_t bytes_read = 0; + uint8_t buffer[100] = {0}; + ssize_t bytes_read = read(master_fd_, buffer, sizeof(buffer)); - EXPECT_NO_THROW({ bytes_read = serial_port.read(read_buffer); }); + ASSERT_EQ(bytes_read, data.size()); + EXPECT_EQ(std::vector(buffer, buffer + bytes_read), data); +} - EXPECT_EQ(bytes_read, test_message.length()); - EXPECT_EQ(*read_buffer, test_message); +TEST_F(PseudoTerminalTest, WriteRawPartialWrites) { + libserial::Serial serial_port; + serial_port.open(slave_port_); + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); + + std::vector data = {1, 2, 3, 4, 5, 6}; + + size_t call_count = 0; + + serial_port.setWriteSystemFunction( + [&call_count](int, const void* buf, size_t len) -> ssize_t { + call_count++; + + // Simulate partial writes (2 bytes per call) + size_t to_write = std::min(2, len); + return to_write; + }); + + ssize_t written = serial_port.writeRaw(data.data(), data.size()); + + EXPECT_EQ(written, data.size()); + EXPECT_GT(call_count, 1); // ensure loop was used +} + +TEST_F(PseudoTerminalTest, WriteRawWithEINTR) { + libserial::Serial serial_port; + serial_port.open(slave_port_); + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); + + std::vector data = {1, 2, 3}; + + int call_count = 0; + + serial_port.setWriteSystemFunction( + [&call_count](int, const void*, size_t len) -> ssize_t { + if (call_count++ == 0) { + errno = EINTR; + return -1; + } + return len; + }); + + EXPECT_NO_THROW({ + ssize_t written = serial_port.writeRaw(data.data(), data.size()); + EXPECT_EQ(written, data.size()); + }); } -TEST_F(PseudoTerminalTest, ReadWithNullBuffer) { +TEST_F(PseudoTerminalTest, WriteRawWithError) { libserial::Serial serial_port; + serial_port.open(slave_port_); + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); + + std::vector data = {1, 2, 3}; + serial_port.setWriteSystemFunction( + [](int, const void*, size_t) -> ssize_t { + errno = EIO; + return -1; + }); + + EXPECT_THROW({ + try { + serial_port.writeRaw(data.data(), data.size()); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ("Error writing raw data: Input/output error", e.what()); + throw; + } + }, libserial::IOException); +} + +TEST_F(PseudoTerminalTest, WriteRawLargeBuffer) { + libserial::Serial serial_port; serial_port.open(slave_port_); - serial_port.setBaudRate(9600); + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); + + EXPECT_NO_THROW({ + std::vector data(4096, 0xAA); + ssize_t written = serial_port.writeRaw(data.data(), data.size()); + EXPECT_EQ(written, data.size()); + }); +} + +TEST_F(PseudoTerminalTest, WriteRawPollTimeout) { + libserial::Serial serial; + + serial.setFdForTest(slave_fd_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setWriteTimeout(std::chrono::milliseconds(100)); + + // poll always times out + serial.setPollSystemFunction( + [](struct pollfd*, nfds_t, int) { + return 0; + }); + + uint8_t data[10] = {0}; + + ssize_t written = serial.writeRaw(data, sizeof(data)); + + EXPECT_EQ(written, 0); +} - std::shared_ptr null_buffer; +TEST_F(PseudoTerminalTest, WriteRawNullBuffer) { + libserial::Serial serial_port; + serial_port.open(slave_port_); // Open a valid port to avoid fd errors + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); + EXPECT_THROW({ + try { + serial_port.writeRaw(nullptr, 10); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ("Invalid buffer passed to writeRaw", e.what()); + throw; + } + }, libserial::IOException); +} +TEST_F(PseudoTerminalTest, WriteRawZeroSize) { + libserial::Serial serial_port; + serial_port.open(slave_port_); // Open a valid port to avoid fd errors + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); + uint8_t dummy = 0; EXPECT_THROW({ try { - serial_port.read(null_buffer); + serial_port.writeRaw(&dummy, 0); } catch (const libserial::IOException& e) { - EXPECT_STREQ("Null pointer passed to read function", e.what()); + EXPECT_STREQ("Invalid buffer passed to writeRaw", e.what()); throw; } }, libserial::IOException); } +TEST_F(PseudoTerminalTest, ReadCanonicalMode) { + libserial::Serial serial_port; + + serial_port.open(slave_port_); + serial_port.setBaudRate(9600); + + const std::string test_message{"Read canonical mode test!\n"}; + + ssize_t bytes_written = write(master_fd_, test_message.c_str(), test_message.length()); + ASSERT_GT(bytes_written, 0) << "Failed to write to master end"; + + // Give time for data to propagate + fsync(master_fd_); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + { + // Test reading with shared pointer + std::string read_buffer; + size_t bytes_read = 0; + + EXPECT_NO_THROW({ bytes_read = serial_port.read(read_buffer); }); + + EXPECT_EQ(bytes_read, test_message.length()); + EXPECT_EQ(read_buffer, test_message); + } +} + TEST_F(PseudoTerminalTest, ReadNonCanonicalMode) { libserial::Serial serial_port; @@ -360,16 +436,15 @@ TEST_F(PseudoTerminalTest, ReadNonCanonicalMode) { fsync(master_fd_); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - // Attempt to read using read() - should throw exception - auto read_buffer = std::make_shared(); - EXPECT_THROW({ try { + // Attempt to read using read() - should throw exception + std::string read_buffer; serial_port.read(read_buffer); } catch (const libserial::IOException& e) { EXPECT_STREQ( - "read() is not supported in non-canonical mode; use readBytes() or readUntil() instead", + "read() is not supported in non-canonical mode; use readBytes(), readUntil() or readRaw() instead", e.what()); throw; } @@ -386,13 +461,12 @@ TEST_F(PseudoTerminalTest, ReadTimeout) { int time_out_ms = 100; serial_port.setReadTimeout(std::chrono::milliseconds(time_out_ms)); - auto read_buffer = std::make_shared(); - auto expected_what = "Read operation timed out after " + std::to_string(time_out_ms) + " milliseconds"; EXPECT_THROW({ try { + std::string read_buffer; serial_port.read(read_buffer); } catch (const libserial::IOException& e) { @@ -404,7 +478,7 @@ TEST_F(PseudoTerminalTest, ReadTimeout) { TEST_F(PseudoTerminalTest, ReadWithReadFail) { libserial::Serial serial_port; - auto read_buffer = std::make_shared(); + std::string read_buffer; for (const auto& [error_num, error_msg] : errors_read_) { serial_port.setPollSystemFunction( @@ -433,7 +507,7 @@ TEST_F(PseudoTerminalTest, ReadWithReadFail) { TEST_F(PseudoTerminalTest, ReadWithPollFail) { libserial::Serial serial_port; - auto read_buffer = std::make_shared(); + std::string read_buffer; for (const auto& [error_num, error_msg] : errors_poll_) { serial_port.setPollSystemFunction( @@ -473,33 +547,13 @@ TEST_F(PseudoTerminalTest, ReadBytesNonCanonicalMode) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Test reading with shared pointer - auto read_buffer = std::make_shared(); + std::string read_buffer; size_t bytes_read = 0; EXPECT_NO_THROW({ bytes_read = serial_port.readBytes(read_buffer, test_message.length()); }); EXPECT_EQ(bytes_read, test_message.length()); - EXPECT_EQ(*read_buffer, test_message); -} - -TEST_F(PseudoTerminalTest, ReadBytesWithNullBuffer) { - libserial::Serial serial_port; - - serial_port.open(slave_port_); - serial_port.setBaudRate(9600); - serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); - - std::shared_ptr null_buffer; - - EXPECT_THROW({ - try { - serial_port.readBytes(null_buffer, 10); - } - catch (const libserial::IOException& e) { - EXPECT_STREQ("Null pointer passed to readBytes function", e.what()); - throw; - } - }, libserial::IOException); + EXPECT_EQ(read_buffer, test_message); } TEST_F(PseudoTerminalTest, ReadBytesWithInvalidNumBytes) { @@ -509,10 +563,9 @@ TEST_F(PseudoTerminalTest, ReadBytesWithInvalidNumBytes) { serial_port.setBaudRate(9600); serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); - auto read_buffer = std::make_shared(); - EXPECT_THROW({ try { + std::string read_buffer; serial_port.readBytes(read_buffer, 0); } catch (const libserial::IOException& e) { @@ -529,7 +582,7 @@ TEST_F(PseudoTerminalTest, ReadBytesWithReadFail) { serial_port.setBaudRate(9600); serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); - auto read_buffer = std::make_shared(); + std::string read_buffer; for (const auto& [error_num, error_msg] : errors_read_) { serial_port.setReadSystemFunction( @@ -559,10 +612,9 @@ TEST_F(PseudoTerminalTest, ReadBytesCanonicalMode) { serial_port.setBaudRate(9600); serial_port.setCanonicalMode(libserial::CanonicalMode::ENABLE); - auto read_buffer = std::make_shared(); - EXPECT_THROW({ try { + std::string read_buffer; serial_port.readBytes(read_buffer, 5); ADD_FAILURE() << "Expected SerialException but no exception was thrown"; } @@ -591,30 +643,11 @@ TEST_F(PseudoTerminalTest, ReadUntil) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Test reading with shared pointer - only read what's available - auto read_buffer = std::make_shared(); + std::string read_buffer; EXPECT_NO_THROW({serial_port.readUntil(read_buffer, '!'); }); - EXPECT_EQ(*read_buffer, "Read Until!"); -} - -TEST_F(PseudoTerminalTest, ReadUntilWithNullBuffer) { - libserial::Serial serial_port; - - serial_port.open(slave_port_); - serial_port.setBaudRate(9600); - - std::shared_ptr null_buffer; - - EXPECT_THROW({ - try { - serial_port.readUntil(null_buffer, '!'); - } - catch (const libserial::IOException& e) { - EXPECT_STREQ("Null pointer passed to readUntil function", e.what()); - throw; - } - }, libserial::IOException); + EXPECT_EQ(read_buffer, "Read Until!"); } TEST_F(PseudoTerminalTest, ReadUntilTimeout) { @@ -632,15 +665,15 @@ TEST_F(PseudoTerminalTest, ReadUntilTimeout) { fsync(master_fd_); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - // Test reading with shared pointer - only read what's available - auto read_buffer = std::make_shared(); - - EXPECT_THROW({serial_port.readUntil(read_buffer, '!'); }, libserial::IOException); + EXPECT_THROW({ + // Test reading with shared pointer - only read what's available + std::string read_buffer; + serial_port.readUntil(read_buffer, '!'); + }, libserial::IOException); } TEST_F(PseudoTerminalTest, ReadUntilWithReadFail) { libserial::Serial serial_port; - auto read_buffer = std::make_shared(); for (const auto& [error_num, error_msg] : errors_read_) { if (error_num == EAGAIN || error_num == EWOULDBLOCK) { @@ -661,6 +694,7 @@ TEST_F(PseudoTerminalTest, ReadUntilWithReadFail) { EXPECT_THROW({ try { + std::string read_buffer; serial_port.readUntil(read_buffer, '!'); } catch (const libserial::IOException& e) { @@ -673,7 +707,6 @@ TEST_F(PseudoTerminalTest, ReadUntilWithReadFail) { TEST_F(PseudoTerminalTest, ReadUntilWithPollFail) { libserial::Serial serial_port; - auto read_buffer = std::make_shared(); for (const auto& [error_num, error_msg] : errors_poll_) { serial_port.setPollSystemFunction( @@ -686,6 +719,7 @@ TEST_F(PseudoTerminalTest, ReadUntilWithPollFail) { EXPECT_THROW({ try { + std::string read_buffer; serial_port.readUntil(read_buffer, '!'); } catch (const libserial::IOException& e) { @@ -713,15 +747,13 @@ TEST_F(PseudoTerminalTest, ReadUntilWithOverflowBuffer) { fsync(master_fd_); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - // Test reading with shared pointer - only read what's available - auto read_buffer = std::make_shared(); - auto expected_what = "Read buffer exceeded maximum size limit of " + std::to_string(serial_port.getMaxSafeReadSize()) + " bytes without finding terminator"; EXPECT_THROW({ try { + std::string read_buffer; serial_port.readUntil(read_buffer, '!'); } catch (const libserial::IOException& e) { @@ -730,3 +762,177 @@ TEST_F(PseudoTerminalTest, ReadUntilWithOverflowBuffer) { } }, libserial::IOException); } + +TEST_F(PseudoTerminalTest, ReadRawCanonicalMode) { + libserial::Serial serial; + + serial.open(slave_port_); + serial.setBaudRate(9600); + + // Enable canonical mode + serial.setCanonicalMode(libserial::CanonicalMode::ENABLE); + + EXPECT_THROW({ + try { + std::vector buffer(10); + serial.readRaw(buffer.data(), buffer.size()); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ( + "readRaw() is not supported in canonical mode; use read() or readUntil() instead", + e.what()); + throw; + } + }, libserial::IOException); +} + +TEST_F(PseudoTerminalTest, ReadRawFullRead) { + libserial::Serial serial; + serial.open(slave_port_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setReadTimeout(std::chrono::milliseconds(500)); + + const std::string msg = "HelloRaw"; + write(master_fd_, msg.data(), msg.size()); + + std::vector buffer(msg.size()); + + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); + + EXPECT_EQ(n, msg.size()); + EXPECT_EQ(std::string(buffer.begin(), buffer.end()), msg); +} + +TEST_F(PseudoTerminalTest, ReadRawPartialTimeout) { + libserial::Serial serial; + serial.open(slave_port_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setReadTimeout(std::chrono::milliseconds(100)); + + const std::string msg = "ABC"; + write(master_fd_, msg.data(), msg.size()); + + std::vector buffer(10); + + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); + + EXPECT_EQ(n, msg.size()); +} + +TEST_F(PseudoTerminalTest, ReadRawTimeoutNoData) { + libserial::Serial serial; + serial.open(slave_port_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setReadTimeout(std::chrono::milliseconds(100)); + + std::vector buffer(10); + + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); + + EXPECT_EQ(n, 0); +} + +TEST_F(PseudoTerminalTest, ReadRawPollTimeoutSimulated) { + libserial::Serial serial; + serial.setFdForTest(slave_fd_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setPollSystemFunction( + [](struct pollfd*, nfds_t, int) { + return 0; // timeout + }); + + std::vector buffer(10); + + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); + + EXPECT_EQ(n, 0); +} + +TEST_F(PseudoTerminalTest, ReadRawPollError) { + libserial::Serial serial; + serial.setFdForTest(slave_fd_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setPollSystemFunction( + [](struct pollfd*, nfds_t, int) { + errno = EINVAL; + return -1; + }); + + EXPECT_THROW({ + try { + std::vector buffer(10); + serial.readRaw(buffer.data(), buffer.size()); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ( + std::string("Error in poll(): " + std::string(strerror(EINVAL))).c_str(), + e.what()); + throw; + } + }, libserial::IOException); +} + +TEST_F(PseudoTerminalTest, ReadRawReadError) { + libserial::Serial serial; + serial.setFdForTest(slave_fd_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setPollSystemFunction( + [](struct pollfd*, nfds_t, int) { + return 1; + }); + + serial.setReadSystemFunction( + [](int, void*, size_t) -> ssize_t { + errno = EIO; + return -1; + }); + + EXPECT_THROW({ + try { + std::vector buffer(10); + serial.readRaw(buffer.data(), buffer.size()); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ( + std::string("Error reading raw data: " + std::string(strerror(EIO))).c_str(), + e.what()); + throw; + } + }, libserial::IOException); +} + +TEST_F(PseudoTerminalTest, ReadRawMultipleChunks) { + libserial::Serial serial; + serial.setFdForTest(slave_fd_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + + serial.setPollSystemFunction( + [](struct pollfd*, nfds_t, int) { + return 1; + }); + + int call = 0; + + serial.setReadSystemFunction( + [&call](int, void* buf, size_t) -> ssize_t { + uint8_t* b = static_cast(buf); + + if (call == 0) { + b[0] = 'A'; + call++; + return 1; + } + else { + b[0] = 'B'; + return 1; + } + }); + + std::vector buffer(2); + + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); + + EXPECT_EQ(n, 2); + EXPECT_EQ(buffer[0], 'A'); + EXPECT_EQ(buffer[1], 'B'); +} diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index 1351914..14de5c2 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -32,25 +32,12 @@ TEST_F(SerialTest, ConstructorWithInvalidPort) { }, libserial::SerialException); } -TEST_F(SerialTest, WriteWithSharedPtr) { +TEST_F(SerialTest, WriteWithEmptyStringView) { libserial::Serial serial; - // Test that write function accepts shared_ptr - auto message = std::make_shared("Test message"); - - // This will throw since no port is opened, but tests the API - EXPECT_THROW({ - serial.write(message); - }, libserial::SerialException); -} - -TEST_F(SerialTest, WriteWithNullPtr) { - libserial::Serial serial; - - // Test that write function handles null pointer - std::shared_ptr null_message; - EXPECT_THROW({ + // Test that write function handles a default-constructed empty string_view + std::string_view null_message; serial.write(null_message); }, libserial::SerialException); } @@ -70,19 +57,9 @@ TEST_F(SerialTest, APIExists) { libserial::SerialException); - // Test new shared pointer read API - auto buffer = std::make_shared(); + // Verify read APIs remain available and report unopened-port errors + std::string buffer; EXPECT_THROW(serial.read(buffer), libserial::IOException); - EXPECT_THROW(serial.readUntil(buffer, '\n'), libserial::IOException); -} - -TEST_F(SerialTest, ReadWithNullSharedPtr) { - libserial::Serial serial; - - // Test that read function handles null shared pointer - std::shared_ptr null_buffer; - - EXPECT_THROW({ serial.read(null_buffer); }, libserial::SerialException); } TEST_F(SerialTest, CloseWithInvalidFd) { @@ -97,3 +74,5 @@ TEST_F(SerialTest, CloseWithInvalidFd) { EXPECT_EQ(msg, "Error closing port: Bad file descriptor"); } } + + diff --git a/tools/serialctl.cpp b/tools/serialctl.cpp new file mode 100644 index 0000000..05842a0 --- /dev/null +++ b/tools/serialctl.cpp @@ -0,0 +1,64 @@ +// Copyright 2025 Nestor Neto + +// Simple CLI for libserial: list serial ports +#include +#include +#include + +#include "libserial/ports.hpp" +#include "libserial/device.hpp" + +void print_help(const char* prog) { + std::cout << "Usage: " << prog << " [--list] [--help] [--version]\n"; + std::cout << "Options:\n"; + std::cout << " --list List available serial ports\n"; + std::cout << " --version Print program/library version\n"; + std::cout << " --help Show this help message\n"; +} + +int main(int argc, char** argv) { + if (argc <= 1) { + print_help(argv[0]); + return 0; + } + + std::string arg = argv[1]; + if (arg == "--help" || arg == "-h") { + print_help(argv[0]); + return 0; + } + + if (arg == "--version") { + std::cout << "libserial CLI\n"; + return 0; + } + + if (arg == "--list") { + try { + libserial::Ports ports; + uint16_t num = ports.scanPorts(); + if (num == 0) { + std::cout << "Found 0 entries\n"; + return 0; + } + std::cout << "Found " << num << " entries (index 0.." << (num - 1) << ")\n"; + + for (uint16_t i = 0; i < num; ++i) { + auto name = ports.findName(i); + auto port = ports.findPortPath(i); + auto bus = ports.findBusPath(i); + std::cout << "[" << i << "] " + << name.value_or("unknown") << " -> " + << port.value_or("unknown") << " (bus: " + << bus.value_or("unknown") << ")\n"; + } + return 0; + } catch (const std::exception& e) { + std::cerr << "Error listing ports: " << e.what() << std::endl; + return 2; + } + } + + print_help(argv[0]); + return 1; +}