From d406366e442d22403de49805a56e4c3e4f1cc2e7 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 12:41:35 -0300 Subject: [PATCH 01/34] =?UTF-8?q?=E2=9C=A8=20feat(cli):=20Create=20a=20new?= =?UTF-8?q?=20CLI=20aplication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 13 ++++++++++ tools/serialctl.cpp | 58 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tools/serialctl.cpp 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/tools/serialctl.cpp b/tools/serialctl.cpp new file mode 100644 index 0000000..652880d --- /dev/null +++ b/tools/serialctl.cpp @@ -0,0 +1,58 @@ +// Copyright 2025 Nestor Neto + +// Simple CLI for libserial: list serial ports +#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(); + std::cout << "Found " << (num + 1) << " entries (index 0.." << num << ")\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; +} From 538c465cfb5e39e710413b0bece93f61914f95c6 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 12:42:57 -0300 Subject: [PATCH 02/34] =?UTF-8?q?=E2=9C=A8=20feat(serial):=20Create=20a=20?= =?UTF-8?q?new=20write=20raw=20data=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 13 +++++++++++++ src/serial.cpp | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 6ab8365..b2ecd60 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -105,6 +105,19 @@ void close(); */ void write(std::shared_ptr data); +/** + * @brief Writes raw byte data to the serial port + * + * Sends the provided byte data to the serial port without any modification. + * + * @param data Vector containing the byte data to write + * @param size Number of bytes to write from the data vector + * @return Number of bytes actually written + * @throws SerialException if write operation fails + * @throws std::invalid_argument if data vector is empty + */ +ssize_t writeRaw(const uint8_t* data, size_t size); + /** * @brief Reads data from serial port into a shared pointer buffer * diff --git a/src/serial.cpp b/src/serial.cpp index efe97d9..339d447 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -54,6 +54,29 @@ void Serial::write(std::shared_ptr data) { } } +ssize_t Serial::writeRaw(const uint8_t* data, size_t size) { + if (!data || size == 0) { + throw IOException("Invalid buffer passed to writeRaw"); + } + + size_t total_written = 0; + + while (total_written < size) { + ssize_t ret = ::write(fd_serial_port_, + data + total_written, + size - total_written); + + if (ret < 0) { + if (errno == EINTR) continue; + throw IOException("Error writing raw data: " + std::string(strerror(errno))); + } + + total_written += static_cast(ret); + } + + return static_cast(total_written); +} + size_t Serial::read(std::shared_ptr buffer) { if (canonical_mode_ == CanonicalMode::DISABLE) { throw IOException( From 70ffd74e6af5b79ac56e90f6e09dbe0264561bf1 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 17:07:11 -0300 Subject: [PATCH 03/34] =?UTF-8?q?=E2=9C=A8=20feat(serial):=20Create=20a=20?= =?UTF-8?q?new=20write=20raw=20overload=20for=20uint8=20vector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 14 ++++++++++++++ src/serial.cpp | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index b2ecd60..e9e20fc 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -118,6 +118,20 @@ void write(std::shared_ptr data); */ 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 SerialException if write operation fails + * @throws std::invalid_argument if data vector is empty + */ +ssize_t writeRaw(const std::vector& data); + /** * @brief Reads data from serial port into a shared pointer buffer * diff --git a/src/serial.cpp b/src/serial.cpp index 339d447..8544501 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -77,6 +77,13 @@ ssize_t Serial::writeRaw(const uint8_t* data, size_t size) { 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::shared_ptr buffer) { if (canonical_mode_ == CanonicalMode::DISABLE) { throw IOException( From 29d3393c26185bbdedf1b13198327662de099323 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 17:07:42 -0300 Subject: [PATCH 04/34] =?UTF-8?q?=E2=9C=85=20test(serial):=20Remove=20unec?= =?UTF-8?q?essary=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_pty.cpp | 64 ---------------------------------------- 1 file changed, 64 deletions(-) diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 457fd57..dbe20e3 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -102,31 +102,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 +125,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; From 9914c6c329f02504234216cc70b3691bc6d32d5b Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 17:11:36 -0300 Subject: [PATCH 05/34] =?UTF-8?q?=E2=9C=85=20test(serial):=20New=20test=20?= =?UTF-8?q?case=20WriteRawBasic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_pty.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index dbe20e3..3508a5a 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -666,3 +666,25 @@ TEST_F(PseudoTerminalTest, ReadUntilWithOverflowBuffer) { } }, libserial::IOException); } + +TEST_F(PseudoTerminalTest, WriteRawBasic) { + libserial::Serial serial_port; + + serial_port.open(slave_port_); + serial_port.setBaudRate(115200); + + std::vector data = {0x00, 0xFF, 0x10, 0x41, 0x00}; + + EXPECT_NO_THROW({ + ssize_t written = serial_port.writeRaw(data.data(), data.size()); + EXPECT_EQ(written, data.size()); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + uint8_t buffer[100] = {0}; + ssize_t bytes_read = read(master_fd_, buffer, sizeof(buffer)); + + ASSERT_EQ(bytes_read, data.size()); + EXPECT_EQ(std::vector(buffer, buffer + bytes_read), data); +} From 39d08dd8ba6a71e26cd1f776b4bb45f7b3cc66f1 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 17:15:18 -0300 Subject: [PATCH 06/34] =?UTF-8?q?=E2=9C=85=20test(serial):=20New=20test=20?= =?UTF-8?q?case=20WriteRawNullBuffer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_simple.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index 1351914..495b6cd 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -97,3 +97,15 @@ TEST_F(SerialTest, CloseWithInvalidFd) { EXPECT_EQ(msg, "Error closing port: Bad file descriptor"); } } + +TEST_F(SerialTest, WriteRawNullBuffer) { + libserial::Serial serial_port; + 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); +} From 703d777e9045f2548425e4dec979181024dec86b Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 17:17:18 -0300 Subject: [PATCH 07/34] =?UTF-8?q?=E2=9C=85=20test(serial):=20New=20test=20?= =?UTF-8?q?case=20WriteRawZeroSize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_simple.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index 495b6cd..848c7bd 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -109,3 +109,16 @@ TEST_F(SerialTest, WriteRawNullBuffer) { } }, libserial::IOException); } + +TEST_F(SerialTest, WriteRawZeroSize) { + libserial::Serial serial_port; + uint8_t dummy = 0; + EXPECT_THROW({ + try { + serial_port.writeRaw(&dummy, 0); + } catch (const libserial::IOException& e) { + EXPECT_STREQ("Invalid buffer passed to writeRaw", e.what()); + throw; + } + }, libserial::IOException); +} From 75b82ad0d31f40bbf9e16bf845eec57f46f963e4 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 19:03:30 -0300 Subject: [PATCH 08/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Remove=20redundant=20lambd=20wrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index e9e20fc..9bb9716 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -396,24 +396,18 @@ 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; } /* *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 From 487de0f35a94cbf5738fc03487252c5049decf08 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 20:35:58 -0300 Subject: [PATCH 09/34] =?UTF-8?q?=E2=9C=85=20test(serial):=20New=20test=20?= =?UTF-8?q?case=20write=20raw=20partial=20writes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 15 +++++++++++++++ src/serial.cpp | 4 ++-- test/test_serial_pty.cpp | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 9bb9716..9e1ebbf 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -404,6 +404,11 @@ void setReadSystemFunction( read_ = read_func; } +void setWriteSystemFunction( + std::function write_func) { + write_ = write_func; +} + /* *INDENT-OFF* */ void setIoctlSystemFunction( std::function ioctl_func) { // NOLINT @@ -445,6 +450,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 8544501..6153d80 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -47,7 +47,7 @@ void Serial::write(std::shared_ptr data) { throw IOException("Null pointer 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->c_str(), data->size()); if (bytes_written < 0) { throw IOException("Error writing to serial port: " + std::string(strerror(errno))); @@ -62,7 +62,7 @@ ssize_t Serial::writeRaw(const uint8_t* data, size_t size) { size_t total_written = 0; while (total_written < size) { - ssize_t ret = ::write(fd_serial_port_, + ssize_t ret = write_(fd_serial_port_, data + total_written, size - total_written); diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 3508a5a..b1111d1 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -688,3 +688,26 @@ TEST_F(PseudoTerminalTest, WriteRawBasic) { ASSERT_EQ(bytes_read, data.size()); EXPECT_EQ(std::vector(buffer, buffer + bytes_read), data); } + +TEST_F(PseudoTerminalTest, WriteRawPartialWrites) { + libserial::Serial serial_port; + serial_port.open(slave_port_); + + 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 +} From 4d6bd7ec58edd206c8a138fbec7529d904f1d477 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 15 Apr 2026 20:43:57 -0300 Subject: [PATCH 10/34] =?UTF-8?q?=E2=9C=85=20test(serial):=20New=20tests?= =?UTF-8?q?=20cases=20write=20raw=20Large=20buffer,=20with=20error=20and?= =?UTF-8?q?=20EINTR=20signal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_pty.cpp | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index b1111d1..89470bd 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -711,3 +711,60 @@ TEST_F(PseudoTerminalTest, WriteRawPartialWrites) { 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_); + + 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, WriteRawWithError) { + libserial::Serial serial_port; + serial_port.open(slave_port_); + + 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_); + + std::vector data(4096, 0xAA); + + EXPECT_NO_THROW({ + ssize_t written = serial_port.writeRaw(data.data(), data.size()); + EXPECT_EQ(written, data.size()); + }); +} \ No newline at end of file From f72521e539180fb0afcd35d097e9f9b1e33f3a5a Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Thu, 16 Apr 2026 10:00:29 -0300 Subject: [PATCH 11/34] =?UTF-8?q?=E2=9C=85=20test(ports):=20Fix=20ge=20dev?= =?UTF-8?q?ice=20populates=20list=20case=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_ports.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_ports.cpp b/test/test_ports.cpp index c12f2ed..0301296 100644 --- a/test/test_ports.cpp +++ b/test/test_ports.cpp @@ -111,8 +111,8 @@ TEST_F(PortsTest, GetDevicesPopulatesList) { }); EXPECT_EQ(devices.size(), 2); - EXPECT_EQ(devices[0].getName(), "usb-Device_Two_0002"); - EXPECT_EQ(devices[1].getName(), "usb-Device_One_0001"); + EXPECT_EQ(devices[0].getName(), "usb-Device_One_0001"); + EXPECT_EQ(devices[1].getName(), "usb-Device_Two_0002"); } From a7d40e36a27e269fb17eb6d9800775c40d3f1413 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Fri, 17 Apr 2026 16:48:09 -0300 Subject: [PATCH 12/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Use=20string=5Fview=20instead=20smart=20pointer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 20 +++++++++++++++++--- src/serial.cpp | 8 ++++---- test/test_serial_pty.cpp | 4 ++-- test/test_serial_simple.cpp | 20 ++++++++++---------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 9e1ebbf..1cf5c9e 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -96,14 +96,14 @@ void close(); * Sends the provided string data to the serial port. A carriage return * character ('\\r') is automatically appended to the data. * - * @param data Shared pointer to the string data to write + * @param data String view containing the data to write * @throws SerialException if write operation fails - * @throws std::invalid_argument if data pointer is null + * @throws std::invalid_argument if data is empty * * @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 Writes raw byte data to the serial port @@ -182,6 +182,20 @@ size_t readBytes(std::shared_ptr buffer, size_t num_bytes); */ size_t readUntil(std::shared_ptr 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 Pointer to a 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 * diff --git a/src/serial.cpp b/src/serial.cpp index 6153d80..8f920a7 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -42,12 +42,12 @@ 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))); diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 89470bd..12c20ab 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -219,7 +219,7 @@ TEST_F(PseudoTerminalTest, WriteTest) { 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); }); @@ -233,7 +233,7 @@ 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) { diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index 848c7bd..23a41b8 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -32,23 +32,23 @@ TEST_F(SerialTest, ConstructorWithInvalidPort) { }, libserial::SerialException); } -TEST_F(SerialTest, WriteWithSharedPtr) { - libserial::Serial serial; +// TEST_F(SerialTest, WriteWithSharedPtr) { +// libserial::Serial serial; - // Test that write function accepts shared_ptr - auto message = std::make_shared("Test message"); +// // 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); -} +// // 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; + std::string_view null_message; EXPECT_THROW({ serial.write(null_message); From e67153ef0da473f07c99a302a3b93197e597e61b Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Sat, 18 Apr 2026 11:37:25 -0300 Subject: [PATCH 13/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Use=20&string=20instead=20smart=20pointer=20in=20read=20functio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 2 +- src/serial.cpp | 18 ++++++++---------- test/test_serial_pty.cpp | 33 +++++++-------------------------- test/test_serial_simple.cpp | 13 ++----------- 4 files changed, 18 insertions(+), 48 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 1cf5c9e..194b3a7 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -147,7 +147,7 @@ ssize_t writeRaw(const std::vector& data); * * @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 diff --git a/src/serial.cpp b/src/serial.cpp index 8f920a7..06943f1 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -84,18 +84,15 @@ ssize_t Serial::writeRaw(const std::vector& data) { return writeRaw(data.data(), data.size()); } -size_t Serial::read(std::shared_ptr buffer) { +size_t Serial::read(std::string & buffer) { if (canonical_mode_ == CanonicalMode::DISABLE) { throw IOException( "read() is not supported in non-canonical mode; use readBytes() or readUntil() instead"); } - if (!buffer) { - throw IOException("Null pointer passed to read function"); - } - - buffer->clear(); - buffer->resize(max_safe_read_size_); + // if (buffer.empty()) { + // throw IOException("Empty buffer passed to read function"); + // } struct pollfd fd_poll; fd_poll.fd = fd_serial_port_; @@ -112,13 +109,14 @@ size_t Serial::read(std::shared_ptr buffer) { " milliseconds"); } + buffer.resize(max_safe_read_size_); + // Data available: do the read - ssize_t bytes_read = read_(fd_serial_port_, const_cast(buffer->data()), - 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); } diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 12c20ab..4a36b9e 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -242,7 +242,7 @@ TEST_F(PseudoTerminalTest, ReadCanonicalMode) { serial_port.open(slave_port_); serial_port.setBaudRate(9600); - const std::string test_message{"Smart Pointer Test!\n"}; + 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"; @@ -252,32 +252,13 @@ TEST_F(PseudoTerminalTest, ReadCanonicalMode) { 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.read(read_buffer); }); EXPECT_EQ(bytes_read, test_message.length()); - EXPECT_EQ(*read_buffer, test_message); -} - -TEST_F(PseudoTerminalTest, ReadWithNullBuffer) { - libserial::Serial serial_port; - - serial_port.open(slave_port_); - serial_port.setBaudRate(9600); - - std::shared_ptr null_buffer; - - EXPECT_THROW({ - try { - serial_port.read(null_buffer); - } - catch (const libserial::IOException& e) { - EXPECT_STREQ("Null pointer passed to read function", e.what()); - throw; - } - }, libserial::IOException); + EXPECT_EQ(read_buffer, test_message); } TEST_F(PseudoTerminalTest, ReadNonCanonicalMode) { @@ -297,7 +278,7 @@ TEST_F(PseudoTerminalTest, ReadNonCanonicalMode) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Attempt to read using read() - should throw exception - auto read_buffer = std::make_shared(); + std::string read_buffer; EXPECT_THROW({ try { @@ -322,7 +303,7 @@ TEST_F(PseudoTerminalTest, ReadTimeout) { int time_out_ms = 100; serial_port.setReadTimeout(std::chrono::milliseconds(time_out_ms)); - auto read_buffer = std::make_shared(); + std::string read_buffer; auto expected_what = "Read operation timed out after " + std::to_string(time_out_ms) + " milliseconds"; @@ -340,7 +321,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( @@ -369,7 +350,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( diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index 23a41b8..eb4e7dd 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -71,18 +71,9 @@ TEST_F(SerialTest, APIExists) { // Test new shared pointer read API - auto buffer = std::make_shared(); + 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); + // EXPECT_THROW(serial.readUntil(buffer, '\n'), libserial::IOException); } TEST_F(SerialTest, CloseWithInvalidFd) { From e7dacf37479f9d8a885d3d219a87c856671f81df Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Sat, 18 Apr 2026 21:36:57 -0300 Subject: [PATCH 14/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Use=20&string=20instead=20smart=20pointer=20in=20readBytes=20an?= =?UTF-8?q?d=20readUntil=20functions=20and=20it's=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 4 +-- src/serial.cpp | 32 ++++++------------- test/test_serial_pty.cpp | 61 +++++++----------------------------- 3 files changed, 23 insertions(+), 74 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 194b3a7..6bf60b8 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -164,7 +164,7 @@ size_t read(std::string & buffer); * * @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 @@ -180,7 +180,7 @@ size_t readBytes(std::shared_ptr buffer, size_t num_bytes); * @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 diff --git a/src/serial.cpp b/src/serial.cpp index 06943f1..e4c4abb 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -90,10 +90,6 @@ size_t Serial::read(std::string & buffer) { "read() is not supported in non-canonical mode; use readBytes() or readUntil() instead"); } - // if (buffer.empty()) { - // throw IOException("Empty buffer passed to read function"); - // } - struct pollfd fd_poll; fd_poll.fd = fd_serial_port_; fd_poll.events = POLLIN; @@ -120,46 +116,38 @@ size_t Serial::read(std::string & buffer) { 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"); @@ -210,10 +198,10 @@ size_t Serial::readUntil(std::shared_ptr buffer, char terminator) { } // Add the character to buffer (including terminator) - buffer->push_back(temp_char); + buffer.push_back(temp_char); } - return buffer->size(); + return buffer.size(); } void Serial::flushInputBuffer() { diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 4a36b9e..6aaf05b 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -390,33 +390,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) { @@ -426,7 +406,7 @@ TEST_F(PseudoTerminalTest, ReadBytesWithInvalidNumBytes) { serial_port.setBaudRate(9600); serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); - auto read_buffer = std::make_shared(); + std::string read_buffer; EXPECT_THROW({ try { @@ -446,7 +426,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( @@ -476,7 +456,7 @@ TEST_F(PseudoTerminalTest, ReadBytesCanonicalMode) { serial_port.setBaudRate(9600); serial_port.setCanonicalMode(libserial::CanonicalMode::ENABLE); - auto read_buffer = std::make_shared(); + std::string read_buffer; EXPECT_THROW({ try { @@ -508,30 +488,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) { @@ -550,14 +511,14 @@ TEST_F(PseudoTerminalTest, ReadUntilTimeout) { 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_THROW({serial_port.readUntil(read_buffer, '!'); }, libserial::IOException); } TEST_F(PseudoTerminalTest, ReadUntilWithReadFail) { libserial::Serial serial_port; - auto read_buffer = std::make_shared(); + std::string read_buffer; for (const auto& [error_num, error_msg] : errors_read_) { if (error_num == EAGAIN || error_num == EWOULDBLOCK) { @@ -590,7 +551,7 @@ TEST_F(PseudoTerminalTest, ReadUntilWithReadFail) { TEST_F(PseudoTerminalTest, ReadUntilWithPollFail) { 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( @@ -631,7 +592,7 @@ TEST_F(PseudoTerminalTest, ReadUntilWithOverflowBuffer) { 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; auto expected_what = "Read buffer exceeded maximum size limit of " + std::to_string(serial_port.getMaxSafeReadSize()) + From f39840111c721bb8b2139939e83db7804665ca73 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Sat, 18 Apr 2026 21:53:16 -0300 Subject: [PATCH 15/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Open=20the=20seria=20port=20in=20blocking=20by=20defalt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/serial.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/serial.cpp b/src/serial.cpp index e4c4abb..13c4389 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -22,14 +22,11 @@ Serial::~Serial() { } void Serial::open(const std::string& port) { - fd_serial_port_ = ::open(port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK); + fd_serial_port_ = ::open(port.c_str(), O_RDWR | O_NOCTTY); if (fd_serial_port_ == -1) { throw SerialException("Error opening port " + port + ": " + strerror(errno)); } - else { - fcntl(fd_serial_port_, F_SETFL, 0); - } } void Serial::close() { From f30e1f6bcedc50430581ff21420e375e5f4c37fa Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Tue, 21 Apr 2026 18:45:14 -0300 Subject: [PATCH 16/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Refactoring=20variables=20names=20and=20remove=20unecessary=20c?= =?UTF-8?q?omments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/serial.cpp | 17 ++++------------- test/test_serial_pty.cpp | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/serial.cpp b/src/serial.cpp index 13c4389..4ad12ca 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -84,27 +84,25 @@ ssize_t Serial::writeRaw(const std::vector& data) { size_t Serial::read(std::string & buffer) { if (canonical_mode_ == CanonicalMode::DISABLE) { throw IOException( - "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"); } 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"); } buffer.resize(max_safe_read_size_); - // Data available: do the read 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)); @@ -143,7 +141,6 @@ size_t Serial::readUntil(std::string & buffer, char terminator) { 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_) { throw IOException("Read buffer exceeded maximum size limit of " + std::to_string(max_safe_read_size_) + @@ -160,8 +157,6 @@ size_t Serial::readUntil(std::string & 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; @@ -178,23 +173,19 @@ size_t Serial::readUntil(std::string & 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); } diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 6aaf05b..fcdcf1d 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -286,7 +286,7 @@ TEST_F(PseudoTerminalTest, ReadNonCanonicalMode) { } 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; } From 0f8f002dd99f3b044fe360670e55d2c83ed8699f Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Tue, 21 Apr 2026 19:20:04 -0300 Subject: [PATCH 17/34] =?UTF-8?q?=E2=9C=A8=20feat(serial):=20Create=20a=20?= =?UTF-8?q?new=20read=20raw=20function=20with=20its=20unit=20test=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/serial.cpp | 65 +++++++++ test/test_serial_pty.cpp | 294 ++++++++++++++++++++++++++++++--------- 2 files changed, 297 insertions(+), 62 deletions(-) diff --git a/src/serial.cpp b/src/serial.cpp index 4ad12ca..c36f25e 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -192,6 +192,71 @@ size_t Serial::readUntil(std::string & buffer, char terminator) { 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 static_cast(total_read); +} + void Serial::flushInputBuffer() { if (ioctl_(fd_serial_port_, TCFLSH, TCIFLUSH) != 0) { throw SerialException("Error flushing input buffer: " + std::string(strerror(errno))); diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index fcdcf1d..332fe45 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -236,6 +236,108 @@ TEST_F(PseudoTerminalTest, WriteTest) { EXPECT_EQ(received, std::string(test_data)); } +TEST_F(PseudoTerminalTest, WriteRawBasic) { + libserial::Serial serial_port; + + serial_port.open(slave_port_); + serial_port.setBaudRate(115200); + + std::vector data = {0x00, 0xFF, 0x10, 0x41, 0x00}; + + EXPECT_NO_THROW({ + ssize_t written = serial_port.writeRaw(data.data(), data.size()); + EXPECT_EQ(written, data.size()); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + uint8_t buffer[100] = {0}; + ssize_t bytes_read = read(master_fd_, buffer, sizeof(buffer)); + + ASSERT_EQ(bytes_read, data.size()); + EXPECT_EQ(std::vector(buffer, buffer + bytes_read), data); +} + +TEST_F(PseudoTerminalTest, WriteRawPartialWrites) { + libserial::Serial serial_port; + serial_port.open(slave_port_); + + 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_); + + 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, WriteRawWithError) { + libserial::Serial serial_port; + serial_port.open(slave_port_); + + 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_); + + std::vector data(4096, 0xAA); + + EXPECT_NO_THROW({ + ssize_t written = serial_port.writeRaw(data.data(), data.size()); + EXPECT_EQ(written, data.size()); + }); +} + TEST_F(PseudoTerminalTest, ReadCanonicalMode) { libserial::Serial serial_port; @@ -609,104 +711,172 @@ TEST_F(PseudoTerminalTest, ReadUntilWithOverflowBuffer) { }, libserial::IOException); } -TEST_F(PseudoTerminalTest, WriteRawBasic) { - libserial::Serial serial_port; +TEST_F(PseudoTerminalTest, ReadRawCanonicalMode) { + libserial::Serial serial; - serial_port.open(slave_port_); - serial_port.setBaudRate(115200); + serial.open(slave_port_); + serial.setBaudRate(9600); - std::vector data = {0x00, 0xFF, 0x10, 0x41, 0x00}; + // Enable canonical mode + serial.setCanonicalMode(libserial::CanonicalMode::ENABLE); - EXPECT_NO_THROW({ - ssize_t written = serial_port.writeRaw(data.data(), data.size()); - EXPECT_EQ(written, data.size()); - }); + std::vector buffer(10); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_THROW({ + try { + 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); +} - uint8_t buffer[100] = {0}; - ssize_t bytes_read = read(master_fd_, buffer, sizeof(buffer)); +TEST_F(PseudoTerminalTest, ReadRawFullRead) { + libserial::Serial serial; + serial.open(slave_port_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setReadTimeout(std::chrono::milliseconds(500)); - ASSERT_EQ(bytes_read, data.size()); - EXPECT_EQ(std::vector(buffer, buffer + bytes_read), data); -} + const std::string msg = "HelloRaw"; + write(master_fd_, msg.data(), msg.size()); -TEST_F(PseudoTerminalTest, WriteRawPartialWrites) { - libserial::Serial serial_port; - serial_port.open(slave_port_); + std::vector buffer(msg.size()); - std::vector data = {1,2,3,4,5,6}; + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); - size_t call_count = 0; + EXPECT_EQ(n, msg.size()); + EXPECT_EQ(std::string(buffer.begin(), buffer.end()), msg); +} - serial_port.setWriteSystemFunction( - [&call_count](int, const void* buf, size_t len) -> ssize_t { - call_count++; +TEST_F(PseudoTerminalTest, ReadRawPartialTimeout) { + libserial::Serial serial; + serial.open(slave_port_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setReadTimeout(std::chrono::milliseconds(100)); - // Simulate partial writes (2 bytes per call) - size_t to_write = std::min(2, len); - return to_write; - }); + const std::string msg = "ABC"; + write(master_fd_, msg.data(), msg.size()); - ssize_t written = serial_port.writeRaw(data.data(), data.size()); + std::vector buffer(10); - EXPECT_EQ(written, data.size()); - EXPECT_GT(call_count, 1); // ensure loop was used + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); + + EXPECT_EQ(n, msg.size()); } -TEST_F(PseudoTerminalTest, WriteRawWithEINTR) { - libserial::Serial serial_port; - serial_port.open(slave_port_); +TEST_F(PseudoTerminalTest, ReadRawTimeoutNoData) { + libserial::Serial serial; + serial.open(slave_port_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); + serial.setReadTimeout(std::chrono::milliseconds(100)); - std::vector data = {1,2,3}; + std::vector buffer(10); - int call_count = 0; + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); - serial_port.setWriteSystemFunction( - [&call_count](int, const void*, size_t len) -> ssize_t { - if (call_count++ == 0) { - errno = EINTR; - return -1; - } - return len; + 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 }); - EXPECT_NO_THROW({ - ssize_t written = serial_port.writeRaw(data.data(), data.size()); - EXPECT_EQ(written, data.size()); - }); + std::vector buffer(10); + + ssize_t n = serial.readRaw(buffer.data(), buffer.size()); + + EXPECT_EQ(n, 0); } -TEST_F(PseudoTerminalTest, WriteRawWithError) { - libserial::Serial serial_port; - serial_port.open(slave_port_); +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; + }); - std::vector data = {1,2,3}; + std::vector buffer(10); - serial_port.setWriteSystemFunction( - [](int, const void*, size_t) -> ssize_t { + EXPECT_THROW({ + try { + 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; }); + std::vector buffer(10); + EXPECT_THROW({ try { - serial_port.writeRaw(data.data(), data.size()); + serial.readRaw(buffer.data(), buffer.size()); } catch (const libserial::IOException& e) { - EXPECT_STREQ("Error writing raw data: Input/output error", e.what()); + EXPECT_STREQ( + std::string("Error reading raw data: " + std::string(strerror(EIO))).c_str(), + e.what()); throw; } }, libserial::IOException); } -TEST_F(PseudoTerminalTest, WriteRawLargeBuffer) { - libserial::Serial serial_port; - serial_port.open(slave_port_); +TEST_F(PseudoTerminalTest, ReadRawMultipleChunks) { + libserial::Serial serial; + serial.setFdForTest(slave_fd_); + serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); - std::vector data(4096, 0xAA); + serial.setPollSystemFunction( + [](struct pollfd*, nfds_t, int) { return 1; }); - EXPECT_NO_THROW({ - ssize_t written = serial_port.writeRaw(data.data(), data.size()); - EXPECT_EQ(written, data.size()); - }); + 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'); } \ No newline at end of file From 3c4e9c0a686f6a97226831023fd057f617d5282f Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Tue, 21 Apr 2026 19:26:32 -0300 Subject: [PATCH 18/34] =?UTF-8?q?=F0=9F=8E=A8=20style(serial):=20Fix=20sty?= =?UTF-8?q?le=20erros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 2 +- src/serial.cpp | 6 +- test/test_serial_pty.cpp | 141 ++++++++++++++++++----------------- test/test_serial_simple.cpp | 11 +-- 4 files changed, 81 insertions(+), 79 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 6bf60b8..16f3aaf 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -466,7 +466,7 @@ std::function read_ = /** * @brief Write system call function wrapper - * + * * Allows injection of custom write function for testing. */ std::function write_ = diff --git a/src/serial.cpp b/src/serial.cpp index c36f25e..9b0c5f0 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -60,8 +60,8 @@ ssize_t Serial::writeRaw(const uint8_t* data, size_t size) { while (total_written < size) { ssize_t ret = write_(fd_serial_port_, - data + total_written, - size - total_written); + data + total_written, + size - total_written); if (ret < 0) { if (errno == EINTR) continue; @@ -197,7 +197,7 @@ ssize_t Serial::readRaw(uint8_t* buffer, size_t size) { 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"); } diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 332fe45..45b7135 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -268,12 +268,12 @@ TEST_F(PseudoTerminalTest, WriteRawPartialWrites) { serial_port.setWriteSystemFunction( [&call_count](int, const void* buf, size_t len) -> ssize_t { - call_count++; + call_count++; - // Simulate partial writes (2 bytes per call) - size_t to_write = std::min(2, len); - return to_write; - }); + // 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()); @@ -291,12 +291,12 @@ TEST_F(PseudoTerminalTest, WriteRawWithEINTR) { serial_port.setWriteSystemFunction( [&call_count](int, const void*, size_t len) -> ssize_t { - if (call_count++ == 0) { - errno = EINTR; - return -1; - } - return len; - }); + if (call_count++ == 0) { + errno = EINTR; + return -1; + } + return len; + }); EXPECT_NO_THROW({ ssize_t written = serial_port.writeRaw(data.data(), data.size()); @@ -312,14 +312,15 @@ TEST_F(PseudoTerminalTest, WriteRawWithError) { serial_port.setWriteSystemFunction( [](int, const void*, size_t) -> ssize_t { - errno = EIO; - return -1; - }); + errno = EIO; + return -1; + }); EXPECT_THROW({ try { serial_port.writeRaw(data.data(), data.size()); - } catch (const libserial::IOException& e) { + } + catch (const libserial::IOException& e) { EXPECT_STREQ("Error writing raw data: Input/output error", e.what()); throw; } @@ -330,9 +331,8 @@ TEST_F(PseudoTerminalTest, WriteRawLargeBuffer) { libserial::Serial serial_port; serial_port.open(slave_port_); - std::vector data(4096, 0xAA); - EXPECT_NO_THROW({ + std::vector data(4096, 0xAA); ssize_t written = serial_port.writeRaw(data.data(), data.size()); EXPECT_EQ(written, data.size()); }); @@ -353,14 +353,16 @@ TEST_F(PseudoTerminalTest, ReadCanonicalMode) { 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; + { + // 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_NO_THROW({ bytes_read = serial_port.read(read_buffer); }); - EXPECT_EQ(bytes_read, test_message.length()); - EXPECT_EQ(read_buffer, test_message); + EXPECT_EQ(bytes_read, test_message.length()); + EXPECT_EQ(read_buffer, test_message); + } } TEST_F(PseudoTerminalTest, ReadNonCanonicalMode) { @@ -379,11 +381,10 @@ TEST_F(PseudoTerminalTest, ReadNonCanonicalMode) { fsync(master_fd_); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - // Attempt to read using read() - should throw exception - std::string read_buffer; - 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) { @@ -405,13 +406,12 @@ TEST_F(PseudoTerminalTest, ReadTimeout) { int time_out_ms = 100; serial_port.setReadTimeout(std::chrono::milliseconds(time_out_ms)); - std::string read_buffer; - 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) { @@ -508,10 +508,9 @@ TEST_F(PseudoTerminalTest, ReadBytesWithInvalidNumBytes) { serial_port.setBaudRate(9600); serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); - std::string read_buffer; - EXPECT_THROW({ try { + std::string read_buffer; serial_port.readBytes(read_buffer, 0); } catch (const libserial::IOException& e) { @@ -558,10 +557,9 @@ TEST_F(PseudoTerminalTest, ReadBytesCanonicalMode) { serial_port.setBaudRate(9600); serial_port.setCanonicalMode(libserial::CanonicalMode::ENABLE); - std::string read_buffer; - EXPECT_THROW({ try { + std::string read_buffer; serial_port.readBytes(read_buffer, 5); ADD_FAILURE() << "Expected SerialException but no exception was thrown"; } @@ -612,15 +610,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 - std::string read_buffer; - - 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; - std::string read_buffer; for (const auto& [error_num, error_msg] : errors_read_) { if (error_num == EAGAIN || error_num == EWOULDBLOCK) { @@ -641,6 +639,7 @@ TEST_F(PseudoTerminalTest, ReadUntilWithReadFail) { EXPECT_THROW({ try { + std::string read_buffer; serial_port.readUntil(read_buffer, '!'); } catch (const libserial::IOException& e) { @@ -653,7 +652,6 @@ TEST_F(PseudoTerminalTest, ReadUntilWithReadFail) { TEST_F(PseudoTerminalTest, ReadUntilWithPollFail) { libserial::Serial serial_port; - std::string read_buffer; for (const auto& [error_num, error_msg] : errors_poll_) { serial_port.setPollSystemFunction( @@ -666,6 +664,7 @@ TEST_F(PseudoTerminalTest, ReadUntilWithPollFail) { EXPECT_THROW({ try { + std::string read_buffer; serial_port.readUntil(read_buffer, '!'); } catch (const libserial::IOException& e) { @@ -693,15 +692,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 - std::string read_buffer; - 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) { @@ -720,10 +717,9 @@ TEST_F(PseudoTerminalTest, ReadRawCanonicalMode) { // Enable canonical mode serial.setCanonicalMode(libserial::CanonicalMode::ENABLE); - std::vector buffer(10); - EXPECT_THROW({ try { + std::vector buffer(10); serial.readRaw(buffer.data(), buffer.size()); } catch (const libserial::IOException& e) { @@ -787,8 +783,8 @@ TEST_F(PseudoTerminalTest, ReadRawPollTimeoutSimulated) { serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); serial.setPollSystemFunction( [](struct pollfd*, nfds_t, int) { - return 0; // timeout - }); + return 0; // timeout + }); std::vector buffer(10); @@ -803,16 +799,16 @@ TEST_F(PseudoTerminalTest, ReadRawPollError) { serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); serial.setPollSystemFunction( [](struct pollfd*, nfds_t, int) { - errno = EINVAL; - return -1; - }); - - std::vector buffer(10); + errno = EINVAL; + return -1; + }); EXPECT_THROW({ try { + std::vector buffer(10); serial.readRaw(buffer.data(), buffer.size()); - } catch (const libserial::IOException& e) { + } + catch (const libserial::IOException& e) { EXPECT_STREQ( std::string("Error in poll(): " + std::string(strerror(EINVAL))).c_str(), e.what()); @@ -826,20 +822,22 @@ TEST_F(PseudoTerminalTest, ReadRawReadError) { serial.setFdForTest(slave_fd_); serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); serial.setPollSystemFunction( - [](struct pollfd*, nfds_t, int) { return 1; }); + [](struct pollfd*, nfds_t, int) { + return 1; + }); serial.setReadSystemFunction( [](int, void*, size_t) -> ssize_t { - errno = EIO; - return -1; - }); - - std::vector buffer(10); + errno = EIO; + return -1; + }); EXPECT_THROW({ try { + std::vector buffer(10); serial.readRaw(buffer.data(), buffer.size()); - } catch (const libserial::IOException& e) { + } + catch (const libserial::IOException& e) { EXPECT_STREQ( std::string("Error reading raw data: " + std::string(strerror(EIO))).c_str(), e.what()); @@ -854,23 +852,26 @@ TEST_F(PseudoTerminalTest, ReadRawMultipleChunks) { serial.setCanonicalMode(libserial::CanonicalMode::DISABLE); serial.setPollSystemFunction( - [](struct pollfd*, nfds_t, int) { return 1; }); + [](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; - } - }); + 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); diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index eb4e7dd..6e79872 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -47,10 +47,9 @@ TEST_F(SerialTest, ConstructorWithInvalidPort) { TEST_F(SerialTest, WriteWithNullPtr) { libserial::Serial serial; - // Test that write function handles null pointer - std::string_view null_message; - EXPECT_THROW({ + // Test that write function handles null pointer + std::string_view null_message; serial.write(null_message); }, libserial::SerialException); } @@ -94,7 +93,8 @@ TEST_F(SerialTest, WriteRawNullBuffer) { EXPECT_THROW({ try { serial_port.writeRaw(nullptr, 10); - } catch (const libserial::IOException& e) { + } + catch (const libserial::IOException& e) { EXPECT_STREQ("Invalid buffer passed to writeRaw", e.what()); throw; } @@ -107,7 +107,8 @@ TEST_F(SerialTest, WriteRawZeroSize) { EXPECT_THROW({ try { serial_port.writeRaw(&dummy, 0); - } catch (const libserial::IOException& e) { + } + catch (const libserial::IOException& e) { EXPECT_STREQ("Invalid buffer passed to writeRaw", e.what()); throw; } From 693d3261a34996f4471d43a458a7d7878623653e Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 08:37:11 -0300 Subject: [PATCH 19/34] =?UTF-8?q?=E2=9C=85=20test(ports):=20Search=20the?= =?UTF-8?q?=20device=20name=20to=20compare=20with=20the=20expect=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_ports.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/test_ports.cpp b/test/test_ports.cpp index 0301296..2de7d1a 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_One_0001"); - EXPECT_EQ(devices[1].getName(), "usb-Device_Two_0002"); + + 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()); } From 84481857e48455cebc344478cf77ef67fb1ae8b5 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 08:40:40 -0300 Subject: [PATCH 20/34] =?UTF-8?q?=F0=9F=8E=A8=20style(serial):=20Fix=20cpp?= =?UTF-8?q?lint=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/serial.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/serial.cpp b/src/serial.cpp index 9b0c5f0..8c4be2b 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 { From a42455e9476f205364314d59323a0a63a2568e51 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 08:42:27 -0300 Subject: [PATCH 21/34] =?UTF-8?q?=F0=9F=8E=A8=20style(serial):=20Fix=20cpp?= =?UTF-8?q?lint=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_ports.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ports.cpp b/test/test_ports.cpp index 2de7d1a..445d07b 100644 --- a/test/test_ports.cpp +++ b/test/test_ports.cpp @@ -116,7 +116,7 @@ TEST_F(PortsTest, GetDevicesPopulatesList) { } EXPECT_EQ(devices.size(), 2); - + 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()); } From dd71acb0034b73a86f2d948e8ca560da70ec9e30 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 08:51:51 -0300 Subject: [PATCH 22/34] =?UTF-8?q?=F0=9F=8E=A8=20style(serial):=20Fix=20cpp?= =?UTF-8?q?lint=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_pty.cpp | 21 +++++++++++---------- test/test_serial_simple.cpp | 3 +-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 45b7135..0882403 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" @@ -262,7 +263,7 @@ TEST_F(PseudoTerminalTest, WriteRawPartialWrites) { libserial::Serial serial_port; serial_port.open(slave_port_); - std::vector data = {1,2,3,4,5,6}; + std::vector data = {1, 2, 3, 4, 5, 6}; size_t call_count = 0; @@ -285,7 +286,7 @@ TEST_F(PseudoTerminalTest, WriteRawWithEINTR) { libserial::Serial serial_port; serial_port.open(slave_port_); - std::vector data = {1,2,3}; + std::vector data = {1, 2, 3}; int call_count = 0; @@ -308,7 +309,7 @@ TEST_F(PseudoTerminalTest, WriteRawWithError) { libserial::Serial serial_port; serial_port.open(slave_port_); - std::vector data = {1,2,3}; + std::vector data = {1, 2, 3}; serial_port.setWriteSystemFunction( [](int, const void*, size_t) -> ssize_t { @@ -880,4 +881,4 @@ TEST_F(PseudoTerminalTest, ReadRawMultipleChunks) { EXPECT_EQ(n, 2); EXPECT_EQ(buffer[0], 'A'); EXPECT_EQ(buffer[1], 'B'); -} \ No newline at end of file +} diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index 6e79872..4c49a93 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -69,10 +69,9 @@ TEST_F(SerialTest, APIExists) { libserial::SerialException); - // Test new shared pointer read API + // 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, CloseWithInvalidFd) { From 0206027d64681b09282431b67e904dd7358a96fb Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 08:57:29 -0300 Subject: [PATCH 23/34] =?UTF-8?q?=E2=9C=85=20test(ports):=20Remov=20unused?= =?UTF-8?q?=20test=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_simple.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index 4c49a93..2103313 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -32,18 +32,6 @@ TEST_F(SerialTest, ConstructorWithInvalidPort) { }, libserial::SerialException); } -// TEST_F(SerialTest, WriteWithSharedPtr) { -// 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; From 8443dc7b8b512fddc8af4a418cb02efa155c0cb4 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 12:25:32 -0300 Subject: [PATCH 24/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Open=20the=20port=20as=20non-blocking=20as=20default=20and=20th?= =?UTF-8?q?e=20turn=20to=20blocking=20to=20avoid=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/serial.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/serial.cpp b/src/serial.cpp index 8c4be2b..77e2361 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -23,11 +23,29 @@ Serial::~Serial() { } void Serial::open(const std::string& port) { - fd_serial_port_ = ::open(port.c_str(), O_RDWR | O_NOCTTY); + // 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)); } + + 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)); + } } void Serial::close() { From 78246997c2c32c99a04d6bc42b7d69691eb28c55 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 12:33:43 -0300 Subject: [PATCH 25/34] =?UTF-8?q?=F0=9F=93=9D=20docs(serial):=20Improve=20?= =?UTF-8?q?method=20descript=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 16f3aaf..9fb39f4 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -93,15 +93,12 @@ 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 String view containing the data to write - * @throws SerialException if write operation fails - * @throws std::invalid_argument if data is empty + * @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::string_view data); @@ -110,11 +107,10 @@ void write(std::string_view data); * * Sends the provided byte data to the serial port without any modification. * - * @param data Vector containing the byte data to write - * @param size Number of bytes to write from the data vector + * @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 SerialException if write operation fails - * @throws std::invalid_argument if data vector is empty + * @throws libserial::IOException if the write operation fails */ ssize_t writeRaw(const uint8_t* data, size_t size); @@ -127,8 +123,7 @@ ssize_t writeRaw(const uint8_t* data, size_t size); * * @param data Vector containing the byte data to write * @return Number of bytes actually written - * @throws SerialException if write operation fails - * @throws std::invalid_argument if data vector is empty + * @throws libserial::IOException if the write operation fails */ ssize_t writeRaw(const std::vector& data); @@ -142,8 +137,7 @@ ssize_t writeRaw(const std::vector& data); * * @param buffer Shared pointer to 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 */ From f7337a29ec063c6e68a71516297c3284b2ade938 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 12:54:09 -0300 Subject: [PATCH 26/34] =?UTF-8?q?=F0=9F=90=9B=20fix(serial):=20Dix=20readi?= =?UTF-8?q?ng=20a=20extra=20port=20from=20CLI=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/serialctl.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/serialctl.cpp b/tools/serialctl.cpp index 652880d..587a6af 100644 --- a/tools/serialctl.cpp +++ b/tools/serialctl.cpp @@ -36,8 +36,13 @@ int main(int argc, char** argv) { try { libserial::Ports ports; uint16_t num = ports.scanPorts(); - std::cout << "Found " << (num + 1) << " entries (index 0.." << num << ")\n"; - for (uint16_t i = 0; i <= num; ++i) { + 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); From 6eec7e2a14227f569fc6be1801dc74d97bceb0ae Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Wed, 22 Apr 2026 13:21:26 -0300 Subject: [PATCH 27/34] =?UTF-8?q?=F0=9F=93=9D=20docs(serial):=20Fix=20doxy?= =?UTF-8?q?gen=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 9fb39f4..ead5e0a 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -135,7 +135,7 @@ ssize_t writeRaw(const std::vector& data); * 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 Pointer to string where data will be stored * @return Number of bytes actually read * @throws libserial::IOException if the read operation fails * @@ -149,12 +149,12 @@ size_t read(std::string & buffer); * Reads exactly num_bytes from the serial port and stores them * in the provided shared string buffer. Just works in non-canonical mode. * - * @param buffer Shared pointer to string where data will be stored + * @param buffer Pointer to 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 */ @@ -167,9 +167,11 @@ size_t readBytes(std::string & 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 Pointer to 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 From 66106c90fdf248088b6ad4dac34af99994473d85 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Thu, 23 Apr 2026 13:14:22 -0300 Subject: [PATCH 28/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Add=20timeout=20to=20writeRaw=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/serial.cpp | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/serial.cpp b/src/serial.cpp index 77e2361..52ce523 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -71,25 +71,71 @@ void Serial::write(std::string_view data) { } ssize_t Serial::writeRaw(const uint8_t* data, size_t size) { + if (canonical_mode_ == CanonicalMode::ENABLE) { + throw IOException( + "writeRaw() is not supported in canonical mode; use write() instead"); + } + if (!data || size == 0) { throw IOException("Invalid buffer passed to writeRaw"); } 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); } From c70611e7f3ff0451371696fc9a31f5cae2f800e7 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Thu, 23 Apr 2026 13:15:09 -0300 Subject: [PATCH 29/34] =?UTF-8?q?=E2=9C=85=20test(serial):=20Add=20test=20?= =?UTF-8?q?case=20to=20check=20writeRaw=20timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_pty.cpp | 56 +++++++++++++++++++++++++++++++++++++ test/test_serial_simple.cpp | 25 ----------------- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 0882403..72955c1 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -241,6 +241,7 @@ TEST_F(PseudoTerminalTest, WriteRawBasic) { libserial::Serial serial_port; serial_port.open(slave_port_); + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); serial_port.setBaudRate(115200); std::vector data = {0x00, 0xFF, 0x10, 0x41, 0x00}; @@ -262,6 +263,7 @@ TEST_F(PseudoTerminalTest, WriteRawBasic) { 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}; @@ -285,6 +287,7 @@ TEST_F(PseudoTerminalTest, WriteRawPartialWrites) { 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}; @@ -308,6 +311,7 @@ TEST_F(PseudoTerminalTest, WriteRawWithEINTR) { 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}; @@ -331,6 +335,7 @@ TEST_F(PseudoTerminalTest, WriteRawWithError) { TEST_F(PseudoTerminalTest, WriteRawLargeBuffer) { libserial::Serial serial_port; serial_port.open(slave_port_); + serial_port.setCanonicalMode(libserial::CanonicalMode::DISABLE); EXPECT_NO_THROW({ std::vector data(4096, 0xAA); @@ -339,6 +344,57 @@ TEST_F(PseudoTerminalTest, WriteRawLargeBuffer) { }); } +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); +} + +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.writeRaw(&dummy, 0); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ("Invalid buffer passed to writeRaw", e.what()); + throw; + } + }, libserial::IOException); +} + TEST_F(PseudoTerminalTest, ReadCanonicalMode) { libserial::Serial serial_port; diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index 2103313..e6e8518 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -75,29 +75,4 @@ TEST_F(SerialTest, CloseWithInvalidFd) { } } -TEST_F(SerialTest, WriteRawNullBuffer) { - libserial::Serial serial_port; - 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(SerialTest, WriteRawZeroSize) { - libserial::Serial serial_port; - uint8_t dummy = 0; - EXPECT_THROW({ - try { - serial_port.writeRaw(&dummy, 0); - } - catch (const libserial::IOException& e) { - EXPECT_STREQ("Invalid buffer passed to writeRaw", e.what()); - throw; - } - }, libserial::IOException); -} From a502d69c93ab4e6ffc4e135a68b7455e48fffd74 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Thu, 23 Apr 2026 13:17:40 -0300 Subject: [PATCH 30/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serial):=20?= =?UTF-8?q?Include=20=20in=20serialctl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/serialctl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/serialctl.cpp b/tools/serialctl.cpp index 587a6af..05842a0 100644 --- a/tools/serialctl.cpp +++ b/tools/serialctl.cpp @@ -1,6 +1,7 @@ // Copyright 2025 Nestor Neto // Simple CLI for libserial: list serial ports +#include #include #include From 2aec37543f893faf5b8dccd38e5bd7a0c23519d6 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Thu, 23 Apr 2026 13:21:03 -0300 Subject: [PATCH 31/34] =?UTF-8?q?=F0=9F=93=9D=20docs(serial):=20Adjust=20r?= =?UTF-8?q?eadBytes=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index ead5e0a..fa582c2 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -144,9 +144,9 @@ ssize_t writeRaw(const std::vector& data); 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 + * Reads up to num_bytes from the serial port and stores them * in the provided shared string buffer. Just works in non-canonical mode. * * @param buffer Pointer to string where data will be stored From ba206709bbad8dfa977d958ccd055518bbdbd249 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Thu, 23 Apr 2026 13:24:52 -0300 Subject: [PATCH 32/34] =?UTF-8?q?=E2=9C=85=20test(serial):=20Update=20test?= =?UTF-8?q?=20name=20to=20match=20with=20new=20string=5Fview=20implementat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_simple.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_serial_simple.cpp b/test/test_serial_simple.cpp index e6e8518..14de5c2 100644 --- a/test/test_serial_simple.cpp +++ b/test/test_serial_simple.cpp @@ -32,11 +32,11 @@ TEST_F(SerialTest, ConstructorWithInvalidPort) { }, libserial::SerialException); } -TEST_F(SerialTest, WriteWithNullPtr) { +TEST_F(SerialTest, WriteWithEmptyStringView) { libserial::Serial serial; EXPECT_THROW({ - // Test that write function handles null pointer + // Test that write function handles a default-constructed empty string_view std::string_view null_message; serial.write(null_message); }, libserial::SerialException); From 83b4b0ded0cd5f4fd672fec57ac5e7f6b109b02b Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Thu, 23 Apr 2026 13:26:43 -0300 Subject: [PATCH 33/34] =?UTF-8?q?=E2=9C=85=20test(serial=5Fpty):=20Remove?= =?UTF-8?q?=20unecessary=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_serial_pty.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 72955c1..3496d20 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -219,10 +219,8 @@ TEST_F(PseudoTerminalTest, WriteTest) { serial_port.open(slave_port_); serial_port.setBaudRate(115200); - // Create test data using smart pointer 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 From a0cfe313ecfc0798b545e71d066d10b5dbfa05e9 Mon Sep 17 00:00:00 2001 From: Nestor Neto Date: Thu, 23 Apr 2026 13:34:14 -0300 Subject: [PATCH 34/34] =?UTF-8?q?=F0=9F=93=9D=20docs(serial):=20Adjust=20?= =?UTF-8?q?=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/libserial/serial.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index fa582c2..9b3679e 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -128,14 +128,14 @@ ssize_t writeRaw(const uint8_t* data, size_t size); ssize_t writeRaw(const std::vector& data); /** - * @brief Reads data from serial port into a shared pointer buffer + * @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 Pointer to string where data will be stored + * @param buffer String where data will be stored * @return Number of bytes actually read * @throws libserial::IOException if the read operation fails * @@ -147,9 +147,9 @@ size_t read(std::string & buffer); * @brief Reads a number of bytes from the serial port * * Reads up to num_bytes from the serial port and stores them - * in the provided shared string buffer. Just works in non-canonical mode. + * in the provided string buffer. Just works in non-canonical mode. * - * @param buffer 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 libserial::IOException if the read operation fails @@ -167,7 +167,7 @@ size_t readBytes(std::string & 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 Pointer to string where data will be stored + * @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 libserial::IOException if the read operation fails @@ -184,7 +184,7 @@ size_t readUntil(std::string & buffer, char terminator); * 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 Pointer to a byte array where data will be stored + * @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