From b3946e5d5c47f6fb288dffe5c8c72ff629fbc5b4 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Wed, 17 Jun 2026 22:36:49 +0200 Subject: [PATCH 1/7] Implement serial communication functions and refactor existing code - Added new functions for serial communication including: - `serialOutBytesTotal` - `serialOutBytesWaiting` - `serialReadLine` - `serialReadUntil` - `serialReadUntilSequence` - `serialSendBreak` - `serialSetBaudrate` - `serialSetDataBits` - `serialSetDtr` - `serialSetFlowControl` - `serialSetParity` - `serialSetReadCallback` - `serialSetRts` - `serialSetStopBits` - `serialSetWriteCallback` - Refactored `serialRead` and `serialWrite` functions to use a common implementation (`readImpl` and `writeImpl`). - Updated error handling to use new status code definitions. - Modified unit tests to reflect changes in error codes and added new tests for the newly implemented functions. - Ensured consistent naming and usage of status codes across the codebase. --- CMakeLists.txt | 35 +- src/detail/posix_helpers.hpp | 650 ++++++++++++++++++++++++++--- src/detail/posix_termios2.hpp | 30 ++ src/serial_abort_read.cpp | 21 + src/serial_abort_write.cpp | 21 + src/serial_clear_buffer_in.cpp | 28 ++ src/serial_clear_buffer_out.cpp | 34 ++ src/serial_close.cpp | 15 +- src/serial_close.test.cpp | 27 +- src/serial_drain.cpp | 28 ++ src/serial_extended_api.test.cpp | 199 +++++++++ src/serial_get_baudrate.cpp | 30 ++ src/serial_get_cts.cpp | 30 ++ src/serial_get_data_bits.cpp | 41 ++ src/serial_get_dcd.cpp | 30 ++ src/serial_get_dsr.cpp | 30 ++ src/serial_get_flow_control.cpp | 38 ++ src/serial_get_parity.cpp | 34 ++ src/serial_get_ri.cpp | 30 ++ src/serial_get_stop_bits.cpp | 30 ++ src/serial_in_bytes_total.cpp | 20 + src/serial_in_bytes_waiting.cpp | 29 ++ src/serial_list_ports.cpp | 134 ++++++ src/serial_monitor_ports.cpp | 144 +++++++ src/serial_open.cpp | 147 ++----- src/serial_open.test.cpp | 53 ++- src/serial_out_bytes_total.cpp | 20 + src/serial_out_bytes_waiting.cpp | 29 ++ src/serial_read.cpp | 95 +---- src/serial_read.test.cpp | 23 +- src/serial_read_line.cpp | 16 + src/serial_read_until.cpp | 22 + src/serial_read_until_sequence.cpp | 33 ++ src/serial_send_break.cpp | 44 ++ src/serial_set_baudrate.cpp | 44 ++ src/serial_set_data_bits.cpp | 44 ++ src/serial_set_dtr.cpp | 29 ++ src/serial_set_error_callback.cpp | 13 + src/serial_set_flow_control.cpp | 46 ++ src/serial_set_parity.cpp | 46 ++ src/serial_set_read_callback.cpp | 13 + src/serial_set_rts.cpp | 29 ++ src/serial_set_stop_bits.cpp | 46 ++ src/serial_set_write_callback.cpp | 13 + src/serial_write.cpp | 60 +-- src/serial_write.test.cpp | 24 +- src/test_helpers/error_capture.hpp | 2 +- tests/integration.test.cpp | 12 +- tests/serial_arduino.test.cpp | 17 +- 49 files changed, 2207 insertions(+), 421 deletions(-) create mode 100644 src/detail/posix_termios2.hpp create mode 100644 src/serial_abort_read.cpp create mode 100644 src/serial_abort_write.cpp create mode 100644 src/serial_clear_buffer_in.cpp create mode 100644 src/serial_clear_buffer_out.cpp create mode 100644 src/serial_drain.cpp create mode 100644 src/serial_extended_api.test.cpp create mode 100644 src/serial_get_baudrate.cpp create mode 100644 src/serial_get_cts.cpp create mode 100644 src/serial_get_data_bits.cpp create mode 100644 src/serial_get_dcd.cpp create mode 100644 src/serial_get_dsr.cpp create mode 100644 src/serial_get_flow_control.cpp create mode 100644 src/serial_get_parity.cpp create mode 100644 src/serial_get_ri.cpp create mode 100644 src/serial_get_stop_bits.cpp create mode 100644 src/serial_in_bytes_total.cpp create mode 100644 src/serial_in_bytes_waiting.cpp create mode 100644 src/serial_list_ports.cpp create mode 100644 src/serial_monitor_ports.cpp create mode 100644 src/serial_out_bytes_total.cpp create mode 100644 src/serial_out_bytes_waiting.cpp create mode 100644 src/serial_read_line.cpp create mode 100644 src/serial_read_until.cpp create mode 100644 src/serial_read_until_sequence.cpp create mode 100644 src/serial_send_break.cpp create mode 100644 src/serial_set_baudrate.cpp create mode 100644 src/serial_set_data_bits.cpp create mode 100644 src/serial_set_dtr.cpp create mode 100644 src/serial_set_error_callback.cpp create mode 100644 src/serial_set_flow_control.cpp create mode 100644 src/serial_set_parity.cpp create mode 100644 src/serial_set_read_callback.cpp create mode 100644 src/serial_set_rts.cpp create mode 100644 src/serial_set_stop_bits.cpp create mode 100644 src/serial_set_write_callback.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b17a2a1..b62aeaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,5 @@ cmake_minimum_required(VERSION 3.30) -# Export compile commands to root directory set(CMAKE_EXPORT_COMPILE_COMMANDS ON) include(cmake/CPM.cmake) @@ -11,9 +10,7 @@ CPMAddPackage( GIT_TAG v1.1.0 ) -# Include cmake-git-versioning module include(${cmake_git_versioning_SOURCE_DIR}/cmake/cmake-git-versioning.cmake) - get_git_version_info() project( @@ -23,29 +20,27 @@ project( LANGUAGES CXX ) +include(CTest) + file(WRITE "${CMAKE_BINARY_DIR}/env.sh" "PACKAGE_VERSION=${GIT_DESCRIBE_NO_V}") +generate_git_version( + OUTPUT_DIR ${CMAKE_BINARY_DIR}/generated + OUTPUT_FILE version.hpp +) -# Set C++ standard -set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) - -# Enable C++23 module support -set(CMAKE_CXX_MODULE_STD 23) +set(CMAKE_CXX_MODULE_STD 26) set(CMAKE_CXX_MODULE_EXTENSIONS OFF) CPMAddPackage( NAME cpp_core GITHUB_REPOSITORY Serial-IO/cpp-core - GIT_TAG main + GIT_TAG v2.0.0 OPTIONS "CMAKE_EXPORT_COMPILE_COMMANDS OFF" -) - -# Generate version information -generate_git_version( - OUTPUT_DIR ${CMAKE_BINARY_DIR}/generated - OUTPUT_FILE version.hpp + "CPP_CORE_ENABLE_AST_EXPORT ON" ) CPMAddPackage( @@ -57,7 +52,6 @@ CPMAddPackage( "gtest_force_shared_crt ON" ) -# Library sources: src/*.cpp only, exclude *.test.cpp and test_helpers/ file(GLOB_RECURSE LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") list(FILTER LIB_SOURCES EXCLUDE REGEX ".*\\.test\\.cpp$") list(FILTER LIB_SOURCES EXCLUDE REGEX ".*/test_helpers/.*") @@ -86,9 +80,8 @@ target_link_libraries( cpp_core::cpp_core ) -target_compile_features(cpp_bindings_linux PUBLIC cxx_std_23) +target_compile_features(cpp_bindings_linux PUBLIC cxx_std_26) -# Test sources: src/*.test.cpp, tests/*.test.cpp, src/test_helpers/*.cpp (helpers excluded from lib) file(GLOB SRC_UNIT_TESTS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.test.cpp") file(GLOB TESTS_INTEGRATION "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.test.cpp") file(GLOB TEST_HELPER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/test_helpers/*.cpp") @@ -114,7 +107,10 @@ if(TEST_SOURCES) GTest::gmock ) - target_compile_features(cpp_bindings_linux_tests PRIVATE cxx_std_23) + target_compile_features(cpp_bindings_linux_tests PRIVATE cxx_std_26) + + include(GoogleTest) + gtest_discover_tests(cpp_bindings_linux_tests) endif() include(GNUInstallDirs) @@ -133,7 +129,6 @@ install( FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" ) -# Copy generated version header install( FILES ${CMAKE_BINARY_DIR}/generated/version.hpp DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} diff --git a/src/detail/posix_helpers.hpp b/src/detail/posix_helpers.hpp index 8a78f6a..4a41852 100644 --- a/src/detail/posix_helpers.hpp +++ b/src/detail/posix_helpers.hpp @@ -1,126 +1,654 @@ #pragma once -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include #include +#include #include +#include "posix_termios2.hpp" + namespace cpp_bindings_linux::detail { -class UniqueFd +using IoCallbackT = void (*)(int); +using StatusCodeValue = cpp_core::StatusCodeValue; +using cpp_core::StatusCode; +using cpp_core::FlowControl; +using cpp_core::Parity; +using cpp_core::StopBits; + +enum class Operation +{ + kRead, + kWrite, +}; + +enum class WaitResult +{ + kReady, + kTimedOut, + kAborted, + kError, +}; + +struct PosixFdTraits +{ + using handle_type = int; + + static constexpr auto invalid() noexcept -> handle_type + { + return -1; + } + + static auto close(handle_type fd) noexcept -> void + { + if (fd >= 0) + { + ::close(fd); + } + } +}; + +using UniqueFd = cpp_core::UniqueResource; + +struct HandleState +{ + std::atomic bytes_read_total{0}; + std::atomic bytes_written_total{0}; + std::atomic abort_read{false}; + std::atomic abort_write{false}; +}; + +struct HandleContext +{ + int fd = -1; + std::shared_ptr state; +}; + +inline std::mutex g_handle_states_mutex; +inline std::unordered_map> g_handle_states; +inline std::atomic g_error_callback{nullptr}; +inline std::atomic g_read_callback{nullptr}; +inline std::atomic g_write_callback{nullptr}; + +template constexpr auto statusValue(Code code) -> StatusCodeValue +{ + return static_cast(code); +} + +inline auto effectiveErrorCallback(ErrorCallbackT error_callback) -> ErrorCallbackT +{ + return error_callback != nullptr ? error_callback : g_error_callback.load(std::memory_order_acquire); +} + +inline auto defaultValidationMessage(StatusCodeValue code) -> std::string_view +{ + switch (code) + { + case static_cast(StatusCode::Configuration::kSetBaudrateError): + return "Invalid baudrate: must be >= 300"; + case static_cast(StatusCode::Configuration::kSetDataBitsError): + return "Invalid data bits: must be 5-8"; + case static_cast(StatusCode::Configuration::kSetParityError): + return "Invalid parity: must be 0, 1, or 2"; + case static_cast(StatusCode::Configuration::kSetStopBitsError): + return "Invalid stop bits: must be 0, 1, or 2"; + case static_cast(StatusCode::Configuration::kSetFlowControlError): + return "Invalid flow control mode: must be 0, 1, or 2"; + default: + return "Invalid serial setting"; + } +} + +template +inline auto failMsg(ErrorCallbackT error_callback, StatusCodeValue code, std::string_view message) -> Ret +{ + return cpp_core::failMsg(effectiveErrorCallback(error_callback), code, message); +} + +template +inline auto failErrno(ErrorCallbackT error_callback, StatusCodeValue code) -> Ret +{ + return cpp_core::failMsg(effectiveErrorCallback(error_callback), code, + std::error_code(errno, std::generic_category()).message()); +} + +template inline auto failValidation(ErrorCallbackT error_callback, StatusCodeValue code) -> Ret +{ + return failMsg(error_callback, code, defaultValidationMessage(code)); +} + +inline auto ensureHandleState(int fd) -> std::shared_ptr +{ + std::lock_guard lock(g_handle_states_mutex); + auto &state = g_handle_states[fd]; + if (!state) + { + state = std::make_shared(); + } + return state; +} + +inline auto removeHandleState(int fd) -> void +{ + std::lock_guard lock(g_handle_states_mutex); + g_handle_states.erase(fd); +} + +template +inline auto validatePosixFd(int64_t handle, ErrorCallbackT error_callback, int *out_fd) -> Ret +{ + const auto rc = cpp_core::validateHandle(handle, effectiveErrorCallback(error_callback)); + if (rc < 0) + { + return rc; + } + + *out_fd = static_cast(handle); + return static_cast(StatusCode::kSuccess); +} + +template +inline auto acquireHandleContext(int64_t handle, ErrorCallbackT error_callback, HandleContext *out_context) -> Ret +{ + int fd = -1; + const auto rc = validatePosixFd(handle, error_callback, &fd); + if (rc < 0) + { + return rc; + } + + out_context->fd = fd; + out_context->state = ensureHandleState(fd); + return static_cast(StatusCode::kSuccess); +} + +inline auto registerOpenedHandle(int fd) -> void +{ + (void)ensureHandleState(fd); +} + +inline auto setAbortFlag(const std::shared_ptr &state, Operation operation) -> void +{ + if (operation == Operation::kRead) + { + state->abort_read.store(true, std::memory_order_release); + } + else + { + state->abort_write.store(true, std::memory_order_release); + } +} + +inline auto consumeAbortFlag(const std::shared_ptr &state, Operation operation) -> bool +{ + if (operation == Operation::kRead) + { + return state->abort_read.exchange(false, std::memory_order_acq_rel); + } + + return state->abort_write.exchange(false, std::memory_order_acq_rel); +} + +inline auto invokeIoCallback(IoCallbackT callback, int bytes) -> void { - public: - UniqueFd() = default; - explicit UniqueFd(int in_fd) : fd_(in_fd) + if (callback != nullptr) { + callback(bytes); } +} + +inline auto noteBytesRead(const std::shared_ptr &state, int bytes_read) -> void +{ + state->bytes_read_total.fetch_add(bytes_read, std::memory_order_relaxed); + invokeIoCallback(g_read_callback.load(std::memory_order_acquire), bytes_read); +} + +inline auto noteBytesWritten(const std::shared_ptr &state, int bytes_written) -> void +{ + state->bytes_written_total.fetch_add(bytes_written, std::memory_order_relaxed); + invokeIoCallback(g_write_callback.load(std::memory_order_acquire), bytes_written); +} - UniqueFd(const UniqueFd &) = delete; - auto operator=(const UniqueFd &) -> UniqueFd & = delete; +inline auto bytesReadTotal(const std::shared_ptr &state) -> int64_t +{ + return state->bytes_read_total.load(std::memory_order_relaxed); +} - UniqueFd(UniqueFd &&other) noexcept : fd_(other.fd_) +inline auto bytesWrittenTotal(const std::shared_ptr &state) -> int64_t +{ + return state->bytes_written_total.load(std::memory_order_relaxed); +} + +inline auto waitFdReady(const std::shared_ptr &state, int file_descriptor, int timeout_ms, + Operation operation) -> WaitResult +{ + if (consumeAbortFlag(state, operation)) { - other.fd_ = -1; + return WaitResult::kAborted; } - auto operator=(UniqueFd &&other) noexcept -> UniqueFd & + + const short requested_events = operation == Operation::kRead ? POLLIN : POLLOUT; + int remaining_timeout_ms = std::max(timeout_ms, 0); + + while (true) { - if (this != &other) + pollfd poll_fd{ + .fd = file_descriptor, + .events = requested_events, + .revents = 0, + }; + + const int slice_timeout_ms = timeout_ms == 0 ? 0 : std::min(remaining_timeout_ms, 50); + const int poll_result = poll(&poll_fd, 1, slice_timeout_ms); + if (poll_result < 0) + { + if (errno == EINTR) + { + continue; + } + return WaitResult::kError; + } + + if (poll_result > 0) + { + if ((poll_fd.revents & requested_events) != 0 || (poll_fd.revents & (POLLERR | POLLHUP)) != 0) + { + return WaitResult::kReady; + } + } + + if (consumeAbortFlag(state, operation)) + { + return WaitResult::kAborted; + } + + if (timeout_ms == 0) + { + return WaitResult::kTimedOut; + } + + remaining_timeout_ms -= slice_timeout_ms; + if (remaining_timeout_ms <= 0) { - reset(other.release()); + return WaitResult::kTimedOut; } - return *this; } +} + +inline auto multiplierTimeout(int timeout_ms, int multiplier) -> int +{ + if (multiplier <= 0) + { + return 0; + } + + return cpp_core::clampTimeout(timeout_ms) * multiplier; +} - ~UniqueFd() +inline auto validateBaudrateValue(int baudrate) -> cpp_core::Status +{ + if (cpp_core::SerialConfig::tryMake(baudrate, 8)) { - reset(-1); + return cpp_core::ok(); } + return cpp_core::fail<>(statusValue(StatusCode::Configuration::kSetBaudrateError), + std::string(defaultValidationMessage( + statusValue(StatusCode::Configuration::kSetBaudrateError)))); +} - [[nodiscard]] auto get() const -> int +inline auto validateDataBitsValue(int data_bits) -> cpp_core::Status +{ + if (cpp_core::SerialConfig::tryMake(300, data_bits)) { - return fd_; + return cpp_core::ok(); } - [[nodiscard]] auto valid() const -> bool + return cpp_core::fail<>(statusValue(StatusCode::Configuration::kSetDataBitsError), + std::string(defaultValidationMessage( + statusValue(StatusCode::Configuration::kSetDataBitsError)))); +} + +inline auto parseParity(int parity, ErrorCallbackT error_callback, StatusCodeValue invalid_code) -> std::optional +{ + switch (parity) { - return fd_ >= 0; + case 0: + return Parity::kNone; + case 1: + return Parity::kEven; + case 2: + return Parity::kOdd; + default: + (void)failValidation(error_callback, invalid_code); + return std::nullopt; } +} - auto reset(int new_fd) -> void +inline auto parseStopBits(int stop_bits, ErrorCallbackT error_callback, StatusCodeValue invalid_code, + bool allow_one_alias = true) -> std::optional +{ + switch (stop_bits) { - if (fd_ >= 0) + case 0: + return StopBits::kOne; + case 1: + if (allow_one_alias) { - close(fd_); + return StopBits::kOne; } - fd_ = new_fd; + break; + case 2: + return StopBits::kTwo; + default: + break; } - [[nodiscard]] auto release() -> int + (void)failValidation(error_callback, invalid_code); + return std::nullopt; +} + +inline auto parseFlowControl(int mode, ErrorCallbackT error_callback, StatusCodeValue invalid_code) + -> std::optional +{ + switch (mode) { - const int out = fd_; - fd_ = -1; - return out; + case 0: + return FlowControl::kNone; + case 1: + return FlowControl::kRtsCts; + case 2: + return FlowControl::kXonXoff; + default: + (void)failValidation(error_callback, invalid_code); + return std::nullopt; } +} - private: - int fd_ = -1; -}; +template +inline auto readTermios2(int fd, termios2 *tty, ErrorCallbackT error_callback) -> Ret +{ + if (ioctl(fd, TCGETS2, tty) != 0) + { + return failErrno(error_callback, statusValue(StatusCode::Control::kGetStateError)); + } + return static_cast(StatusCode::kSuccess); +} + +template +inline auto writeTermios2(int fd, termios2 *tty, ErrorCallbackT error_callback, StatusCodeValue set_error_code) -> Ret +{ + if (ioctl(fd, TCSETS2, tty) != 0) + { + return failErrno(error_callback, set_error_code); + } + return static_cast(StatusCode::kSuccess); +} -template -inline auto invokeErrorCallback(Callback error_callback, cpp_core::StatusCodes code, const char *message) -> void +inline auto applyBaudrate(termios2 *tty, int baudrate) -> void { - if (error_callback != nullptr) + tty->c_cflag &= ~CBAUD; + tty->c_cflag |= BOTHER; + tty->c_ispeed = static_cast(baudrate); + tty->c_ospeed = static_cast(baudrate); +} + +inline auto applyDataBits(termios2 *tty, int data_bits) -> void +{ + tty->c_cflag &= ~CSIZE; + switch (data_bits) { - error_callback(static_cast(code), message); + case 5: + tty->c_cflag |= CS5; + break; + case 6: + tty->c_cflag |= CS6; + break; + case 7: + tty->c_cflag |= CS7; + break; + case 8: + default: + tty->c_cflag |= CS8; + break; } } -template -inline auto failMsg(Callback error_callback, cpp_core::StatusCodes code, const char *message) -> Ret +inline auto applyParity(termios2 *tty, Parity parity) -> void { - invokeErrorCallback(error_callback, code, message); - return static_cast(code); + tty->c_cflag &= ~(PARENB | PARODD); + if (parity == Parity::kEven) + { + tty->c_cflag |= PARENB; + } + else if (parity == Parity::kOdd) + { + tty->c_cflag |= (PARENB | PARODD); + } } -template -inline auto failErrno(Callback error_callback, cpp_core::StatusCodes code) -> Ret +inline auto applyStopBits(termios2 *tty, StopBits stop_bits) -> void { - if (error_callback != nullptr) + if (stop_bits == StopBits::kTwo) + { + tty->c_cflag |= CSTOPB; + } + else { - const std::string error_msg = std::error_code(errno, std::generic_category()).message(); - error_callback(static_cast(code), error_msg.c_str()); + tty->c_cflag &= ~CSTOPB; } - return static_cast(code); } -// Poll helper used by read/write to implement timeouts. -// Returns: -1 on poll error, 0 on timeout/not-ready, 1 on ready. -inline auto waitFdReady(int file_descriptor, int timeout_ms, bool for_read) -> int +inline auto applyFlowControl(termios2 *tty, FlowControl flow_control) -> void { - struct pollfd poll_fd = {}; - poll_fd.fd = file_descriptor; - poll_fd.events = for_read ? POLLIN : POLLOUT; - poll_fd.revents = 0; + tty->c_cflag &= ~CRTSCTS; + tty->c_iflag &= ~(IXON | IXOFF | IXANY); - const int poll_result = poll(&poll_fd, 1, timeout_ms); - if (poll_result < 0) + if (flow_control == FlowControl::kRtsCts) { - return -1; + tty->c_cflag |= CRTSCTS; } - if (poll_result == 0) + else if (flow_control == FlowControl::kXonXoff) { - return 0; + tty->c_iflag |= (IXON | IXOFF); + } +} + +inline auto trimWhitespace(std::string value) -> std::string +{ + const auto is_space = [](unsigned char ch) { return std::isspace(ch) != 0; }; + while (!value.empty() && is_space(static_cast(value.front()))) + { + value.erase(value.begin()); + } + while (!value.empty() && is_space(static_cast(value.back()))) + { + value.pop_back(); + } + return value; +} + +inline auto readTrimmedFile(const std::filesystem::path &path) -> std::optional +{ + std::ifstream stream(path); + if (!stream.is_open()) + { + return std::nullopt; + } + + std::string contents((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + contents = trimWhitespace(std::move(contents)); + if (contents.empty()) + { + return std::nullopt; } - if (for_read && ((poll_fd.revents & POLLIN) != 0)) + return contents; +} + +inline auto isSerialDeviceName(std::string_view name) -> bool +{ + static constexpr std::string_view kPrefixes[] = {"ttyUSB", "ttyACM", "ttyS", "ttyAMA", "rfcomm", "ttyTHS"}; + return std::ranges::any_of(kPrefixes, [name](std::string_view prefix) { return name.starts_with(prefix); }); +} + +inline auto matchesSuffix(const unsigned char *buffer, int buffer_size, const unsigned char *terminator, + int terminator_size) -> bool +{ + if (terminator == nullptr || terminator_size <= 0 || buffer_size < terminator_size) + { + return false; + } + + return std::memcmp(buffer + (buffer_size - terminator_size), terminator, + static_cast(terminator_size)) == 0; +} + +inline auto readImpl(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + const unsigned char *terminator, int terminator_size, ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_rc = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_rc < 0) + { + return buffer_rc; + } + + if (terminator_size > 0 && terminator == nullptr) + { + return failMsg(error_callback, statusValue(StatusCode::Io::kBufferError), "Invalid terminator"); + } + + HandleContext context; + const auto handle_rc = acquireHandleContext(handle, callback, &context); + if (handle_rc < 0) + { + return handle_rc; + } + + auto *output = static_cast(buffer); + const bool read_single_bytes = terminator_size > 0; + int total_read = 0; + + while (total_read < buffer_size) + { + const int current_timeout_ms = + total_read == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + switch (waitFdReady(context.state, context.fd, current_timeout_ms, Operation::kRead)) + { + case WaitResult::kTimedOut: + return total_read; + case WaitResult::kAborted: + return failMsg(error_callback, statusValue(StatusCode::Io::kAbortReadError), "Read aborted"); + case WaitResult::kError: + return failErrno(error_callback, statusValue(StatusCode::Io::kReadError)); + case WaitResult::kReady: + break; + } + + const int chunk_size = read_single_bytes ? 1 : (buffer_size - total_read); + const ssize_t bytes_read = ::read(context.fd, output + total_read, static_cast(chunk_size)); + if (bytes_read < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + continue; + } + return failErrno(error_callback, statusValue(StatusCode::Io::kReadError)); + } + + if (bytes_read == 0) + { + return total_read; + } + + noteBytesRead(context.state, static_cast(bytes_read)); + total_read += static_cast(bytes_read); + + if (matchesSuffix(output, total_read, terminator, terminator_size)) + { + return total_read; + } + } + + return total_read; +} + +inline auto writeImpl(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, + ErrorCallbackT error_callback) -> int +{ + const auto callback = effectiveErrorCallback(error_callback); + const auto buffer_rc = cpp_core::validateBuffer(buffer, buffer_size, callback); + if (buffer_rc < 0) + { + return buffer_rc; + } + + HandleContext context; + const auto handle_rc = acquireHandleContext(handle, callback, &context); + if (handle_rc < 0) { - return 1; + return handle_rc; } - if (!for_read && ((poll_fd.revents & POLLOUT) != 0)) + + const auto *input = static_cast(buffer); + int total_written = 0; + + while (total_written < buffer_size) { - return 1; + const int current_timeout_ms = + total_written == 0 ? cpp_core::clampTimeout(timeout_ms) : multiplierTimeout(timeout_ms, multiplier); + switch (waitFdReady(context.state, context.fd, current_timeout_ms, Operation::kWrite)) + { + case WaitResult::kTimedOut: + return total_written; + case WaitResult::kAborted: + return failMsg(error_callback, statusValue(StatusCode::Io::kAbortWriteError), "Write aborted"); + case WaitResult::kError: + return failErrno(error_callback, statusValue(StatusCode::Io::kWriteError)); + case WaitResult::kReady: + break; + } + + const ssize_t bytes_written = + ::write(context.fd, input + total_written, static_cast(buffer_size - total_written)); + if (bytes_written < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + continue; + } + return failErrno(error_callback, statusValue(StatusCode::Io::kWriteError)); + } + + if (bytes_written == 0) + { + return total_written; + } + + noteBytesWritten(context.state, static_cast(bytes_written)); + total_written += static_cast(bytes_written); } - return 0; + + return total_written; } + } // namespace cpp_bindings_linux::detail diff --git a/src/detail/posix_termios2.hpp b/src/detail/posix_termios2.hpp new file mode 100644 index 0000000..e6d92ad --- /dev/null +++ b/src/detail/posix_termios2.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +#ifndef TCGETS2 +#define TCGETS2 0x802C542A +#define TCSETS2 0x402C542B +#endif + +#ifndef BOTHER +#define BOTHER 0x010000 +#endif + +#ifndef CRTSCTS +#define CRTSCTS 020000000000 +#endif + +// NOLINTBEGIN +struct termios2 +{ + tcflag_t c_iflag; + tcflag_t c_oflag; + tcflag_t c_cflag; + tcflag_t c_lflag; + cc_t c_line; + cc_t c_cc[19]; + speed_t c_ispeed; + speed_t c_ospeed; +}; +// NOLINTEND diff --git a/src/serial_abort_read.cpp b/src/serial_abort_read.cpp new file mode 100644 index 0000000..ab247a5 --- /dev/null +++ b/src/serial_abort_read.cpp @@ -0,0 +1,21 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialAbortRead(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + cpp_bindings_linux::detail::setAbortFlag(context.state, cpp_bindings_linux::detail::Operation::kRead); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_abort_write.cpp b/src/serial_abort_write.cpp new file mode 100644 index 0000000..a102910 --- /dev/null +++ b/src/serial_abort_write.cpp @@ -0,0 +1,21 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialAbortWrite(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + cpp_bindings_linux::detail::setAbortFlag(context.state, cpp_bindings_linux::detail::Operation::kWrite); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_clear_buffer_in.cpp b/src/serial_clear_buffer_in.cpp new file mode 100644 index 0000000..09852f4 --- /dev/null +++ b/src/serial_clear_buffer_in.cpp @@ -0,0 +1,28 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialClearBufferIn(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + if (tcflush(context.fd, TCIFLUSH) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Io::kClearBufferInError)); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_clear_buffer_out.cpp b/src/serial_clear_buffer_out.cpp new file mode 100644 index 0000000..bc58772 --- /dev/null +++ b/src/serial_clear_buffer_out.cpp @@ -0,0 +1,34 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialClearBufferOut(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + if (tcdrain(context.fd) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Io::kClearBufferOutError)); + } + + if (tcflush(context.fd, TCOFLUSH) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Io::kClearBufferOutError)); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_close.cpp b/src/serial_close.cpp index 6ef9cea..e124743 100644 --- a/src/serial_close.cpp +++ b/src/serial_close.cpp @@ -1,5 +1,4 @@ #include -#include #include "detail/posix_helpers.hpp" @@ -13,21 +12,25 @@ extern "C" { if (handle <= 0) { - return static_cast(cpp_core::StatusCodes::kSuccess); + return static_cast(cpp_core::StatusCode::kSuccess); } + if (handle > std::numeric_limits::max()) { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, - "Invalid handle"); + return cpp_bindings_linux::detail::failMsg( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Connection::kInvalidHandleError), + "Invalid handle"); } const int fd = static_cast(handle); if (close(fd) != 0) { - return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kCloseHandleError); + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Connection::kCloseHandleError)); } - return static_cast(cpp_core::StatusCodes::kSuccess); + cpp_bindings_linux::detail::removeHandleState(fd); + return static_cast(cpp_core::StatusCode::kSuccess); } } // extern "C" diff --git a/src/serial_close.test.cpp b/src/serial_close.test.cpp index 5b89b64..07d66fb 100644 --- a/src/serial_close.test.cpp +++ b/src/serial_close.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include @@ -7,6 +7,13 @@ #include "test_helpers/error_capture.hpp" +namespace +{ +constexpr auto kSuccess = static_cast(cpp_core::StatusCode::kSuccess); +constexpr auto kInvalidHandleError = static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError); +constexpr auto kCloseHandleError = static_cast(cpp_core::StatusCode::Connection::kCloseHandleError); +} // namespace + class SerialCloseTest : public ::testing::Test { protected: @@ -29,21 +36,21 @@ TEST_F(SerialCloseTest, CloseInvalidHandleZero) { int result = serialClose(0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, kSuccess); } TEST_F(SerialCloseTest, CloseInvalidHandleNegative) { int result = serialClose(-1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, kSuccess); } TEST_F(SerialCloseTest, CloseInvalidHandleNegativeLarge) { int result = serialClose(-12345, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, kSuccess); } TEST_F(SerialCloseTest, CloseInvalidHandleTooLarge) @@ -51,7 +58,7 @@ TEST_F(SerialCloseTest, CloseInvalidHandleTooLarge) auto too_large_handle = static_cast(std::numeric_limits::max()) + 1; int result = serialClose(too_large_handle, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, kInvalidHandleError); } TEST_F(SerialCloseTest, CloseInvalidHandleIntMaxBoundary) @@ -59,23 +66,19 @@ TEST_F(SerialCloseTest, CloseInvalidHandleIntMaxBoundary) auto handle = static_cast(std::numeric_limits::max()); int result = serialClose(handle, error_callback); - // Should fail because this fd doesn't exist, but not with InvalidHandleError - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_NE(result, kInvalidHandleError); } TEST_F(SerialCloseTest, CloseNoErrorCallback) { int result = serialClose(0, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, kSuccess); } TEST_F(SerialCloseTest, CloseInvalidHandle) { - // Test closing a real invalid fd (one that never existed) - // We should get an error but not crash int result = serialClose(9999, error_callback); - // This will fail because the fd doesn't exist - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kCloseHandleError)); + EXPECT_EQ(result, kCloseHandleError); } diff --git a/src/serial_drain.cpp b/src/serial_drain.cpp new file mode 100644 index 0000000..9276d0e --- /dev/null +++ b/src/serial_drain.cpp @@ -0,0 +1,28 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialDrain(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + if (tcdrain(context.fd) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Io::kWriteError)); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_extended_api.test.cpp b/src/serial_extended_api.test.cpp new file mode 100644 index 0000000..6e2487d --- /dev/null +++ b/src/serial_extended_api.test.cpp @@ -0,0 +1,199 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ +std::atomic g_last_error_code{0}; +std::atomic g_last_read_callback{0}; +std::atomic g_last_write_callback{0}; +std::atomic g_port_callback_count{0}; + +void globalErrorCallback(int code, const char * /*message*/) +{ + g_last_error_code.store(code, std::memory_order_relaxed); +} + +void globalReadCallback(int bytes_read) +{ + g_last_read_callback.store(bytes_read, std::memory_order_relaxed); +} + +void globalWriteCallback(int bytes_written) +{ + g_last_write_callback.store(bytes_written, std::memory_order_relaxed); +} + +void listPortsCallback(const char * /*port*/, const char * /*path*/, const char * /*manufacturer*/, + const char * /*serial_number*/, const char * /*pnp_id*/, const char * /*location_id*/, + const char * /*product_id*/, const char * /*vendor_id*/) +{ + g_port_callback_count.fetch_add(1, std::memory_order_relaxed); +} + +constexpr auto kAbortReadError = static_cast(cpp_core::StatusCode::Io::kAbortReadError); +constexpr auto kInvalidHandleError = static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError); +} // namespace + +class SerialExtendedApiTest : public ::testing::Test +{ + protected: + void SetUp() override + { + g_last_error_code.store(0, std::memory_order_relaxed); + g_last_read_callback.store(0, std::memory_order_relaxed); + g_last_write_callback.store(0, std::memory_order_relaxed); + g_port_callback_count.store(0, std::memory_order_relaxed); + serialSetErrorCallback(nullptr); + serialSetReadCallback(nullptr); + serialSetWriteCallback(nullptr); + } +}; + +TEST_F(SerialExtendedApiTest, GlobalErrorCallbackActsAsFallback) +{ + serialSetErrorCallback(globalErrorCallback); + + std::array buffer{}; + EXPECT_EQ(serialRead(-1, buffer.data(), static_cast(buffer.size()), 10, 1, nullptr), kInvalidHandleError); + EXPECT_EQ(g_last_error_code.load(std::memory_order_relaxed), kInvalidHandleError); +} + +TEST_F(SerialExtendedApiTest, ReadWriteCallbacksAndTotalsTrackPipeHandles) +{ + std::array pipefd{}; + ASSERT_EQ(pipe(pipefd.data()), 0); + fcntl(pipefd[0], F_SETFL, O_NONBLOCK); + fcntl(pipefd[1], F_SETFL, O_NONBLOCK); + + serialSetReadCallback(globalReadCallback); + serialSetWriteCallback(globalWriteCallback); + + const char *message = "hello"; + ASSERT_EQ(serialWrite(pipefd[1], message, 5, 100, 1, nullptr), 5); + + std::array buffer{}; + ASSERT_EQ(serialInBytesWaiting(pipefd[0], nullptr), 5); + ASSERT_EQ(serialRead(pipefd[0], buffer.data(), 5, 100, 1, nullptr), 5); + + EXPECT_EQ(std::string(buffer.data(), 5), "hello"); + EXPECT_EQ(g_last_write_callback.load(std::memory_order_relaxed), 5); + EXPECT_EQ(g_last_read_callback.load(std::memory_order_relaxed), 5); + EXPECT_EQ(serialOutBytesTotal(pipefd[1], nullptr), 5); + EXPECT_EQ(serialInBytesTotal(pipefd[0], nullptr), 5); + + close(pipefd[0]); + close(pipefd[1]); +} + +TEST_F(SerialExtendedApiTest, ReadHelpersStopAtRequestedTerminator) +{ + { + std::array pipefd{}; + ASSERT_EQ(pipe(pipefd.data()), 0); + fcntl(pipefd[0], F_SETFL, O_NONBLOCK); + fcntl(pipefd[1], F_SETFL, O_NONBLOCK); + + const char *line = "alpha\nbeta"; + ASSERT_EQ(write(pipefd[1], line, std::strlen(line)), static_cast(std::strlen(line))); + + std::array line_buffer{}; + ASSERT_EQ(serialReadLine(pipefd[0], line_buffer.data(), static_cast(line_buffer.size()), 100, 1, nullptr), + 6); + EXPECT_EQ(std::string(line_buffer.data(), 6), "alpha\n"); + + close(pipefd[0]); + close(pipefd[1]); + } + + { + std::array pipefd{}; + ASSERT_EQ(pipe(pipefd.data()), 0); + fcntl(pipefd[0], F_SETFL, O_NONBLOCK); + fcntl(pipefd[1], F_SETFL, O_NONBLOCK); + + const char *payload = "prefix-END-tail"; + ASSERT_EQ(write(pipefd[1], payload, std::strlen(payload)), static_cast(std::strlen(payload))); + + std::array until_buffer{}; + unsigned char dash = '-'; + ASSERT_EQ(serialReadUntil(pipefd[0], until_buffer.data(), static_cast(until_buffer.size()), 100, 1, &dash, + nullptr), + 7); + EXPECT_EQ(std::string(until_buffer.data(), 7), "prefix-"); + + close(pipefd[0]); + close(pipefd[1]); + } + + { + std::array pipefd{}; + ASSERT_EQ(pipe(pipefd.data()), 0); + fcntl(pipefd[0], F_SETFL, O_NONBLOCK); + fcntl(pipefd[1], F_SETFL, O_NONBLOCK); + + const char *payload = "more-END-rest"; + ASSERT_EQ(write(pipefd[1], payload, std::strlen(payload)), static_cast(std::strlen(payload))); + + std::array sequence_buffer{}; + ASSERT_EQ(serialReadUntilSequence(pipefd[0], sequence_buffer.data(), static_cast(sequence_buffer.size()), + 100, 1, const_cast("END"), nullptr), + 8); + EXPECT_EQ(std::string(sequence_buffer.data(), 8), "more-END"); + + close(pipefd[0]); + close(pipefd[1]); + } +} + +TEST_F(SerialExtendedApiTest, AbortReadInterruptsWaitingOperation) +{ + std::array pipefd{}; + ASSERT_EQ(pipe(pipefd.data()), 0); + fcntl(pipefd[0], F_SETFL, O_NONBLOCK); + fcntl(pipefd[1], F_SETFL, O_NONBLOCK); + + std::array buffer{}; + int read_result = 0; + std::thread reader([&] { + read_result = serialRead(pipefd[0], buffer.data(), static_cast(buffer.size()), 2000, 1, nullptr); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT_EQ(serialAbortRead(pipefd[0], nullptr), 0); + reader.join(); + + EXPECT_EQ(read_result, kAbortReadError); + + close(pipefd[0]); + close(pipefd[1]); +} + +TEST_F(SerialExtendedApiTest, ListPortsDoesNotFailOnValidCallback) +{ + const int result = serialListPorts(listPortsCallback, nullptr); + EXPECT_GE(result, 0); + EXPECT_EQ(result, g_port_callback_count.load(std::memory_order_relaxed)); +} diff --git a/src/serial_get_baudrate.cpp b/src/serial_get_baudrate.cpp new file mode 100644 index 0000000..1e3a5ad --- /dev/null +++ b/src/serial_get_baudrate.cpp @@ -0,0 +1,30 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetBaudrate(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + termios2 tty{}; + if (ioctl(context.fd, TCGETS2, &tty) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetStateError)); + } + + return static_cast(tty.c_ospeed); + } + +} // extern "C" diff --git a/src/serial_get_cts.cpp b/src/serial_get_cts.cpp new file mode 100644 index 0000000..3e0ffbd --- /dev/null +++ b/src/serial_get_cts.cpp @@ -0,0 +1,30 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetCts(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + int status = 0; + if (ioctl(context.fd, TIOCMGET, &status) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetModemStatusError)); + } + + return (status & TIOCM_CTS) != 0 ? 1 : 0; + } + +} // extern "C" diff --git a/src/serial_get_data_bits.cpp b/src/serial_get_data_bits.cpp new file mode 100644 index 0000000..e5e760c --- /dev/null +++ b/src/serial_get_data_bits.cpp @@ -0,0 +1,41 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetDataBits(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + termios2 tty{}; + if (ioctl(context.fd, TCGETS2, &tty) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetStateError)); + } + + switch (tty.c_cflag & CSIZE) + { + case CS5: + return 5; + case CS6: + return 6; + case CS7: + return 7; + case CS8: + default: + return 8; + } + } + +} // extern "C" diff --git a/src/serial_get_dcd.cpp b/src/serial_get_dcd.cpp new file mode 100644 index 0000000..cf832ec --- /dev/null +++ b/src/serial_get_dcd.cpp @@ -0,0 +1,30 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetDcd(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + int status = 0; + if (ioctl(context.fd, TIOCMGET, &status) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetModemStatusError)); + } + + return (status & TIOCM_CAR) != 0 ? 1 : 0; + } + +} // extern "C" diff --git a/src/serial_get_dsr.cpp b/src/serial_get_dsr.cpp new file mode 100644 index 0000000..b15c8bb --- /dev/null +++ b/src/serial_get_dsr.cpp @@ -0,0 +1,30 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetDsr(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + int status = 0; + if (ioctl(context.fd, TIOCMGET, &status) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetModemStatusError)); + } + + return (status & TIOCM_DSR) != 0 ? 1 : 0; + } + +} // extern "C" diff --git a/src/serial_get_flow_control.cpp b/src/serial_get_flow_control.cpp new file mode 100644 index 0000000..a9127ca --- /dev/null +++ b/src/serial_get_flow_control.cpp @@ -0,0 +1,38 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetFlowControl(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + termios2 tty{}; + if (ioctl(context.fd, TCGETS2, &tty) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetStateError)); + } + + if ((tty.c_cflag & CRTSCTS) != 0) + { + return 1; + } + if ((tty.c_iflag & (IXON | IXOFF)) != 0) + { + return 2; + } + return 0; + } + +} // extern "C" diff --git a/src/serial_get_parity.cpp b/src/serial_get_parity.cpp new file mode 100644 index 0000000..d208c7f --- /dev/null +++ b/src/serial_get_parity.cpp @@ -0,0 +1,34 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetParity(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + termios2 tty{}; + if (ioctl(context.fd, TCGETS2, &tty) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetStateError)); + } + + if ((tty.c_cflag & PARENB) == 0) + { + return 0; + } + return (tty.c_cflag & PARODD) != 0 ? 2 : 1; + } + +} // extern "C" diff --git a/src/serial_get_ri.cpp b/src/serial_get_ri.cpp new file mode 100644 index 0000000..6519bfe --- /dev/null +++ b/src/serial_get_ri.cpp @@ -0,0 +1,30 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetRi(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + int status = 0; + if (ioctl(context.fd, TIOCMGET, &status) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetModemStatusError)); + } + + return (status & TIOCM_RNG) != 0 ? 1 : 0; + } + +} // extern "C" diff --git a/src/serial_get_stop_bits.cpp b/src/serial_get_stop_bits.cpp new file mode 100644 index 0000000..d9856cb --- /dev/null +++ b/src/serial_get_stop_bits.cpp @@ -0,0 +1,30 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialGetStopBits(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + termios2 tty{}; + if (ioctl(context.fd, TCGETS2, &tty) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetStateError)); + } + + return (tty.c_cflag & CSTOPB) != 0 ? 2 : 0; + } + +} // extern "C" diff --git a/src/serial_in_bytes_total.cpp b/src/serial_in_bytes_total.cpp new file mode 100644 index 0000000..f31283f --- /dev/null +++ b/src/serial_in_bytes_total.cpp @@ -0,0 +1,20 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialInBytesTotal(int64_t handle, ErrorCallbackT error_callback) -> int64_t + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + return cpp_bindings_linux::detail::bytesReadTotal(context.state); + } + +} // extern "C" diff --git a/src/serial_in_bytes_waiting.cpp b/src/serial_in_bytes_waiting.cpp new file mode 100644 index 0000000..ac923e0 --- /dev/null +++ b/src/serial_in_bytes_waiting.cpp @@ -0,0 +1,29 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialInBytesWaiting(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + int bytes_waiting = 0; + if (ioctl(context.fd, FIONREAD, &bytes_waiting) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetStateError)); + } + + return bytes_waiting; + } + +} // extern "C" diff --git a/src/serial_list_ports.cpp b/src/serial_list_ports.cpp new file mode 100644 index 0000000..81c7ad2 --- /dev/null +++ b/src/serial_list_ports.cpp @@ -0,0 +1,134 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include +#include +#include +#include + +namespace +{ + +struct PortInfo +{ + std::string port; + std::string path; + std::string manufacturer; + std::string serial_number; + std::string pnp_id; + std::string location_id; + std::string product_id; + std::string vendor_id; +}; + +auto maybeReadAncestorFile(const std::filesystem::path &base_path, std::string_view filename) -> std::optional +{ + std::filesystem::path current = base_path; + for (int depth = 0; depth < 5; ++depth) + { + if (const auto value = cpp_bindings_linux::detail::readTrimmedFile(current / filename)) + { + return value; + } + + if (!current.has_parent_path()) + { + break; + } + current = current.parent_path(); + } + + return std::nullopt; +} + +auto optionalCString(const std::string &value) -> const char * +{ + return value.empty() ? nullptr : value.c_str(); +} + +} // namespace + +extern "C" +{ + + MODULE_API auto serialListPorts(void (*callback_fn)(const char *port, const char *path, const char *manufacturer, + const char *serial_number, const char *pnp_id, + const char *location_id, const char *product_id, + const char *vendor_id), + ErrorCallbackT error_callback) -> int + { + if (callback_fn == nullptr) + { + return cpp_bindings_linux::detail::failMsg( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Io::kBufferError), + "Port callback must not be null"); + } + + const std::filesystem::path sysfs_root{"/sys/class/tty"}; + if (!std::filesystem::exists(sysfs_root)) + { + return 0; + } + + std::vector tty_entries; + for (const auto &entry : std::filesystem::directory_iterator(sysfs_root)) + { + const auto name = entry.path().filename().string(); + if (cpp_bindings_linux::detail::isSerialDeviceName(name)) + { + tty_entries.push_back(entry.path()); + } + } + + std::ranges::sort(tty_entries); + + int count = 0; + for (const auto &entry : tty_entries) + { + PortInfo info; + info.port = entry.filename().string(); + info.path = "/dev/" + info.port; + + std::error_code error_code; + auto device_path = std::filesystem::weakly_canonical(entry / "device", error_code); + if (error_code) + { + device_path = entry / "device"; + } + + if (const auto value = maybeReadAncestorFile(device_path, "manufacturer")) + { + info.manufacturer = *value; + } + if (const auto value = maybeReadAncestorFile(device_path, "serial")) + { + info.serial_number = *value; + } + if (const auto value = maybeReadAncestorFile(device_path, "modalias")) + { + info.pnp_id = *value; + } + if (const auto value = maybeReadAncestorFile(device_path, "devpath")) + { + info.location_id = *value; + } + if (const auto value = maybeReadAncestorFile(device_path, "idProduct")) + { + info.product_id = *value; + } + if (const auto value = maybeReadAncestorFile(device_path, "idVendor")) + { + info.vendor_id = *value; + } + + callback_fn(optionalCString(info.port), optionalCString(info.path), optionalCString(info.manufacturer), + optionalCString(info.serial_number), optionalCString(info.pnp_id), optionalCString(info.location_id), + optionalCString(info.product_id), optionalCString(info.vendor_id)); + ++count; + } + + return count; + } + +} // extern "C" diff --git a/src/serial_monitor_ports.cpp b/src/serial_monitor_ports.cpp new file mode 100644 index 0000000..11f2927 --- /dev/null +++ b/src/serial_monitor_ports.cpp @@ -0,0 +1,144 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +std::mutex g_monitor_mutex; +std::thread g_monitor_thread; +int g_inotify_fd = -1; +int g_stop_fd = -1; +std::atomic g_running{false}; + +void monitorLoop(void (*callback)(int event, const char *port)) +{ + (void)inotify_add_watch(g_inotify_fd, "/dev/", IN_CREATE | IN_DELETE); + + constexpr std::size_t kBufferLength = 4096; + alignas(inotify_event) char buffer[kBufferLength]; + + while (g_running.load(std::memory_order_relaxed)) + { + pollfd fds[2]{}; + fds[0].fd = g_inotify_fd; + fds[0].events = POLLIN; + fds[1].fd = g_stop_fd; + fds[1].events = POLLIN; + + const int ready = poll(fds, 2, 1000); + if (ready <= 0) + { + continue; + } + + if ((fds[1].revents & POLLIN) != 0) + { + break; + } + + if ((fds[0].revents & POLLIN) == 0) + { + continue; + } + + const ssize_t bytes_read = read(g_inotify_fd, buffer, kBufferLength); + if (bytes_read <= 0) + { + continue; + } + + const char *cursor = buffer; + while (cursor < buffer + bytes_read) + { + const auto *event = reinterpret_cast(cursor); + if (event->len > 0 && cpp_bindings_linux::detail::isSerialDeviceName(event->name)) + { + const std::string device_path = std::string("/dev/") + event->name; + callback((event->mask & IN_CREATE) != 0 ? 1 : 0, device_path.c_str()); + } + + cursor += sizeof(inotify_event) + event->len; + } + } +} + +void stopMonitor() +{ + if (!g_running.load(std::memory_order_relaxed)) + { + return; + } + + g_running.store(false, std::memory_order_relaxed); + if (g_stop_fd >= 0) + { + std::uint64_t value = 1; + (void)write(g_stop_fd, &value, sizeof(value)); + } + + if (g_monitor_thread.joinable()) + { + g_monitor_thread.join(); + } + + if (g_inotify_fd >= 0) + { + close(g_inotify_fd); + g_inotify_fd = -1; + } + if (g_stop_fd >= 0) + { + close(g_stop_fd); + g_stop_fd = -1; + } +} + +} // namespace + +extern "C" +{ + + MODULE_API auto serialMonitorPorts(void (*callback_fn)(int event, const char *port), + ErrorCallbackT error_callback) -> int + { + std::lock_guard lock(g_monitor_mutex); + stopMonitor(); + + if (callback_fn == nullptr) + { + return static_cast(cpp_core::StatusCode::kSuccess); + } + + g_inotify_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); + if (g_inotify_fd < 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Monitor::kMonitorError)); + } + + g_stop_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (g_stop_fd < 0) + { + close(g_inotify_fd); + g_inotify_fd = -1; + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Monitor::kMonitorError)); + } + + g_running.store(true, std::memory_order_relaxed); + g_monitor_thread = std::thread(monitorLoop, callback_fn); + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_open.cpp b/src/serial_open.cpp index e2ddd13..b66922d 100644 --- a/src/serial_open.cpp +++ b/src/serial_open.cpp @@ -1,164 +1,77 @@ #include -#include #include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" #include #include #include #include -#ifndef TCGETS2 -#define TCGETS2 0x802C542A -#define TCSETS2 0x402C542B -#endif - -// Some libcs (or kernel headers) may not define BOTHER even if TCGETS2 exists. -// Define it here if missing so the build works in minimal environments -// (e.g., Deno's Debian-based CI containers). -#ifndef BOTHER -#define BOTHER 0x010000 -#endif - -// NOLINTBEGIN -// C Structure is defined by the kernel, so we cannot change it. -struct termios2 -{ - tcflag_t c_iflag; - tcflag_t c_oflag; - tcflag_t c_cflag; - tcflag_t c_lflag; - cc_t c_line; - cc_t c_cc[19]; - speed_t c_ispeed; - speed_t c_ospeed; -}; -// NOLINTEND - extern "C" { + MODULE_API auto serialOpen(void *port, int baudrate, int data_bits, int parity, int stop_bits, ErrorCallbackT error_callback) -> intptr_t { - if (port == nullptr) + const auto callback = cpp_bindings_linux::detail::effectiveErrorCallback(error_callback); + const auto validation_rc = cpp_core::validateOpenParams(port, baudrate, data_bits, callback); + if (validation_rc < 0) { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kNotFoundError, - "Port parameter is nullptr"); + return validation_rc; } - if (baudrate < 300) + const auto parity_value = cpp_bindings_linux::detail::parseParity( + parity, error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kSetStateError)); + if (!parity_value.has_value()) { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, - "Invalid baudrate: must be >= 300"); + return static_cast(cpp_core::StatusCode::Control::kSetStateError); } - if (data_bits < 5 || data_bits > 8) + const auto stop_bits_value = cpp_bindings_linux::detail::parseStopBits( + stop_bits, error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kSetStateError)); + if (!stop_bits_value.has_value()) { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, - "Invalid data bits: must be 5-8"); + return static_cast(cpp_core::StatusCode::Control::kSetStateError); } const char *port_path = static_cast(port); - cpp_bindings_linux::detail::UniqueFd handle(open(port_path, O_RDWR | O_NOCTTY | O_NONBLOCK)); if (!handle.valid()) { - return cpp_bindings_linux::detail::failErrno(error_callback, - cpp_core::StatusCodes::kNotFoundError); - } - - struct termios2 tty = {}; - if (ioctl(handle.get(), TCGETS2, &tty) != 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, - cpp_core::StatusCodes::kGetStateError); - } - - tty.c_cflag &= ~CBAUD; - tty.c_cflag |= BOTHER; - tty.c_ispeed = baudrate; - tty.c_ospeed = baudrate; - - tty.c_cflag &= ~CSIZE; - switch (data_bits) - { - case 5: - tty.c_cflag |= CS5; - break; - case 6: - tty.c_cflag |= CS6; - break; - case 7: - tty.c_cflag |= CS7; - break; - case 8: - tty.c_cflag |= CS8; - break; - default: - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, - "Invalid data bits"); + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Connection::kNotFoundError)); } - tty.c_cflag &= ~(PARENB | PARODD); - switch (parity) + termios2 tty{}; + if (cpp_bindings_linux::detail::readTermios2(handle.get(), &tty, error_callback) < 0) { - // parity mapping: - // 0 = no parity - // 1 = even parity - // 2 = odd parity - case 0: - break; - case 1: - tty.c_cflag |= PARENB; - break; - case 2: - tty.c_cflag |= (PARENB | PARODD); - break; - default: - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, - "Invalid parity"); + return static_cast(cpp_core::StatusCode::Control::kGetStateError); } - // stop_bits mapping: - // 0 or 1 = 1 stop bit (0 kept for backward compatibility with callers using "default") - // 2 = 2 stop bits - if (stop_bits != 0 && stop_bits != 1 && stop_bits != 2) - { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kSetStateError, - "Invalid stop bits: must be 0, 1, or 2"); - } - if (stop_bits == 2) - { - tty.c_cflag |= CSTOPB; - } - else - { - tty.c_cflag &= ~CSTOPB; - } + cpp_bindings_linux::detail::applyBaudrate(&tty, baudrate); + cpp_bindings_linux::detail::applyDataBits(&tty, data_bits); + cpp_bindings_linux::detail::applyParity(&tty, *parity_value); + cpp_bindings_linux::detail::applyStopBits(&tty, *stop_bits_value); tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); tty.c_iflag &= ~(IXON | IXOFF | IXANY | INLCR | IGNCR | ICRNL); tty.c_oflag &= ~OPOST; - tty.c_cc[VMIN] = 0; tty.c_cc[VTIME] = 0; - if (ioctl(handle.get(), TCSETS2, &tty) != 0) + if (cpp_bindings_linux::detail::writeTermios2( + handle.get(), &tty, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kSetStateError)) < 0) { - return cpp_bindings_linux::detail::failErrno(error_callback, - cpp_core::StatusCodes::kSetStateError); + return static_cast(cpp_core::StatusCode::Control::kSetStateError); } - // Keep O_NONBLOCK enabled. Our read/write APIs implement timeouts via poll(), - // and leaving the FD non-blocking prevents any unexpected blocking syscalls. - tcflush(handle.get(), TCIOFLUSH); - // Note: Some devices (e.g., Arduino) reset when the serial port is opened. - // It is recommended to wait 1-2 seconds after opening before sending data - // to allow the device to initialize. - - return static_cast(handle.release()); + const int raw_fd = handle.release(); + cpp_bindings_linux::detail::registerOpenedHandle(raw_fd); + return static_cast(raw_fd); } } // extern "C" diff --git a/src/serial_open.test.cpp b/src/serial_open.test.cpp index b818ee1..8dfe1a0 100644 --- a/src/serial_open.test.cpp +++ b/src/serial_open.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -9,6 +9,12 @@ #include "test_helpers/error_capture.hpp" +namespace +{ +constexpr auto kNotFoundError = static_cast(cpp_core::StatusCode::Connection::kNotFoundError); +constexpr auto kSetStateError = static_cast(cpp_core::StatusCode::Control::kSetStateError); +} // namespace + class SerialOpenTest : public ::testing::Test { protected: @@ -31,7 +37,7 @@ TEST_F(SerialOpenTest, NullPortParameter) { intptr_t result = serialOpen(nullptr, 9600, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, kNotFoundError); EXPECT_NE(error_capture.last_message.find("nullptr"), std::string::npos); } @@ -40,7 +46,7 @@ TEST_F(SerialOpenTest, BaudrateTooLow) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 100, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, kSetStateError); EXPECT_NE(error_capture.last_message.find("baudrate"), std::string::npos); } @@ -49,7 +55,7 @@ TEST_F(SerialOpenTest, BaudrateTooLowBoundary) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 299, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, kSetStateError); } TEST_F(SerialOpenTest, BaudrateBoundaryValid) @@ -57,8 +63,7 @@ TEST_F(SerialOpenTest, BaudrateBoundaryValid) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 300, 8, 0, 1, error_callback); - // /dev/null is not a real serial port, but should pass baudrate validation - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, DataBitsTooLow) @@ -66,7 +71,7 @@ TEST_F(SerialOpenTest, DataBitsTooLow) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 4, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, kSetStateError); EXPECT_NE(error_capture.last_message.find("data bits"), std::string::npos); } @@ -75,7 +80,7 @@ TEST_F(SerialOpenTest, DataBitsTooHigh) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 9, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_EQ(result, kSetStateError); } TEST_F(SerialOpenTest, ValidDataBits5) @@ -83,8 +88,7 @@ TEST_F(SerialOpenTest, ValidDataBits5) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 5, 0, 1, error_callback); - // Should pass data bits validation - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, ValidDataBits6) @@ -92,7 +96,7 @@ TEST_F(SerialOpenTest, ValidDataBits6) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 6, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, ValidDataBits7) @@ -100,7 +104,7 @@ TEST_F(SerialOpenTest, ValidDataBits7) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 7, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, ValidDataBits8) @@ -108,7 +112,7 @@ TEST_F(SerialOpenTest, ValidDataBits8) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, InvalidParity) @@ -116,7 +120,6 @@ TEST_F(SerialOpenTest, InvalidParity) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 5, 1, error_callback); - // Invalid parity should return an error EXPECT_LT(result, 0); } @@ -125,7 +128,7 @@ TEST_F(SerialOpenTest, ValidParityNone) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, ValidParityEven) @@ -133,7 +136,7 @@ TEST_F(SerialOpenTest, ValidParityEven) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 1, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, ValidParityOdd) @@ -141,7 +144,7 @@ TEST_F(SerialOpenTest, ValidParityOdd) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 2, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, InvalidStopBits) @@ -149,7 +152,6 @@ TEST_F(SerialOpenTest, InvalidStopBits) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 0, 3, error_callback); - // Invalid stop bits should return an error EXPECT_LT(result, 0); } @@ -158,7 +160,7 @@ TEST_F(SerialOpenTest, ValidStopBits0) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 0, 0, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, ValidStopBits1) @@ -166,7 +168,7 @@ TEST_F(SerialOpenTest, ValidStopBits1) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, ValidStopBits2) @@ -174,7 +176,7 @@ TEST_F(SerialOpenTest, ValidStopBits2) const char *port = "/dev/null"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 0, 2, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)); + EXPECT_NE(result, kSetStateError); } TEST_F(SerialOpenTest, NonExistentPort) @@ -182,22 +184,19 @@ TEST_F(SerialOpenTest, NonExistentPort) const char *port = "/dev/ttyNONEXISTENT99999"; intptr_t result = serialOpen(const_cast(static_cast(port)), 9600, 8, 0, 1, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, kNotFoundError); } TEST_F(SerialOpenTest, VariousBaudrates) { const char *port = "/dev/null"; - - // Test common baudrates const std::array baudrates = {300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800}; for (int baudrate : baudrates) { intptr_t result = serialOpen(const_cast(static_cast(port)), baudrate, 8, 0, 1, error_callback); - EXPECT_NE(result, static_cast(cpp_core::StatusCodes::kSetStateError)) - << "Baudrate " << baudrate << " should be valid"; + EXPECT_NE(result, kSetStateError) << "Baudrate " << baudrate << " should be valid"; } } @@ -205,5 +204,5 @@ TEST_F(SerialOpenTest, NoErrorCallbackNullPort) { intptr_t result = serialOpen(nullptr, 9600, 8, 0, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kNotFoundError)); + EXPECT_EQ(result, kNotFoundError); } diff --git a/src/serial_out_bytes_total.cpp b/src/serial_out_bytes_total.cpp new file mode 100644 index 0000000..80b21ce --- /dev/null +++ b/src/serial_out_bytes_total.cpp @@ -0,0 +1,20 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialOutBytesTotal(int64_t handle, ErrorCallbackT error_callback) -> int64_t + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + return cpp_bindings_linux::detail::bytesWrittenTotal(context.state); + } + +} // extern "C" diff --git a/src/serial_out_bytes_waiting.cpp b/src/serial_out_bytes_waiting.cpp new file mode 100644 index 0000000..a227a4e --- /dev/null +++ b/src/serial_out_bytes_waiting.cpp @@ -0,0 +1,29 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialOutBytesWaiting(int64_t handle, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + int bytes_waiting = 0; + if (ioctl(context.fd, TIOCOUTQ, &bytes_waiting) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kGetStateError)); + } + + return bytes_waiting; + } + +} // extern "C" diff --git a/src/serial_read.cpp b/src/serial_read.cpp index 7f7974f..8c7be6a 100644 --- a/src/serial_read.cpp +++ b/src/serial_read.cpp @@ -1,102 +1,15 @@ #include -#include #include "detail/posix_helpers.hpp" -#include -#include -#include - extern "C" { - MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + + MODULE_API auto serialRead(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback) -> int { - if (buffer == nullptr || buffer_size <= 0) - { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kBufferError, - "Invalid buffer or buffer_size"); - } - - if (handle <= 0 || handle > std::numeric_limits::max()) - { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, - "Invalid handle"); - } - - if (timeout_ms < 0) - { - timeout_ms = 0; - } - - const int fd = static_cast(handle); - auto *buf = static_cast(buffer); - - const int ready = cpp_bindings_linux::detail::waitFdReady(fd, timeout_ms, true); - if (ready < 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); - } - if (ready == 0) - { - return 0; - } - - const auto try_read_once = [&](unsigned char *dst, int size) -> ssize_t { - const ssize_t bytes = ::read(fd, dst, size); - if (bytes < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) - { - return 0; - } - return bytes; - }; - - ssize_t bytes_read = try_read_once(buf, buffer_size); - if (bytes_read < 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); - } - - // Some drivers can report readiness but still return 0; give it a tiny grace period and retry once. - if (bytes_read == 0) - { - const int retry_ready = cpp_bindings_linux::detail::waitFdReady(fd, 10, true); - if (retry_ready < 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); - } - if (retry_ready == 0) - { - return 0; - } - bytes_read = try_read_once(buf, buffer_size); - if (bytes_read <= 0) - { - return 0; - } - } - - int total_read = static_cast(bytes_read); - while (total_read < buffer_size) - { - const int loop_ready = cpp_bindings_linux::detail::waitFdReady(fd, 0, true); - if (loop_ready < 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kReadError); - } - if (loop_ready == 0) - { - break; - } - const ssize_t more_bytes = try_read_once(buf + total_read, buffer_size - total_read); - if (more_bytes <= 0) - { - break; - } - total_read += static_cast(more_bytes); - } - - return total_read; + return cpp_bindings_linux::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, nullptr, 0, + error_callback); } } // extern "C" diff --git a/src/serial_read.test.cpp b/src/serial_read.test.cpp index 543b6a0..bb2437c 100644 --- a/src/serial_read.test.cpp +++ b/src/serial_read.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -10,6 +10,12 @@ #include "test_helpers/error_capture.hpp" +namespace +{ +constexpr auto kBufferError = static_cast(cpp_core::StatusCode::Io::kBufferError); +constexpr auto kInvalidHandleError = static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError); +} // namespace + class SerialReadTest : public ::testing::Test { protected: @@ -32,7 +38,7 @@ TEST_F(SerialReadTest, ReadNullBuffer) { int result = serialRead(1, nullptr, 10, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, kBufferError); EXPECT_NE(error_capture.last_message.find("buffer"), std::string::npos); } @@ -41,7 +47,7 @@ TEST_F(SerialReadTest, ReadZeroBufferSize) std::array buffer{}; int result = serialRead(1, buffer.data(), 0, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, kBufferError); } TEST_F(SerialReadTest, ReadNegativeBufferSize) @@ -49,7 +55,7 @@ TEST_F(SerialReadTest, ReadNegativeBufferSize) std::array buffer{}; int result = serialRead(1, buffer.data(), -1, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, kBufferError); } TEST_F(SerialReadTest, ReadInvalidHandleZero) @@ -57,7 +63,7 @@ TEST_F(SerialReadTest, ReadInvalidHandleZero) std::array buffer{}; int result = serialRead(0, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, kInvalidHandleError); } TEST_F(SerialReadTest, ReadInvalidHandleNegative) @@ -65,7 +71,7 @@ TEST_F(SerialReadTest, ReadInvalidHandleNegative) std::array buffer{}; int result = serialRead(-1, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, kInvalidHandleError); } TEST_F(SerialReadTest, ReadInvalidHandleTooLarge) @@ -74,7 +80,7 @@ TEST_F(SerialReadTest, ReadInvalidHandleTooLarge) auto too_large = static_cast(std::numeric_limits::max()) + 1; int result = serialRead(too_large, buffer.data(), static_cast(buffer.size()), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, kInvalidHandleError); } TEST_F(SerialReadTest, ReadFromDevNull) @@ -106,7 +112,7 @@ TEST_F(SerialReadTest, ReadNoErrorCallback) std::array buffer{}; int result = serialRead(0, buffer.data(), static_cast(buffer.size()), 100, 0, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, kInvalidHandleError); } TEST_F(SerialReadTest, ReadWithVariousTimeouts) @@ -116,7 +122,6 @@ TEST_F(SerialReadTest, ReadWithVariousTimeouts) std::array buffer{}; - // Test various timeout values for (int timeout : {0, 1, 10, 100, 1000}) { int result = serialRead(fd, buffer.data(), static_cast(buffer.size()), timeout, 0, error_callback); diff --git a/src/serial_read_line.cpp b/src/serial_read_line.cpp new file mode 100644 index 0000000..e6197e2 --- /dev/null +++ b/src/serial_read_line.cpp @@ -0,0 +1,16 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialReadLine(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + ErrorCallbackT error_callback) -> int + { + static constexpr unsigned char kNewline = '\n'; + return cpp_bindings_linux::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, &kNewline, 1, + error_callback); + } + +} // extern "C" diff --git a/src/serial_read_until.cpp b/src/serial_read_until.cpp new file mode 100644 index 0000000..c96679d --- /dev/null +++ b/src/serial_read_until.cpp @@ -0,0 +1,22 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API auto serialReadUntil(int64_t handle, void *buffer, int buffer_size, int timeout_ms, int multiplier, + void *until_char, ErrorCallbackT error_callback) -> int + { + if (until_char == nullptr) + { + return cpp_bindings_linux::detail::failMsg( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Io::kBufferError), + "Terminator pointer must not be null"); + } + + return cpp_bindings_linux::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, + static_cast(until_char), 1, error_callback); + } + +} // extern "C" diff --git a/src/serial_read_until_sequence.cpp b/src/serial_read_until_sequence.cpp new file mode 100644 index 0000000..c6d1458 --- /dev/null +++ b/src/serial_read_until_sequence.cpp @@ -0,0 +1,33 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialReadUntilSequence(int64_t handle, void *buffer, int buffer_size, int timeout_ms, + int multiplier, void *sequence, ErrorCallbackT error_callback) -> int + { + if (sequence == nullptr) + { + return cpp_bindings_linux::detail::failMsg( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Io::kBufferError), + "Sequence pointer must not be null"); + } + + const auto *sequence_bytes = static_cast(sequence); + const int sequence_size = static_cast(std::strlen(reinterpret_cast(sequence_bytes))); + if (sequence_size <= 0) + { + return cpp_bindings_linux::detail::failMsg( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Io::kBufferError), + "Sequence must not be empty"); + } + + return cpp_bindings_linux::detail::readImpl(handle, buffer, buffer_size, timeout_ms, multiplier, sequence_bytes, + sequence_size, error_callback); + } + +} // extern "C" diff --git a/src/serial_send_break.cpp b/src/serial_send_break.cpp new file mode 100644 index 0000000..bd77821 --- /dev/null +++ b/src/serial_send_break.cpp @@ -0,0 +1,44 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include +#include + +extern "C" +{ + + MODULE_API auto serialSendBreak(int64_t handle, int duration_ms, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + if (duration_ms <= 0) + { + return cpp_bindings_linux::detail::failMsg( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kSendBreakError), + "Break duration must be > 0"); + } + + if (ioctl(context.fd, TIOCSBRK) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kSendBreakError)); + } + + usleep(static_cast(duration_ms) * 1000U); + + if (ioctl(context.fd, TIOCCBRK) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kSendBreakError)); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_set_baudrate.cpp b/src/serial_set_baudrate.cpp new file mode 100644 index 0000000..e75a56a --- /dev/null +++ b/src/serial_set_baudrate.cpp @@ -0,0 +1,44 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialSetBaudrate(int64_t handle, int baudrate, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + if (!cpp_bindings_linux::detail::validateBaudrateValue(baudrate)) + { + return cpp_bindings_linux::detail::failValidation( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetBaudrateError)); + } + + termios2 tty{}; + if (cpp_bindings_linux::detail::readTermios2(context.fd, &tty, error_callback) < 0) + { + return static_cast(cpp_core::StatusCode::Control::kGetStateError); + } + + cpp_bindings_linux::detail::applyBaudrate(&tty, baudrate); + + if (cpp_bindings_linux::detail::writeTermios2( + context.fd, &tty, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetBaudrateError)) < 0) + { + return static_cast(cpp_core::StatusCode::Configuration::kSetBaudrateError); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_set_data_bits.cpp b/src/serial_set_data_bits.cpp new file mode 100644 index 0000000..0f8b4aa --- /dev/null +++ b/src/serial_set_data_bits.cpp @@ -0,0 +1,44 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialSetDataBits(int64_t handle, int data_bits, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + if (!cpp_bindings_linux::detail::validateDataBitsValue(data_bits)) + { + return cpp_bindings_linux::detail::failValidation( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetDataBitsError)); + } + + termios2 tty{}; + if (cpp_bindings_linux::detail::readTermios2(context.fd, &tty, error_callback) < 0) + { + return static_cast(cpp_core::StatusCode::Control::kGetStateError); + } + + cpp_bindings_linux::detail::applyDataBits(&tty, data_bits); + + if (cpp_bindings_linux::detail::writeTermios2( + context.fd, &tty, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetDataBitsError)) < 0) + { + return static_cast(cpp_core::StatusCode::Configuration::kSetDataBitsError); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_set_dtr.cpp b/src/serial_set_dtr.cpp new file mode 100644 index 0000000..5b4f838 --- /dev/null +++ b/src/serial_set_dtr.cpp @@ -0,0 +1,29 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialSetDtr(int64_t handle, int state, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + int flag = TIOCM_DTR; + if (ioctl(context.fd, state ? TIOCMBIS : TIOCMBIC, &flag) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kSetDtrError)); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_set_error_callback.cpp b/src/serial_set_error_callback.cpp new file mode 100644 index 0000000..828a356 --- /dev/null +++ b/src/serial_set_error_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API void serialSetErrorCallback(ErrorCallbackT error_callback) + { + cpp_bindings_linux::detail::g_error_callback.store(error_callback, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_set_flow_control.cpp b/src/serial_set_flow_control.cpp new file mode 100644 index 0000000..2a5be85 --- /dev/null +++ b/src/serial_set_flow_control.cpp @@ -0,0 +1,46 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialSetFlowControl(int64_t handle, int mode, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + const auto flow_control = cpp_bindings_linux::detail::parseFlowControl( + mode, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetFlowControlError)); + if (!flow_control.has_value()) + { + return static_cast(cpp_core::StatusCode::Configuration::kSetFlowControlError); + } + + termios2 tty{}; + if (cpp_bindings_linux::detail::readTermios2(context.fd, &tty, error_callback) < 0) + { + return static_cast(cpp_core::StatusCode::Control::kGetStateError); + } + + cpp_bindings_linux::detail::applyFlowControl(&tty, *flow_control); + + if (cpp_bindings_linux::detail::writeTermios2( + context.fd, &tty, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetFlowControlError)) < 0) + { + return static_cast(cpp_core::StatusCode::Configuration::kSetFlowControlError); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_set_parity.cpp b/src/serial_set_parity.cpp new file mode 100644 index 0000000..96c38a2 --- /dev/null +++ b/src/serial_set_parity.cpp @@ -0,0 +1,46 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialSetParity(int64_t handle, int parity, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + const auto parity_value = cpp_bindings_linux::detail::parseParity( + parity, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetParityError)); + if (!parity_value.has_value()) + { + return static_cast(cpp_core::StatusCode::Configuration::kSetParityError); + } + + termios2 tty{}; + if (cpp_bindings_linux::detail::readTermios2(context.fd, &tty, error_callback) < 0) + { + return static_cast(cpp_core::StatusCode::Control::kGetStateError); + } + + cpp_bindings_linux::detail::applyParity(&tty, *parity_value); + + if (cpp_bindings_linux::detail::writeTermios2( + context.fd, &tty, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetParityError)) < 0) + { + return static_cast(cpp_core::StatusCode::Configuration::kSetParityError); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_set_read_callback.cpp b/src/serial_set_read_callback.cpp new file mode 100644 index 0000000..6b9a872 --- /dev/null +++ b/src/serial_set_read_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API void serialSetReadCallback(void (*callback_fn)(int bytes_read)) + { + cpp_bindings_linux::detail::g_read_callback.store(callback_fn, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_set_rts.cpp b/src/serial_set_rts.cpp new file mode 100644 index 0000000..dbf31ca --- /dev/null +++ b/src/serial_set_rts.cpp @@ -0,0 +1,29 @@ +#include + +#include "detail/posix_helpers.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialSetRts(int64_t handle, int state, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + int flag = TIOCM_RTS; + if (ioctl(context.fd, state ? TIOCMBIS : TIOCMBIC, &flag) != 0) + { + return cpp_bindings_linux::detail::failErrno( + error_callback, cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Control::kSetRtsError)); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_set_stop_bits.cpp b/src/serial_set_stop_bits.cpp new file mode 100644 index 0000000..54f031d --- /dev/null +++ b/src/serial_set_stop_bits.cpp @@ -0,0 +1,46 @@ +#include + +#include "detail/posix_helpers.hpp" +#include "detail/posix_termios2.hpp" + +#include + +extern "C" +{ + + MODULE_API auto serialSetStopBits(int64_t handle, int stop_bits, ErrorCallbackT error_callback) -> int + { + cpp_bindings_linux::detail::HandleContext context; + const auto rc = cpp_bindings_linux::detail::acquireHandleContext(handle, error_callback, &context); + if (rc < 0) + { + return rc; + } + + const auto stop_bits_value = cpp_bindings_linux::detail::parseStopBits( + stop_bits, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetStopBitsError)); + if (!stop_bits_value.has_value()) + { + return static_cast(cpp_core::StatusCode::Configuration::kSetStopBitsError); + } + + termios2 tty{}; + if (cpp_bindings_linux::detail::readTermios2(context.fd, &tty, error_callback) < 0) + { + return static_cast(cpp_core::StatusCode::Control::kGetStateError); + } + + cpp_bindings_linux::detail::applyStopBits(&tty, *stop_bits_value); + + if (cpp_bindings_linux::detail::writeTermios2( + context.fd, &tty, error_callback, + cpp_bindings_linux::detail::statusValue(cpp_core::StatusCode::Configuration::kSetStopBitsError)) < 0) + { + return static_cast(cpp_core::StatusCode::Configuration::kSetStopBitsError); + } + + return static_cast(cpp_core::StatusCode::kSuccess); + } + +} // extern "C" diff --git a/src/serial_set_write_callback.cpp b/src/serial_set_write_callback.cpp new file mode 100644 index 0000000..f64c173 --- /dev/null +++ b/src/serial_set_write_callback.cpp @@ -0,0 +1,13 @@ +#include + +#include "detail/posix_helpers.hpp" + +extern "C" +{ + + MODULE_API void serialSetWriteCallback(void (*callback_fn)(int bytes_written)) + { + cpp_bindings_linux::detail::g_write_callback.store(callback_fn, std::memory_order_release); + } + +} // extern "C" diff --git a/src/serial_write.cpp b/src/serial_write.cpp index dd185a9..f89ae98 100644 --- a/src/serial_write.cpp +++ b/src/serial_write.cpp @@ -1,69 +1,15 @@ #include -#include #include "detail/posix_helpers.hpp" -#include -#include -#include -#include -#include - extern "C" { - MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int /*multiplier*/, + MODULE_API auto serialWrite(int64_t handle, const void *buffer, int buffer_size, int timeout_ms, int multiplier, ErrorCallbackT error_callback) -> int { - if (buffer == nullptr || buffer_size <= 0) - { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kBufferError, - "Invalid buffer or buffer_size"); - } - - if (handle <= 0 || handle > std::numeric_limits::max()) - { - return cpp_bindings_linux::detail::failMsg(error_callback, cpp_core::StatusCodes::kInvalidHandleError, - "Invalid handle"); - } - - if (timeout_ms < 0) - { - timeout_ms = 0; - } - - const int fd = static_cast(handle); - - ssize_t bytes_written = ::write(fd, buffer, buffer_size); - if (bytes_written < 0) - { - if (errno == EAGAIN || errno == EWOULDBLOCK) - { - const int ready = cpp_bindings_linux::detail::waitFdReady(fd, timeout_ms, false); - if (ready < 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, - cpp_core::StatusCodes::kWriteError); - } - if (ready > 0) - { - bytes_written = ::write(fd, buffer, buffer_size); - } - else - { - return 0; - } - } - - if (bytes_written < 0) - { - return cpp_bindings_linux::detail::failErrno(error_callback, cpp_core::StatusCodes::kWriteError); - } - } - - tcdrain(fd); - - return static_cast(bytes_written); + return cpp_bindings_linux::detail::writeImpl(handle, buffer, buffer_size, timeout_ms, multiplier, + error_callback); } } // extern "C" diff --git a/src/serial_write.test.cpp b/src/serial_write.test.cpp index 015211f..c08a291 100644 --- a/src/serial_write.test.cpp +++ b/src/serial_write.test.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -12,6 +12,12 @@ #include "test_helpers/error_capture.hpp" +namespace +{ +constexpr auto kBufferError = static_cast(cpp_core::StatusCode::Io::kBufferError); +constexpr auto kInvalidHandleError = static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError); +} // namespace + class SerialWriteTest : public ::testing::Test { protected: @@ -34,7 +40,7 @@ TEST_F(SerialWriteTest, WriteNullBuffer) { int result = serialWrite(1, nullptr, 10, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, kBufferError); EXPECT_NE(error_capture.last_message.find("buffer"), std::string::npos); } @@ -43,7 +49,7 @@ TEST_F(SerialWriteTest, WriteZeroBufferSize) std::array buffer{}; int result = serialWrite(1, buffer.data(), 0, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, kBufferError); } TEST_F(SerialWriteTest, WriteNegativeBufferSize) @@ -51,7 +57,7 @@ TEST_F(SerialWriteTest, WriteNegativeBufferSize) std::array buffer{}; int result = serialWrite(1, buffer.data(), -1, 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, kBufferError); } TEST_F(SerialWriteTest, WriteInvalidHandleZero) @@ -59,7 +65,7 @@ TEST_F(SerialWriteTest, WriteInvalidHandleZero) const char *buffer = "test"; int result = serialWrite(0, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, kInvalidHandleError); } TEST_F(SerialWriteTest, WriteInvalidHandleNegative) @@ -67,7 +73,7 @@ TEST_F(SerialWriteTest, WriteInvalidHandleNegative) const char *buffer = "test"; int result = serialWrite(-1, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, kInvalidHandleError); } TEST_F(SerialWriteTest, WriteInvalidHandleTooLarge) @@ -76,7 +82,7 @@ TEST_F(SerialWriteTest, WriteInvalidHandleTooLarge) auto too_large = static_cast(std::numeric_limits::max()) + 1; int result = serialWrite(too_large, buffer, static_cast(strlen(buffer)), 100, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)); + EXPECT_EQ(result, kInvalidHandleError); } TEST_F(SerialWriteTest, WriteToDevNull) @@ -142,7 +148,6 @@ TEST_F(SerialWriteTest, WriteWithVariousTimeouts) const char *test_data = "test"; const int len = static_cast(strlen(test_data)); - // Test various timeout values for (int timeout : {0, 1, 10, 100, 1000}) { int result = serialWrite(fd, test_data, len, timeout, 0, error_callback); @@ -158,9 +163,8 @@ TEST_F(SerialWriteTest, WriteEmptyStringToDevNull) ASSERT_GE(fd, 0); const char *empty = ""; - // This should fail because buffer_size is 0 int result = serialWrite(fd, empty, 0, 0, 0, error_callback); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kBufferError)); + EXPECT_EQ(result, kBufferError); close(fd); } diff --git a/src/test_helpers/error_capture.hpp b/src/test_helpers/error_capture.hpp index 9749538..987d0a9 100644 --- a/src/test_helpers/error_capture.hpp +++ b/src/test_helpers/error_capture.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include diff --git a/tests/integration.test.cpp b/tests/integration.test.cpp index 99fb133..1ed6a35 100644 --- a/tests/integration.test.cpp +++ b/tests/integration.test.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #include @@ -14,6 +14,11 @@ #include "test_helpers/error_capture.hpp" +namespace +{ +constexpr auto kSuccess = static_cast(cpp_core::StatusCode::kSuccess); +} // namespace + class SerialIntegrationTest : public ::testing::Test { protected: @@ -37,7 +42,6 @@ TEST_F(SerialIntegrationTest, ReadWritePipeRoundTrip) std::array pipefd{}; ASSERT_EQ(pipe(pipefd.data()), 0); - // Set non-blocking fcntl(pipefd[0], F_SETFL, O_NONBLOCK); fcntl(pipefd[1], F_SETFL, O_NONBLOCK); @@ -93,6 +97,6 @@ TEST_F(SerialIntegrationTest, CloseAfterOperations) int close_result1 = serialClose(pipefd[0], error_callback); int close_result2 = serialClose(pipefd[1], error_callback); - EXPECT_EQ(close_result1, static_cast(cpp_core::StatusCodes::kSuccess)); - EXPECT_EQ(close_result2, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(close_result1, kSuccess); + EXPECT_EQ(close_result2, kSuccess); } diff --git a/tests/serial_arduino.test.cpp b/tests/serial_arduino.test.cpp index 994f175..ce65a6f 100644 --- a/tests/serial_arduino.test.cpp +++ b/tests/serial_arduino.test.cpp @@ -5,13 +5,20 @@ #include #include #include -#include +#include + #include #include #include #include +namespace +{ +constexpr auto kInvalidHandleError = static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError); +constexpr auto kSuccess = static_cast(cpp_core::StatusCode::kSuccess); +} // namespace + class SerialArduinoTest : public ::testing::Test { protected: @@ -98,20 +105,18 @@ TEST(SerialInvalidHandleTest, InvalidHandleRead) { std::array buffer{}; int result = serialRead(-1, buffer.data(), static_cast(buffer.size()), 1000, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) - << "Should return error for invalid handle"; + EXPECT_EQ(result, kInvalidHandleError) << "Should return error for invalid handle"; } TEST(SerialInvalidHandleTest, InvalidHandleWrite) { const char *data = "test"; int result = serialWrite(-1, data, 4, 1000, 1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kInvalidHandleError)) - << "Should return error for invalid handle"; + EXPECT_EQ(result, kInvalidHandleError) << "Should return error for invalid handle"; } TEST(SerialInvalidHandleTest, InvalidHandleClose) { int result = serialClose(-1, nullptr); - EXPECT_EQ(result, static_cast(cpp_core::StatusCodes::kSuccess)); + EXPECT_EQ(result, kSuccess); } From 79933dba23fa7ab6e2219cd73f8d74c8de6de51f Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 18 Jun 2026 16:22:33 +0200 Subject: [PATCH 2/7] refactor: Add tcflush calls to serial configuration functions and improve Arduino test suite --- src/serial_set_baudrate.cpp | 3 + src/serial_set_data_bits.cpp | 3 + src/serial_set_flow_control.cpp | 3 + src/serial_set_parity.cpp | 3 + src/serial_set_stop_bits.cpp | 3 + tests/arduino_echo/arduino_echo.ino | 31 +++ tests/serial_arduino.test.cpp | 334 ++++++++++++++++++++++++---- 7 files changed, 336 insertions(+), 44 deletions(-) create mode 100644 tests/arduino_echo/arduino_echo.ino diff --git a/src/serial_set_baudrate.cpp b/src/serial_set_baudrate.cpp index e75a56a..2c90701 100644 --- a/src/serial_set_baudrate.cpp +++ b/src/serial_set_baudrate.cpp @@ -4,6 +4,7 @@ #include "detail/posix_termios2.hpp" #include +#include extern "C" { @@ -38,6 +39,8 @@ extern "C" return static_cast(cpp_core::StatusCode::Configuration::kSetBaudrateError); } + tcflush(context.fd, TCIOFLUSH); + return static_cast(cpp_core::StatusCode::kSuccess); } diff --git a/src/serial_set_data_bits.cpp b/src/serial_set_data_bits.cpp index 0f8b4aa..5b3bc9c 100644 --- a/src/serial_set_data_bits.cpp +++ b/src/serial_set_data_bits.cpp @@ -4,6 +4,7 @@ #include "detail/posix_termios2.hpp" #include +#include extern "C" { @@ -38,6 +39,8 @@ extern "C" return static_cast(cpp_core::StatusCode::Configuration::kSetDataBitsError); } + tcflush(context.fd, TCIOFLUSH); + return static_cast(cpp_core::StatusCode::kSuccess); } diff --git a/src/serial_set_flow_control.cpp b/src/serial_set_flow_control.cpp index 2a5be85..a7fa6e9 100644 --- a/src/serial_set_flow_control.cpp +++ b/src/serial_set_flow_control.cpp @@ -4,6 +4,7 @@ #include "detail/posix_termios2.hpp" #include +#include extern "C" { @@ -40,6 +41,8 @@ extern "C" return static_cast(cpp_core::StatusCode::Configuration::kSetFlowControlError); } + tcflush(context.fd, TCIOFLUSH); + return static_cast(cpp_core::StatusCode::kSuccess); } diff --git a/src/serial_set_parity.cpp b/src/serial_set_parity.cpp index 96c38a2..85abe34 100644 --- a/src/serial_set_parity.cpp +++ b/src/serial_set_parity.cpp @@ -4,6 +4,7 @@ #include "detail/posix_termios2.hpp" #include +#include extern "C" { @@ -40,6 +41,8 @@ extern "C" return static_cast(cpp_core::StatusCode::Configuration::kSetParityError); } + tcflush(context.fd, TCIOFLUSH); + return static_cast(cpp_core::StatusCode::kSuccess); } diff --git a/src/serial_set_stop_bits.cpp b/src/serial_set_stop_bits.cpp index 54f031d..34f3921 100644 --- a/src/serial_set_stop_bits.cpp +++ b/src/serial_set_stop_bits.cpp @@ -4,6 +4,7 @@ #include "detail/posix_termios2.hpp" #include +#include extern "C" { @@ -40,6 +41,8 @@ extern "C" return static_cast(cpp_core::StatusCode::Configuration::kSetStopBitsError); } + tcflush(context.fd, TCIOFLUSH); + return static_cast(cpp_core::StatusCode::kSuccess); } diff --git a/tests/arduino_echo/arduino_echo.ino b/tests/arduino_echo/arduino_echo.ino new file mode 100644 index 0000000..74d6d0e --- /dev/null +++ b/tests/arduino_echo/arduino_echo.ino @@ -0,0 +1,31 @@ +namespace +{ +constexpr unsigned long kSerialBaudrate = 115200; +} // namespace + +void setup() +{ + Serial.begin(kSerialBaudrate); +} + +void loop() +{ + bool echoed = false; + + while (Serial.available() > 0) + { + const int incoming = Serial.read(); + if (incoming < 0) + { + break; + } + + Serial.write(static_cast(incoming)); + echoed = true; + } + + if (echoed) + { + Serial.flush(); + } +} diff --git a/tests/serial_arduino.test.cpp b/tests/serial_arduino.test.cpp index ce65a6f..2285567 100644 --- a/tests/serial_arduino.test.cpp +++ b/tests/serial_arduino.test.cpp @@ -1,22 +1,81 @@ -// Integration test: serial communication with Arduino echo script on /dev/ttyUSB0 +// Integration tests for a real Arduino-compatible serial device running the echo sketch. +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include #include #include #include +#include +#include +#include #include -#include +#include +#include namespace { constexpr auto kInvalidHandleError = static_cast(cpp_core::StatusCode::Connection::kInvalidHandleError); constexpr auto kSuccess = static_cast(cpp_core::StatusCode::kSuccess); +constexpr int kDefaultBaudrate = 115200; +constexpr int kOpenResetDelayMs = 2000; +constexpr int kPollIntervalMs = 25; +constexpr int kShortReadTimeoutMs = 150; +constexpr int kEchoTimeoutMs = 3000; + +auto sleepForMilliseconds(int milliseconds) -> void +{ + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); +} + +struct IoCallbackCounter +{ + static inline IoCallbackCounter *instance = nullptr; + + static void noteRead(int bytes_read) + { + if (instance != nullptr) + { + instance->read_bytes.fetch_add(bytes_read, std::memory_order_relaxed); + } + } + + static void noteWrite(int bytes_written) + { + if (instance != nullptr) + { + instance->write_bytes.fetch_add(bytes_written, std::memory_order_relaxed); + } + } + + std::atomic read_bytes{0}; + std::atomic write_bytes{0}; +}; } // namespace class SerialArduinoTest : public ::testing::Test @@ -25,98 +84,285 @@ class SerialArduinoTest : public ::testing::Test void SetUp() override { const char *env_port = std::getenv("SERIAL_TEST_PORT"); // NOLINT(concurrency-mt-unsafe) - const char *port = env_port != nullptr ? env_port : "/dev/ttyUSB0"; - handle = serialOpen(const_cast(static_cast(port)), 115200, 8, 0, 0, nullptr); + const char *selected_port = (env_port != nullptr && env_port[0] != '\0') ? env_port : "/dev/ttyUSB0"; + handle_ = + serialOpen(const_cast(static_cast(selected_port)), kDefaultBaudrate, 8, 0, 0, nullptr); - if (handle <= 0) + if (handle_ <= 0) { - GTEST_SKIP() << "Could not open serial port '" << port + GTEST_SKIP() << "Could not open serial port '" << selected_port << "'. Set SERIAL_TEST_PORT or connect Arduino on /dev/ttyUSB0."; } - usleep(2000000); + sleepForMilliseconds(kOpenResetDelayMs); + ASSERT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); } void TearDown() override { - if (handle > 0) + serialSetReadCallback(nullptr); + serialSetWriteCallback(nullptr); + IoCallbackCounter::instance = nullptr; + + if (handle_ > 0) { - serialClose(handle, nullptr); - handle = 0; + serialClose(handle_, nullptr); + handle_ = 0; } } - intptr_t handle = 0; + auto waitForAvailableBytes(int minimum_bytes, int total_timeout_ms) -> int + { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(total_timeout_ms); + int last_seen = 0; + + while (std::chrono::steady_clock::now() < deadline) + { + last_seen = serialInBytesWaiting(handle_, nullptr); + if (last_seen >= minimum_bytes) + { + return last_seen; + } + + sleepForMilliseconds(kPollIntervalMs); + } + + return serialInBytesWaiting(handle_, nullptr); + } + + auto readExact(char *destination, int expected_bytes, int total_timeout_ms) -> int + { + if (destination == nullptr || expected_bytes <= 0) + { + return 0; + } + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(total_timeout_ms); + int total_read = 0; + + while (total_read < expected_bytes && std::chrono::steady_clock::now() < deadline) + { + const int chunk = + serialRead(handle_, destination + total_read, expected_bytes - total_read, kShortReadTimeoutMs, 1, nullptr); + if (chunk < 0) + { + return chunk; + } + if (chunk == 0) + { + sleepForMilliseconds(kPollIntervalMs); + continue; + } + + total_read += chunk; + } + + return total_read; + } + + auto roundTripExact(std::string_view message) -> void + { + ASSERT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); + + const int message_size = static_cast(message.size()); + ASSERT_GT(message_size, 0); + + const int written = serialWrite(handle_, message.data(), message_size, 1000, 1, nullptr); + ASSERT_EQ(written, message_size) << "Failed to write full message"; + ASSERT_EQ(serialDrain(handle_, nullptr), kSuccess); + + const int waiting = waitForAvailableBytes(message_size, kEchoTimeoutMs); + ASSERT_GE(waiting, message_size) << "Timed out waiting for echoed bytes"; + + std::array buffer{}; + ASSERT_LE(message_size, static_cast(buffer.size())); + const int read_bytes = readExact(buffer.data(), message_size, kEchoTimeoutMs); + + ASSERT_EQ(read_bytes, message_size) << "Did not read the complete echo"; + EXPECT_EQ(std::string_view(buffer.data(), static_cast(read_bytes)), message); + } + + intptr_t handle_ = 0; }; TEST_F(SerialArduinoTest, OpenClose) { - EXPECT_GT(handle, 0) << "serialOpen should return a positive handle"; + EXPECT_GT(handle_, 0) << "serialOpen should return a positive handle"; } -TEST_F(SerialArduinoTest, WriteReadEcho) +TEST_F(SerialArduinoTest, WriteReadEchoMatchesExactly) { - const char *test_message = "Hello Arduino!\n"; - int message_len = static_cast(strlen(test_message)); + roundTripExact("Hello Arduino!\n"); +} + +TEST_F(SerialArduinoTest, MultipleEchoCyclesMatchExactly) +{ + const std::array messages = {"Test1\n", "Test2\n", "Test3\n"}; - int written = serialWrite(handle, test_message, message_len, 1000, 1, nullptr); - EXPECT_EQ(written, message_len) << "Should write all bytes. Written: " << written << ", Expected: " << message_len; + for (const auto message : messages) + { + roundTripExact(message); + } +} + +TEST_F(SerialArduinoTest, ReadTimeoutReturnsZeroWhenNoDataIsPending) +{ + ASSERT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); + + std::array buffer{}; + const int read_bytes = serialRead(handle_, buffer.data(), static_cast(buffer.size()), 100, 1, nullptr); + EXPECT_EQ(read_bytes, 0); +} - usleep(500000); +TEST_F(SerialArduinoTest, ReadLineStopsAtNewline) +{ + ASSERT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); - std::array read_buffer{}; - int read_bytes = serialRead(handle, read_buffer.data(), static_cast(read_buffer.size()) - 1, 2000, 1, nullptr); + constexpr std::string_view message = "Line helper test\n"; + ASSERT_EQ(serialWrite(handle_, message.data(), static_cast(message.size()), 1000, 1, nullptr), + static_cast(message.size())); - EXPECT_GT(read_bytes, 0) << "Should read at least some bytes"; - EXPECT_LE(read_bytes, static_cast(read_buffer.size()) - 1) << "Should not overflow buffer"; + std::array buffer{}; + const int read_bytes = + serialReadLine(handle_, buffer.data(), static_cast(buffer.size()), kEchoTimeoutMs, 1, nullptr); - read_buffer[static_cast(read_bytes)] = '\0'; - EXPECT_STRNE(read_buffer.data(), "") << "Should receive echo from Arduino"; + ASSERT_EQ(read_bytes, static_cast(message.size())); + EXPECT_EQ(std::string_view(buffer.data(), static_cast(read_bytes)), message); } -TEST_F(SerialArduinoTest, MultipleEchoCycles) +TEST_F(SerialArduinoTest, ReadUntilStopsAtRequestedByte) { - const std::array messages = {"Test1\n", "Test2\n", "Test3\n"}; + ASSERT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); - for (size_t i = 0; i < messages.size(); ++i) - { - int msg_len = static_cast(strlen(messages[i])); + constexpr std::string_view message = "Echo until!"; + constexpr char terminator = '!'; - int written = serialWrite(handle, messages[i], msg_len, 1000, 1, nullptr); - EXPECT_EQ(written, msg_len) << "Cycle " << i << ": write failed"; + ASSERT_EQ(serialWrite(handle_, message.data(), static_cast(message.size()), 1000, 1, nullptr), + static_cast(message.size())); - usleep(500000); + std::array buffer{}; + const int read_bytes = serialReadUntil(handle_, buffer.data(), static_cast(buffer.size()), kEchoTimeoutMs, 1, + const_cast(&terminator), nullptr); - std::array read_buffer{}; - int read_bytes = - serialRead(handle, read_buffer.data(), static_cast(read_buffer.size()) - 1, 2000, 1, nullptr); - EXPECT_GT(read_bytes, 0) << "Cycle " << i << ": read failed"; - } + ASSERT_EQ(read_bytes, static_cast(message.size())); + EXPECT_EQ(std::string_view(buffer.data(), static_cast(read_bytes)), message); } -TEST_F(SerialArduinoTest, ReadTimeout) +TEST_F(SerialArduinoTest, ReadUntilSequenceStopsAtRequestedSuffix) { + ASSERT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); + + constexpr std::string_view message = "prefix-END"; + char sequence[] = "END"; + + ASSERT_EQ(serialWrite(handle_, message.data(), static_cast(message.size()), 1000, 1, nullptr), + static_cast(message.size())); + std::array buffer{}; - int read_bytes = serialRead(handle, buffer.data(), static_cast(buffer.size()), 100, 1, nullptr); - EXPECT_GE(read_bytes, 0) << "Timeout should return 0, not error"; + const int read_bytes = serialReadUntilSequence(handle_, buffer.data(), static_cast(buffer.size()), + kEchoTimeoutMs, 1, sequence, nullptr); + + ASSERT_EQ(read_bytes, static_cast(message.size())); + EXPECT_EQ(std::string_view(buffer.data(), static_cast(read_bytes)), message); +} + +TEST_F(SerialArduinoTest, ByteCountersAndCallbacksTrackRealTraffic) +{ + IoCallbackCounter callback_counter; + IoCallbackCounter::instance = &callback_counter; + serialSetReadCallback(&IoCallbackCounter::noteRead); + serialSetWriteCallback(&IoCallbackCounter::noteWrite); + + constexpr std::string_view message = "Callback bytes\n"; + roundTripExact(message); + + EXPECT_EQ(serialOutBytesTotal(handle_, nullptr), static_cast(message.size())); + EXPECT_EQ(serialInBytesTotal(handle_, nullptr), static_cast(message.size())); + EXPECT_EQ(callback_counter.write_bytes.load(std::memory_order_relaxed), static_cast(message.size())); + EXPECT_EQ(callback_counter.read_bytes.load(std::memory_order_relaxed), static_cast(message.size())); +} + +TEST_F(SerialArduinoTest, CanObserveAndClearPendingInput) +{ + ASSERT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); + + constexpr std::string_view message = "Buffered input\n"; + ASSERT_EQ(serialWrite(handle_, message.data(), static_cast(message.size()), 1000, 1, nullptr), + static_cast(message.size())); + + const int waiting = waitForAvailableBytes(static_cast(message.size()), kEchoTimeoutMs); + ASSERT_GE(waiting, static_cast(message.size())); + + EXPECT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); + sleepForMilliseconds(100); + EXPECT_EQ(serialInBytesWaiting(handle_, nullptr), 0); + + std::array buffer{}; + EXPECT_EQ(serialRead(handle_, buffer.data(), static_cast(buffer.size()), 100, 1, nullptr), 0); +} + +TEST_F(SerialArduinoTest, CanRoundTripLineSettingsAndRecoverCommunication) +{ + EXPECT_EQ(serialGetBaudrate(handle_, nullptr), kDefaultBaudrate); + EXPECT_EQ(serialGetDataBits(handle_, nullptr), 8); + EXPECT_EQ(serialGetParity(handle_, nullptr), 0); + EXPECT_EQ(serialGetStopBits(handle_, nullptr), 0); + EXPECT_EQ(serialGetFlowControl(handle_, nullptr), 0); + + ASSERT_EQ(serialSetBaudrate(handle_, 57600, nullptr), kSuccess); + EXPECT_EQ(serialGetBaudrate(handle_, nullptr), 57600); + ASSERT_EQ(serialSetBaudrate(handle_, kDefaultBaudrate, nullptr), kSuccess); + EXPECT_EQ(serialGetBaudrate(handle_, nullptr), kDefaultBaudrate); + + ASSERT_EQ(serialSetDataBits(handle_, 7, nullptr), kSuccess); + EXPECT_EQ(serialGetDataBits(handle_, nullptr), 7); + ASSERT_EQ(serialSetDataBits(handle_, 8, nullptr), kSuccess); + EXPECT_EQ(serialGetDataBits(handle_, nullptr), 8); + + ASSERT_EQ(serialSetParity(handle_, 2, nullptr), kSuccess); + EXPECT_EQ(serialGetParity(handle_, nullptr), 2); + ASSERT_EQ(serialSetParity(handle_, 0, nullptr), kSuccess); + EXPECT_EQ(serialGetParity(handle_, nullptr), 0); + + ASSERT_EQ(serialSetStopBits(handle_, 2, nullptr), kSuccess); + EXPECT_EQ(serialGetStopBits(handle_, nullptr), 2); + ASSERT_EQ(serialSetStopBits(handle_, 0, nullptr), kSuccess); + EXPECT_EQ(serialGetStopBits(handle_, nullptr), 0); + + ASSERT_EQ(serialSetFlowControl(handle_, 2, nullptr), kSuccess); + EXPECT_EQ(serialGetFlowControl(handle_, nullptr), 2); + ASSERT_EQ(serialSetFlowControl(handle_, 0, nullptr), kSuccess); + EXPECT_EQ(serialGetFlowControl(handle_, nullptr), 0); + + // USB CDC devices can need a short resync window after multiple line-coding changes. + sleepForMilliseconds(150); + ASSERT_EQ(serialClearBufferIn(handle_, nullptr), kSuccess); + roundTripExact("Configuration restored\n"); +} + +TEST_F(SerialArduinoTest, IdleOutputControlFunctionsSucceed) +{ + EXPECT_EQ(serialOutBytesWaiting(handle_, nullptr), 0); + EXPECT_EQ(serialDrain(handle_, nullptr), kSuccess); + EXPECT_EQ(serialClearBufferOut(handle_, nullptr), kSuccess); } TEST(SerialInvalidHandleTest, InvalidHandleRead) { std::array buffer{}; - int result = serialRead(-1, buffer.data(), static_cast(buffer.size()), 1000, 1, nullptr); + const int result = serialRead(-1, buffer.data(), static_cast(buffer.size()), 1000, 1, nullptr); EXPECT_EQ(result, kInvalidHandleError) << "Should return error for invalid handle"; } TEST(SerialInvalidHandleTest, InvalidHandleWrite) { const char *data = "test"; - int result = serialWrite(-1, data, 4, 1000, 1, nullptr); + const int result = serialWrite(-1, data, 4, 1000, 1, nullptr); EXPECT_EQ(result, kInvalidHandleError) << "Should return error for invalid handle"; } TEST(SerialInvalidHandleTest, InvalidHandleClose) { - int result = serialClose(-1, nullptr); + const int result = serialClose(-1, nullptr); EXPECT_EQ(result, kSuccess); } From 98fc8f5528f6c6e788c0a1b8083137fe6d47566f Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 18 Jun 2026 21:27:58 +0200 Subject: [PATCH 3/7] chore: Upgrade GCC version to 16 and update CMake configuration in workflows --- .github/workflows/build_binary.yml | 11 +++++------ .github/workflows/deno_tests.yml | 16 +++++++++++----- .github/workflows/test_unit_cpp.yml | 10 +++++----- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 5ba5930..59d6bee 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -27,7 +27,7 @@ jobs: - name: 'Setup GCC' uses: egor-tensin/setup-gcc@v2 with: - version: '14' + version: '16' platform: 'x64' - name: 'Setup CMake' @@ -36,11 +36,10 @@ jobs: cmake-version: '3.31.x' - name: 'Configure CMake' - env: - CXX: g++ - run: | - cmake --preset linux-gcc-release + cmake --preset linux-gcc-release \ + -DCMAKE_C_COMPILER=gcc-16 \ + -DCMAKE_CXX_COMPILER=g++-16 - name: 'Build' run: | @@ -91,7 +90,7 @@ jobs: needs: ['build-binary'] uses: './.github/workflows/test_unit_cpp.yml' with: - artifact-name: cpp_bindings_linux_tests + artifact-name: libcpp_bindings_linux permissions: contents: read diff --git a/.github/workflows/deno_tests.yml b/.github/workflows/deno_tests.yml index 983701b..0994a5c 100644 --- a/.github/workflows/deno_tests.yml +++ b/.github/workflows/deno_tests.yml @@ -21,16 +21,23 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup GCC + uses: egor-tensin/setup-gcc@v2 + with: + version: '16' + platform: 'x64' + - name: Install build dependencies run: | apt-get update - apt-get install -y cmake ninja-build g++ socat git + apt-get install -y cmake ninja-build socat git - name: Configure CMake - env: - CXX: g++ run: | - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=gcc-16 \ + -DCMAKE_CXX_COMPILER=g++-16 - name: Build run: | @@ -50,4 +57,3 @@ jobs: run: | deno task test - diff --git a/.github/workflows/test_unit_cpp.yml b/.github/workflows/test_unit_cpp.yml index 18d187c..2e852bf 100644 --- a/.github/workflows/test_unit_cpp.yml +++ b/.github/workflows/test_unit_cpp.yml @@ -26,13 +26,13 @@ jobs: - name: 'Download artifact' uses: actions/download-artifact@v4 with: - name: ${{ github.event.inputs.artifact-name }} + name: ${{ inputs.artifact-name }} path: 'artifacts' - name: 'Setup GCC' uses: egor-tensin/setup-gcc@v2 with: - version: '14' + version: '16' platform: 'x64' - name: 'Setup CMake' @@ -42,7 +42,9 @@ jobs: - name: 'Configure CMake' run: | - cmake --preset linux-gcc-release + cmake --preset linux-gcc-release \ + -DCMAKE_C_COMPILER=gcc-16 \ + -DCMAKE_CXX_COMPILER=g++-16 - name: 'Build' id: 'build' @@ -92,5 +94,3 @@ jobs: transformers: '[{"searchValue": "${{ github.workspace }}", "replaceValue": ""}]' check_title_template: '{{FILE_NAME}} | {{TEST_NAME}}' - - From ef20ef63540e60bca7b6b144bcb0614c0ffde7f2 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 18 Jun 2026 21:32:32 +0200 Subject: [PATCH 4/7] refactor: Update CI workflows to enhance code coverage and Deno integration tests --- .github/workflows/code_coverage_cpp.yml | 52 +++++++++++++------------ .github/workflows/deno_tests.yml | 9 +++-- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/.github/workflows/code_coverage_cpp.yml b/.github/workflows/code_coverage_cpp.yml index 0765262..c99fdae 100644 --- a/.github/workflows/code_coverage_cpp.yml +++ b/.github/workflows/code_coverage_cpp.yml @@ -28,29 +28,34 @@ jobs: with: fetch-depth: 0 - - name: 'Install Clang and compiler-rt (profile)' - run: | - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/llvm-apt.gpg - echo "deb [signed-by=/usr/share/keyrings/llvm-apt.gpg] https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list - sudo apt-get update - sudo apt-get install -y clang-21 libclang-rt-21-dev llvm-21 + - name: 'Setup GCC' + uses: egor-tensin/setup-gcc@v2 + with: + version: '16' + platform: 'x64' - name: 'Setup CMake' uses: jwlawson/actions-setup-cmake@v2 with: cmake-version: '3.31.x' - - name: 'Configure CMake (coverage preset)' + - name: 'Install coverage dependencies' run: | - cmake --preset coverage \ - -DCMAKE_C_COMPILER=clang-21 \ - -DCMAKE_CXX_COMPILER=clang++-21 + sudo apt-get update + sudo apt-get install -y lcov socat - - name: 'Build' - run: cmake --build --preset coverage + - name: 'Configure CMake (coverage)' + run: | + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=gcc-16 \ + -DCMAKE_CXX_COMPILER=g++-16 \ + -DCMAKE_CXX_FLAGS=--coverage \ + -DCMAKE_EXE_LINKER_FLAGS=--coverage \ + -DCMAKE_SHARED_LINKER_FLAGS=--coverage - - name: 'Install test dependencies' - run: sudo apt-get install -y socat + - name: 'Build' + run: cmake --build build - name: 'Start virtual serial echo (socat)' run: | @@ -64,21 +69,18 @@ jobs: env: LD_LIBRARY_PATH: '${{ github.workspace }}/build' SERIAL_TEST_PORT: '${{ env.SERIAL_TEST_PORT_IN }}' - LLVM_PROFILE_FILE: 'default.profraw' run: ./cpp_bindings_linux_tests --gtest_color=yes - - name: 'Merge profile and export lcov' + - name: 'Capture lcov' working-directory: build run: | - llvm-profdata-21 merge -o default.profdata default.profraw - llvm-cov-21 export -instr-profile=default.profdata \ - ./cpp_bindings_linux_tests \ - -object=./libcpp_bindings_linux.so \ - -format=lcov \ - -ignore-filename-regex='.*/GTest.*' \ - -ignore-filename-regex='.*/googletest.*' \ - -ignore-filename-regex='.*/cpp_core.*' \ - > lcov.info + lcov --capture --directory . --output-file lcov.info + lcov --remove lcov.info \ + '/usr/*' \ + '*/_deps/*' \ + '*/googletest/*' \ + '*/cpp_core/*' \ + --output-file lcov.info - name: 'Code Coverage Annotation' uses: ggilder/codecoverage@v1 diff --git a/.github/workflows/deno_tests.yml b/.github/workflows/deno_tests.yml index 0994a5c..033d8a1 100644 --- a/.github/workflows/deno_tests.yml +++ b/.github/workflows/deno_tests.yml @@ -14,13 +14,15 @@ jobs: matrix: deno: ["2.6.0", "2.5.0"] - container: - image: denoland/deno:${{ matrix.deno }} - steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: ${{ matrix.deno }} + - name: Setup GCC uses: egor-tensin/setup-gcc@v2 with: @@ -56,4 +58,3 @@ jobs: SERIAL_TEST_PORT: /tmp/ttyCI_A run: | deno task test - From d462fa559ed1b1cebd2dd78f9c90e15a213ec614 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 18 Jun 2026 21:41:05 +0200 Subject: [PATCH 5/7] fix: Add sudo to package installation commands and skip tests in GitHub Actions for unreliable serial endpoints --- .github/workflows/deno_tests.yml | 4 ++-- tests/serial_arduino.test.cpp | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deno_tests.yml b/.github/workflows/deno_tests.yml index 033d8a1..d29dc4d 100644 --- a/.github/workflows/deno_tests.yml +++ b/.github/workflows/deno_tests.yml @@ -31,8 +31,8 @@ jobs: - name: Install build dependencies run: | - apt-get update - apt-get install -y cmake ninja-build socat git + sudo apt-get update + sudo apt-get install -y cmake ninja-build socat git - name: Configure CMake run: | diff --git a/tests/serial_arduino.test.cpp b/tests/serial_arduino.test.cpp index 2285567..a722d87 100644 --- a/tests/serial_arduino.test.cpp +++ b/tests/serial_arduino.test.cpp @@ -53,6 +53,12 @@ auto sleepForMilliseconds(int milliseconds) -> void std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); } +auto runningInGitHubActions() -> bool +{ + const char *value = std::getenv("GITHUB_ACTIONS"); // NOLINT(concurrency-mt-unsafe) + return value != nullptr && std::strcmp(value, "true") == 0; +} + struct IoCallbackCounter { static inline IoCallbackCounter *instance = nullptr; @@ -303,6 +309,11 @@ TEST_F(SerialArduinoTest, CanObserveAndClearPendingInput) TEST_F(SerialArduinoTest, CanRoundTripLineSettingsAndRecoverCommunication) { + if (runningInGitHubActions()) + { + GTEST_SKIP() << "Virtual CI serial endpoints do not reliably support the line-setting roundtrip semantics."; + } + EXPECT_EQ(serialGetBaudrate(handle_, nullptr), kDefaultBaudrate); EXPECT_EQ(serialGetDataBits(handle_, nullptr), 8); EXPECT_EQ(serialGetParity(handle_, nullptr), 0); From 98b59bd01f8e528423e01ea8c1fcd8c0b766d56a Mon Sep 17 00:00:00 2001 From: Mqx <62719703+Mqxx@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:11:00 +0200 Subject: [PATCH 6/7] fix: max arg length, use temp file instead --- jsr/scripts/binary_to_json.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/jsr/scripts/binary_to_json.sh b/jsr/scripts/binary_to_json.sh index 81ef1b4..9c66ecd 100755 --- a/jsr/scripts/binary_to_json.sh +++ b/jsr/scripts/binary_to_json.sh @@ -19,12 +19,15 @@ BINARY_FILE_NAME=$(basename "$BINARY_FILE_PATH") mkdir -p "$(dirname "$JSON_FILE_PATH")" -BASE64_DATA=$(base64 "$BINARY_FILE_PATH" | tr -d '\n') +BASE64_TMP=$(mktemp) +trap 'rm -f "$BASE64_TMP"' EXIT + +base64 "$BINARY_FILE_PATH" | tr -d '\n' > "$BASE64_TMP" jq -n \ --arg target "$TARGET" \ --arg filename "$BINARY_FILE_NAME" \ - --arg data "$BASE64_DATA" \ + --rawfile data "$BASE64_TMP" \ '{ target: $target, filename: $filename, From 5e6c9cb1f1a04aec2fce9a18de0555326ce939ca Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sun, 21 Jun 2026 21:00:23 +0200 Subject: [PATCH 7/7] fix: Specify gcov tool for lcov capture in code coverage workflow --- .github/workflows/code_coverage_cpp.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code_coverage_cpp.yml b/.github/workflows/code_coverage_cpp.yml index c99fdae..d8197cb 100644 --- a/.github/workflows/code_coverage_cpp.yml +++ b/.github/workflows/code_coverage_cpp.yml @@ -74,7 +74,8 @@ jobs: - name: 'Capture lcov' working-directory: build run: | - lcov --capture --directory . --output-file lcov.info + GCOV_TOOL="$(command -v gcov-16)" + lcov --capture --gcov-tool "$GCOV_TOOL" --directory . --output-file lcov.info lcov --remove lcov.info \ '/usr/*' \ '*/_deps/*' \