From 24a069123e4d4e0d60100422b5f168f01e0edaf0 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Fri, 4 Jul 2025 16:39:58 +0000 Subject: [PATCH 01/18] add more functionaltity --- CMakeLists.txt | 29 ++-- README.md | 78 ++++++++-- examples/serial_advanced.ts | 135 ++++++++++++++++ src/serial.cpp | 300 ++++++++++++++++++++++++++++++++++-- src/serial.h | 18 +++ src/status_codes.h | 31 ++-- tests/serial_unit_tests.cpp | 118 ++++++++++++++ 7 files changed, 656 insertions(+), 53 deletions(-) create mode 100644 examples/serial_advanced.ts diff --git a/CMakeLists.txt b/CMakeLists.txt index e1886b7..12feaa7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,28 +78,21 @@ set_target_properties(${PROJECT_N} PROPERTIES target_include_directories(${PROJECT_N} PUBLIC ${PROJECT_SOURCE_DIR}/src) -add_executable(serial_integration_tests tests/serial_test.cpp) -target_link_libraries(serial_integration_tests PRIVATE ${PROJECT_N} gtest_main) +# Combined test target aggregating all test sources +add_executable(tests + tests/serial_test.cpp + tests/serial_unit_tests.cpp) -target_include_directories(serial_integration_tests PRIVATE ${PROJECT_SOURCE_DIR}/src) +# Link against the library under test and GoogleTest (without gtest_main, we have our own main) +target_link_libraries(tests PRIVATE ${PROJECT_N} gtest) -add_custom_command(TARGET serial_integration_tests POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMENT "Copy shared library next to test binary") - -# Unit test target covering additional API aspects -add_executable(serial_unit_tests tests/serial_unit_tests.cpp) -target_link_libraries(serial_unit_tests PRIVATE ${PROJECT_N} gtest_main) -target_include_directories(serial_unit_tests PRIVATE ${PROJECT_SOURCE_DIR}/src) +target_include_directories(tests PRIVATE ${PROJECT_SOURCE_DIR}/src) -add_custom_command(TARGET serial_unit_tests POST_BUILD +add_custom_command(TARGET tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $ - COMMENT "Copy shared library next to unit test binary") + $ + COMMENT "Copy shared library next to aggregated test binary") enable_testing() -add_test(NAME SerialEchoTest COMMAND serial_integration_tests /dev/ttyUSB0) -add_test(NAME SerialUnitTests COMMAND serial_unit_tests) +add_test(NAME AllTests COMMAND tests /dev/ttyUSB0) diff --git a/README.md b/README.md index 3e4d0c0..1f43a6a 100644 --- a/README.md +++ b/README.md @@ -64,16 +64,72 @@ lib.close(); ## 3 C API reference -| Function | Description | -|----------|-------------| -| `intptr_t serialOpen(const char* dev, int baud, int bits, int parity, int stop)` | Open a device and return a handle. | -| `void serialClose(intptr_t handle)` | Close the port. | -| `int serialRead(...)` | Read bytes with timeout. | -| `int serialWrite(...)` | Write bytes with timeout. | -| `int serialGetPortsInfo(char* buffer, int len, const char* sep)` | List ports under `/dev/serial/by-id`. | -| `void serialOnError(void (*)(int))` | Register an error callback. | -| *(others in `serial.h`)* | - -Return values ≤ 0 indicate error codes defined in `status_codes.h`. +Below is a condensed overview of the most relevant functions. See `serial.h` for full +signatures and additional helpers. + +### Connection +* `serialOpen(...)` – open a serial device and return a handle +* `serialClose(handle)` – close the device + +### I/O +* `serialRead(...)` / `serialWrite(...)` – basic read/write with timeout +* `serialReadUntil(...)` – read until a specific char is encountered (inclusive) +* `serialReadLine(...)` – read until `\n` +* `serialWriteLine(...)` – write buffer and append `\n` +* `serialReadUntilToken(...)` – read until a string token is encountered +* `serialReadFrame(...)` – read a frame delimited by start & end bytes + +### Helpers +* `serialPeek(...)` – look at the next byte without consuming it +* `serialDrain(...)` – wait until all TX bytes are sent +* `serialClearBufferIn/Out(...)` – drop buffered bytes +* `serialAbortRead/Write(...)` – abort pending I/O operations + +### Statistics +* `serialGetRxBytes(handle)` / `serialGetTxBytes(handle)` – cumulative RX / TX byte counters + +### Enumeration & autodetect +* `serialGetPortsInfo(...)` – list available ports under `/dev/serial/by-id` + +### Callbacks +* `serialOnError(func)` – error callback +* `serialOnRead(func)` – read callback (bytes read) +* `serialOnWrite(func)` – write callback (bytes written) + +Return values ≤ 0 correspond to error codes defined in `status_codes.h`. + +--- + +## 4 Ready-made Deno examples + +Two runnable scripts live in `examples/` and require only Deno plus the compiled +shared library. + +- **serial_echo.ts** – Minimal echo test that lists available ports, opens the + first one and verifies that the micro-controller echoes the sent string. + Run it with: + ```bash + deno run --allow-ffi --allow-read examples/serial_echo.ts \ + --lib ./build/libCPP-Unix-Bindings.so \ + --port /dev/ttyUSB0 + ``` + +- **serial_advanced.ts** – Shows the high-level helpers (`serialWriteLine`, + `serialReadLine`, `serialPeek`, `statistics`, `serialDrain`). It sends three + lines and then prints the TX/RX counters. + ```bash + deno run --allow-ffi --allow-read examples/serial_advanced.ts \ + --lib ./build/libCPP-Unix-Bindings.so \ + --port /dev/ttyUSB0 + ``` + +Notes: +1. `--lib` defaults to `./build/libCPP-Unix-Bindings.so`; pass a custom path if +you installed the library elsewhere. +2. `--port` defaults to `/dev/ttyUSB0`; adjust if your board shows up under a +different device (e.g. `/dev/ttyACM0`). +3. On most Arduino boards opening the port toggles DTR and triggers a reset. + Both scripts therefore wait ~2 s after opening the device before sending the + first command. --- diff --git a/examples/serial_advanced.ts b/examples/serial_advanced.ts new file mode 100644 index 0000000..d03fabd --- /dev/null +++ b/examples/serial_advanced.ts @@ -0,0 +1,135 @@ +// @ts-nocheck +// deno run --allow-ffi --allow-read examples/serial_advanced.ts --lib ./build/libCPP-Unix-Bindings.so --port /dev/ttyUSB0 + +/* + Advanced Deno example showcasing the extended C-API helpers: + 1. serialReadLine / serialWriteLine for convenient newline-terminated I/O + 2. serialPeek to inspect next byte without consuming + 3. Tx/Rx statistics counters + 4. serialDrain to wait until all bytes are sent + + Build the shared library first (e.g. via `cmake --build build`). +*/ + +interface CliOptions { + lib: string; + port?: string; +} + +function parseArgs(): CliOptions { + const opts: CliOptions = { lib: "./build/libCPP-Unix-Bindings.so" }; + + for (let i = 0; i < Deno.args.length; ++i) { + const arg = Deno.args[i]; + if (arg === "--lib" && i + 1 < Deno.args.length) { + opts.lib = Deno.args[++i]; + } else if (arg === "--port" && i + 1 < Deno.args.length) { + opts.port = Deno.args[++i]; + } else { + console.warn(`Unknown argument '${arg}' ignored.`); + } + } + return opts; +} + +// ----------------------------------------------------------------------------- +// Helper utilities for C interop +// ----------------------------------------------------------------------------- +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function cString(str: string): Uint8Array { + const bytes = encoder.encode(str); + const buf = new Uint8Array(bytes.length + 1); + buf.set(bytes, 0); + buf[bytes.length] = 0; + return buf; +} + +function pointer(view: Uint8Array): Deno.UnsafePointer { + return Deno.UnsafePointer.of(view) as Deno.UnsafePointer; +} + +// ----------------------------------------------------------------------------- +// Load dynamic library & bind needed symbols +// ----------------------------------------------------------------------------- +const { lib, port: cliPort } = parseArgs(); + +const dylib = Deno.dlopen( + lib, + { + serialOpen: { parameters: ["pointer", "i32", "i32", "i32", "i32"], result: "pointer" }, + serialClose: { parameters: ["pointer"], result: "void" }, + serialWriteLine: { parameters: ["pointer", "pointer", "i32", "i32"], result: "i32" }, + serialReadLine: { parameters: ["pointer", "pointer", "i32", "i32"], result: "i32" }, + serialPeek: { parameters: ["pointer", "pointer", "i32"], result: "i32" }, + serialDrain: { parameters: ["pointer"], result: "i32" }, + serialGetTxBytes: { parameters: ["pointer"], result: "i64" }, + serialGetRxBytes: { parameters: ["pointer"], result: "i64" }, + } as const, +); + +// ----------------------------------------------------------------------------- +// Open port +// ----------------------------------------------------------------------------- +const portPath = cliPort ?? "/dev/ttyUSB0"; +console.log(`Using port: ${portPath}`); + +const portBuf = cString(portPath); +const handle = dylib.symbols.serialOpen(pointer(portBuf), 115200, 8, 0, 0); +if (handle === null) { + console.error("Failed to open port!"); + dylib.close(); + Deno.exit(1); +} + +// Wait 2 s for Arduino reset (DTR toggle) +await new Promise((r) => setTimeout(r, 2000)); + +// ----------------------------------------------------------------------------- +// 1. Send a few lines and read them back (echo sketch on MCU) +// ----------------------------------------------------------------------------- +const lines = [ + "The quick brown fox jumps over the lazy dog", + "Grüße aus Deno!", + "1234567890", +]; + +for (const ln of lines) { + const payloadBuf = encoder.encode(ln); + const written = dylib.symbols.serialWriteLine(handle, pointer(payloadBuf), payloadBuf.length, 100); + if (written !== payloadBuf.length + 1) { + console.error(`WriteLine failed for '${ln}'`); + continue; + } + + // Peek first byte (should be our first char) + const peekBuf = new Uint8Array(1); + if (dylib.symbols.serialPeek(handle, pointer(peekBuf), 500) === 1) { + console.log(`Peek: '${String.fromCharCode(peekBuf[0])}'`); + } + + const readBuf = new Uint8Array(256); + const n = dylib.symbols.serialReadLine(handle, pointer(readBuf), readBuf.length, 1000); + const lineRx = decoder.decode(readBuf.subarray(0, n)); + console.log(`RX (${n} bytes): '${lineRx}'`); +} + +// Ensure all bytes are transmitted +if (dylib.symbols.serialDrain(handle) === 1) { + console.log("Transmit buffer drained."); +} + +// ----------------------------------------------------------------------------- +// Print statistics +// ----------------------------------------------------------------------------- +const txBytes = Number(dylib.symbols.serialGetTxBytes(handle)); +const rxBytes = Number(dylib.symbols.serialGetRxBytes(handle)); +console.log(`\nStatistics -> TX: ${txBytes} bytes, RX: ${rxBytes} bytes`); + +// ----------------------------------------------------------------------------- +// Cleanup +// ----------------------------------------------------------------------------- +dylib.symbols.serialClose(handle); +dylib.close(); +console.log("Done."); diff --git a/src/serial.cpp b/src/serial.cpp index 82aec05..a158146 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -3,6 +3,7 @@ #include "status_codes.h" #include +#include #include #include #include @@ -31,6 +32,17 @@ struct SerialPortHandle { int fd; termios original; // keep original settings so we can restore on close + + // --- extensions --- + int64_t rx_total{0}; // bytes received so far + int64_t tx_total{0}; // bytes transmitted so far + + bool has_peek{false}; + char peek_char{0}; + + // Abort flags (set from other threads) + std::atomic abort_read{false}; + std::atomic abort_write{false}; }; // Map integer baudrate to POSIX speed_t. Only common rates are supported. @@ -245,22 +257,57 @@ int serialRead(int64_t handlePtr, void* buffer, int bufferSize, int timeout, int return 0; } + // Abort check + if (handle->abort_read.exchange(false)) + { + return 0; + } + + int total_copied = 0; + + // First deliver byte from internal peek buffer if present + if (handle->has_peek && bufferSize > 0) + { + static_cast(buffer)[0] = handle->peek_char; + handle->has_peek = false; + handle->rx_total += 1; + total_copied = 1; + buffer = static_cast(buffer) + 1; + bufferSize -= 1; + if (bufferSize == 0) + { + if (read_callback != nullptr) + { + read_callback(total_copied); + } + return total_copied; + } + } + if (waitFdReady(handle->fd, timeout, false) <= 0) { - return 0; // timeout or error (we ignore error for now) + return total_copied; // return what we may have already copied (could be 0) } ssize_t n = read(handle->fd, buffer, bufferSize); if (n < 0) { invokeError(std::to_underlying(StatusCodes::READ_ERROR)); - return 0; + return total_copied; } + + if (n > 0) + { + handle->rx_total += n; + } + + total_copied += static_cast(n); + if (read_callback != nullptr) { - read_callback(static_cast(n)); + read_callback(total_copied); } - return static_cast(n); + return total_copied; } int serialWrite(int64_t handlePtr, const void* buffer, int bufferSize, int timeout, int /*multiplier*/) @@ -272,6 +319,12 @@ int serialWrite(int64_t handlePtr, const void* buffer, int bufferSize, int timeo return 0; } + // Abort check + if (handle->abort_write.exchange(false)) + { + return 0; + } + if (waitFdReady(handle->fd, timeout, true) <= 0) { return 0; // timeout or error @@ -283,6 +336,12 @@ int serialWrite(int64_t handlePtr, const void* buffer, int bufferSize, int timeo invokeError(std::to_underlying(StatusCodes::WRITE_ERROR)); return 0; } + + if (n > 0) + { + handle->tx_total += n; + } + if (write_callback != nullptr) { write_callback(static_cast(n)); @@ -381,11 +440,53 @@ int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) return result.empty() ? 0 : 1; // number of ports not easily counted here } -// Currently stubbed helpers (no-ops) -void serialClearBufferIn(int64_t /*unused*/) {} -void serialClearBufferOut(int64_t /*unused*/) {} -void serialAbortRead(int64_t /*unused*/) {} -void serialAbortWrite(int64_t /*unused*/) {} +// ----------------------------------------------------------------------------- +// Buffer & abort helpers implementations +// ----------------------------------------------------------------------------- + +void serialClearBufferIn(int64_t handlePtr) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr) + { + return; + } + tcflush(handle->fd, TCIFLUSH); + // reset peek buffer + handle->has_peek = false; +} + +void serialClearBufferOut(int64_t handlePtr) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr) + { + return; + } + tcflush(handle->fd, TCOFLUSH); +} + +void serialAbortRead(int64_t handlePtr) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr) + { + return; + } + handle->abort_read = true; +} + +void serialAbortWrite(int64_t handlePtr) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr) + { + return; + } + handle->abort_write = true; +} + +// ----------------------------------------------------------------------------- // Callback registration void serialOnError(void (*func)(int code)) @@ -400,3 +501,184 @@ void serialOnWrite(void (*func)(int bytes)) { write_callback = func; } + +// ----------------------------------------------------------------------------- +// Extended helper APIs (read line, token, frame, statistics, etc.) +// ----------------------------------------------------------------------------- + +int serialReadLine(int64_t handlePtr, void* buffer, int bufferSize, int timeout) +{ + char newline = '\n'; + return serialReadUntil(handlePtr, buffer, bufferSize, timeout, 1, &newline); +} + +int serialWriteLine(int64_t handlePtr, const void* buffer, int bufferSize, int timeout) +{ + // First write the payload + int written = serialWrite(handlePtr, buffer, bufferSize, timeout, 1); + if (written != bufferSize) + { + return written; // error path, propagate + } + // Append newline (\n) + char nl = '\n'; + int w_nl = serialWrite(handlePtr, &nl, 1, timeout, 1); + if (w_nl != 1) + { + return written; // newline failed, but payload written + } + return written + 1; +} + +int serialReadUntilToken(int64_t handlePtr, void* buffer, int bufferSize, int timeout, void* tokenPtr) +{ + auto* token_cstr = static_cast(tokenPtr); + if (token_cstr == nullptr) + { + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + return 0; + } + std::string token{token_cstr}; + int token_len = static_cast(token.size()); + if (token_len == 0 || bufferSize < token_len) + { + return 0; + } + + auto* buf = static_cast(buffer); + int total = 0; + int matched = 0; // how many chars of token matched so far + + while (total < bufferSize) + { + int res = serialRead(handlePtr, buf + total, 1, timeout, 1); + if (res <= 0) + { + break; // timeout or error + } + + char c = buf[total]; + total += 1; + + if (c == token[matched]) + { + matched += 1; + if (matched == token_len) + { + break; // token fully matched + } + } + else + { + matched = (c == token[0]) ? 1 : 0; // restart match search + } + } + + if (read_callback != nullptr) + { + read_callback(total); + } + return total; +} + +int serialReadFrame(int64_t handlePtr, void* buffer, int bufferSize, int timeout, char startByte, char endByte) +{ + auto* buf = static_cast(buffer); + int total = 0; + bool in_frame = false; + + while (total < bufferSize) + { + char byte; + int res = serialRead(handlePtr, &byte, 1, timeout, 1); + if (res <= 0) + { + break; // timeout + } + + if (!in_frame) + { + if (byte == startByte) + { + in_frame = true; + buf[total++] = byte; + } + continue; // ignore bytes until start byte detected + } + else + { + buf[total++] = byte; + if (byte == endByte) + { + break; // frame finished + } + } + } + + return total; +} + +int64_t serialGetRxBytes(int64_t handlePtr) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr) + { + return 0; + } + return handle->rx_total; +} + +int64_t serialGetTxBytes(int64_t handlePtr) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr) + { + return 0; + } + return handle->tx_total; +} + +int serialPeek(int64_t handlePtr, void* outByte, int timeout) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr || outByte == nullptr) + { + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + return 0; + } + + if (handle->has_peek) + { + *static_cast(outByte) = handle->peek_char; + return 1; + } + + char c; + int res = serialRead(handlePtr, &c, 1, timeout, 1); + if (res <= 0) + { + return 0; // nothing available + } + + // Store into peek buffer and undo stats increment + handle->peek_char = c; + handle->has_peek = true; + if (handle->rx_total > 0) + { + handle->rx_total -= 1; // don't account peek + } + + *static_cast(outByte) = c; + return 1; +} + +int serialDrain(int64_t handlePtr) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr) + { + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + return 0; + } + return (tcdrain(handle->fd) == 0) ? 1 : 0; +} diff --git a/src/serial.h b/src/serial.h index 73a2c4e..fc6285d 100644 --- a/src/serial.h +++ b/src/serial.h @@ -41,6 +41,24 @@ extern "C" MODULE_API void serialOnRead(void (*func)(int bytes)); MODULE_API void serialOnWrite(void (*func)(int bytes)); + MODULE_API int serialReadLine(int64_t handle, void* buffer, int bufferSize, int timeout /*ms*/); + + MODULE_API int serialWriteLine(int64_t handle, const void* buffer, int bufferSize, int timeout /*ms*/); + + MODULE_API int serialReadUntilToken(int64_t handle, void* buffer, int bufferSize, int timeout /*ms*/, void* token); + + MODULE_API int serialReadFrame(int64_t handle, void* buffer, int bufferSize, int timeout /*ms*/, char startByte, char endByte); + + // Byte statistics + MODULE_API int64_t serialGetRxBytes(int64_t handle); + MODULE_API int64_t serialGetTxBytes(int64_t handle); + + // Peek next byte without consuming + MODULE_API int serialPeek(int64_t handle, void* outByte, int timeout /*ms*/); + + // Drain pending TX bytes (wait until sent) + MODULE_API int serialDrain(int64_t handle); + #ifdef __cplusplus } #endif diff --git a/src/status_codes.h b/src/status_codes.h index 3eabfd8..a48b8c8 100644 --- a/src/status_codes.h +++ b/src/status_codes.h @@ -1,18 +1,19 @@ #pragma once -enum class StatusCodes { - SUCCESS = 0, - CLOSE_HANDLE_ERROR = -1, - INVALID_HANDLE_ERROR = -2, - READ_ERROR = -3, - WRITE_ERROR = -4, - GET_STATE_ERROR = -5, - SET_STATE_ERROR = -6, - SET_TIMEOUT_ERROR = -7, - BUFFER_ERROR = -8, - NOT_FOUND_ERROR = -9, - CLEAR_BUFFER_IN_ERROR = -10, - CLEAR_BUFFER_OUT_ERROR = -11, - ABORT_READ_ERROR = -12, - ABORT_WRITE_ERROR = -13, +enum class StatusCodes +{ + SUCCESS = 0, + CLOSE_HANDLE_ERROR = -1, + INVALID_HANDLE_ERROR = -2, + READ_ERROR = -3, + WRITE_ERROR = -4, + GET_STATE_ERROR = -5, + SET_STATE_ERROR = -6, + SET_TIMEOUT_ERROR = -7, + BUFFER_ERROR = -8, + NOT_FOUND_ERROR = -9, + CLEAR_BUFFER_IN_ERROR = -10, + CLEAR_BUFFER_OUT_ERROR = -11, + ABORT_READ_ERROR = -12, + ABORT_WRITE_ERROR = -13, }; diff --git a/tests/serial_unit_tests.cpp b/tests/serial_unit_tests.cpp index dcfd6b6..32e91da 100644 --- a/tests/serial_unit_tests.cpp +++ b/tests/serial_unit_tests.cpp @@ -1,10 +1,19 @@ #include "serial.h" #include "status_codes.h" +#include #include +#include +#include #include +#include #include #include +#include +#include +#include +#include +#include namespace { @@ -18,6 +27,44 @@ void errorCallback(int code) *g_err_ptr = code; } } + +// Helper to resolve serial port path (env var override) +const char* getDefaultPort() +{ + const char* env = std::getenv("SERIAL_PORT"); + return (env != nullptr) ? env : "/dev/ttyUSB0"; +} + +struct SerialDevice +{ + intptr_t handle{0}; + const char* port{nullptr}; + + explicit SerialDevice(int baud = 115200) + { + port = getDefaultPort(); + handle = serialOpen((void*)port, baud, 8, 0, 0); + if (handle == 0) + { + throw std::runtime_error(std::string{"Failed to open port "} + port); + } + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); // give Arduino time to reboot after DTR toggle + } + + ~SerialDevice() + { + if (handle != 0) + { + serialClose(handle); + } + } + + void writeToDevice(std::string_view data) + { + serialWrite(handle, data.data(), static_cast(data.size()), 500, 1); + } +}; + } // namespace // ------------------------------- Error path -------------------------------- @@ -116,3 +163,74 @@ TEST(SerialStubbedFunctions, DoNotCrash) serialAbortWrite(0); SUCCEED(); // reached here without segfaults } + +TEST(SerialHelpers, ReadLine) +{ + SerialDevice dev; + const std::string msg = "Hello World\n"; + dev.writeToDevice(msg); + + char buf[64] = {0}; + int n = serialReadLine(dev.handle, buf, sizeof(buf), 2000); + ASSERT_EQ(n, static_cast(msg.size())); + ASSERT_EQ(std::string_view(buf, n), msg); +} + +TEST(SerialHelpers, ReadUntilToken) +{ + SerialDevice dev; + const std::string payload = "ABC_OK"; + dev.writeToDevice(payload); + + char buf[64] = {0}; + const char token[] = "OK"; + int n = serialReadUntilToken(dev.handle, buf, sizeof(buf), 2000, (void*)token); + ASSERT_EQ(n, static_cast(payload.size())); + ASSERT_EQ(std::string_view(buf, n), payload); +} + +TEST(SerialHelpers, Peek) +{ + SerialDevice dev; + const std::string payload = "XYZ"; + dev.writeToDevice(payload); + + char first = 0; + int res = serialPeek(dev.handle, &first, 2000); + ASSERT_EQ(res, 1); + ASSERT_EQ(first, 'X'); + + char buf[4] = {0}; + int n = serialRead(dev.handle, buf, 3, 2000, 1); + ASSERT_EQ(n, 3); + ASSERT_EQ(std::string_view(buf, 3), payload); +} + +TEST(SerialHelpers, Statistics) +{ + SerialDevice dev; + const std::string payload = "0123456789"; + + // Transmit to device + int written = serialWrite(dev.handle, payload.c_str(), static_cast(payload.size()), 2000, 1); + ASSERT_EQ(written, static_cast(payload.size())); + + // Drain and read echo back + serialDrain(dev.handle); + + char buf[16] = {0}; + int read_bytes = serialRead(dev.handle, buf, static_cast(payload.size()), 2000, 1); + ASSERT_EQ(read_bytes, static_cast(payload.size())); + + ASSERT_EQ(serialGetTxBytes(dev.handle), static_cast(payload.size())); + ASSERT_EQ(serialGetRxBytes(dev.handle), static_cast(payload.size())); +} + +TEST(SerialHelpers, Drain) +{ + SerialDevice dev; + const std::string payload = "TEXT"; + int written = serialWriteLine(dev.handle, payload.c_str(), static_cast(payload.size()), 2000); + ASSERT_GT(written, 0); + ASSERT_EQ(serialDrain(dev.handle), 1); +} From f0b68814122c856ebb98060f4828bafe851076ab Mon Sep 17 00:00:00 2001 From: Katze719 Date: Fri, 4 Jul 2025 17:22:24 +0000 Subject: [PATCH 02/18] modernize --- CMakeLists.txt | 2 - src/serial.cpp | 143 ++++++++++++++++++------------------ tests/serial_test.cpp | 9 ++- tests/serial_unit_tests.cpp | 75 ++++++++++--------- 4 files changed, 112 insertions(+), 117 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 12feaa7..bd4a3dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,12 +78,10 @@ set_target_properties(${PROJECT_N} PROPERTIES target_include_directories(${PROJECT_N} PUBLIC ${PROJECT_SOURCE_DIR}/src) -# Combined test target aggregating all test sources add_executable(tests tests/serial_test.cpp tests/serial_unit_tests.cpp) -# Link against the library under test and GoogleTest (without gtest_main, we have our own main) target_link_libraries(tests PRIVATE ${PROJECT_N} gtest) target_include_directories(tests PRIVATE ${PROJECT_SOURCE_DIR}/src) diff --git a/src/serial.cpp b/src/serial.cpp index a158146..8fcc98f 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -30,17 +30,15 @@ namespace struct SerialPortHandle { - int fd; + int file_descriptor; termios original; // keep original settings so we can restore on close - // --- extensions --- int64_t rx_total{0}; // bytes received so far int64_t tx_total{0}; // bytes transmitted so far bool has_peek{false}; char peek_char{0}; - // Abort flags (set from other threads) std::atomic abort_read{false}; std::atomic abort_write{false}; }; @@ -124,20 +122,20 @@ intptr_t serialOpen(void* port, int baudrate, int dataBits, int parity, int stop } auto port_name = std::string_view{static_cast(port)}; - int fd = open(port_name.data(), O_RDWR | O_NOCTTY | O_SYNC); - if (fd < 0) + int device_descriptor = open(port_name.data(), O_RDWR | O_NOCTTY | O_SYNC); + if (device_descriptor < 0) { invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); return 0; } - auto* handle = new SerialPortHandle{.fd = fd, .original = {}}; + auto* handle = new SerialPortHandle{.file_descriptor = device_descriptor, .original = {}}; termios tty{}; - if (tcgetattr(fd, &tty) != 0) + if (tcgetattr(device_descriptor, &tty) != 0) { invokeError(std::to_underlying(StatusCodes::GET_STATE_ERROR)); - close(fd); + close(device_descriptor); delete handle; return 0; } @@ -205,10 +203,10 @@ intptr_t serialOpen(void* port, int baudrate, int dataBits, int parity, int stop tty.c_cc[VMIN] = 0; // non-blocking by default tty.c_cc[VTIME] = 10; // 1s read timeout - if (tcsetattr(fd, TCSANOW, &tty) != 0) + if (tcsetattr(device_descriptor, TCSANOW, &tty) != 0) { invokeError(std::to_underlying(StatusCodes::SET_STATE_ERROR)); - close(fd); + close(device_descriptor); delete handle; return 0; } @@ -224,28 +222,29 @@ void serialClose(int64_t handlePtr) return; } - tcsetattr(handle->fd, TCSANOW, &handle->original); // restore - if (close(handle->fd) != 0) + tcsetattr(handle->file_descriptor, TCSANOW, &handle->original); // restore + if (close(handle->file_descriptor) != 0) { invokeError(std::to_underlying(StatusCodes::CLOSE_HANDLE_ERROR)); } delete handle; } -static int waitFdReady(int fd, int timeoutMs, bool wantWrite) +static int waitFdReady(int fileDescriptor, int timeoutMs, bool wantWrite) { timeoutMs = std::max(timeoutMs, 0); - fd_set set; - FD_ZERO(&set); - FD_SET(fd, &set); + fd_set descriptor_set; + FD_ZERO(&descriptor_set); + FD_SET(fileDescriptor, &descriptor_set); - timeval tv{}; - tv.tv_sec = timeoutMs / 1000; - tv.tv_usec = (timeoutMs % 1000) * 1000; + timeval wait_time{}; + wait_time.tv_sec = timeoutMs / 1000; + wait_time.tv_usec = (timeoutMs % 1000) * 1000; - int res = select(fd + 1, wantWrite ? nullptr : &set, wantWrite ? &set : nullptr, nullptr, &tv); - return res; // 0 timeout, -1 error, >0 ready + int ready_result = + select(fileDescriptor + 1, wantWrite ? nullptr : &descriptor_set, wantWrite ? &descriptor_set : nullptr, nullptr, &wait_time); + return ready_result; // 0 timeout, -1 error, >0 ready } int serialRead(int64_t handlePtr, void* buffer, int bufferSize, int timeout, int /*multiplier*/) @@ -284,24 +283,24 @@ int serialRead(int64_t handlePtr, void* buffer, int bufferSize, int timeout, int } } - if (waitFdReady(handle->fd, timeout, false) <= 0) + if (waitFdReady(handle->file_descriptor, timeout, false) <= 0) { return total_copied; // return what we may have already copied (could be 0) } - ssize_t n = read(handle->fd, buffer, bufferSize); - if (n < 0) + ssize_t bytes_read_system = read(handle->file_descriptor, buffer, bufferSize); + if (bytes_read_system < 0) { invokeError(std::to_underlying(StatusCodes::READ_ERROR)); return total_copied; } - if (n > 0) + if (bytes_read_system > 0) { - handle->rx_total += n; + handle->rx_total += bytes_read_system; } - total_copied += static_cast(n); + total_copied += static_cast(bytes_read_system); if (read_callback != nullptr) { @@ -325,28 +324,28 @@ int serialWrite(int64_t handlePtr, const void* buffer, int bufferSize, int timeo return 0; } - if (waitFdReady(handle->fd, timeout, true) <= 0) + if (waitFdReady(handle->file_descriptor, timeout, true) <= 0) { return 0; // timeout or error } - ssize_t n = write(handle->fd, buffer, bufferSize); - if (n < 0) + ssize_t bytes_written_system = write(handle->file_descriptor, buffer, bufferSize); + if (bytes_written_system < 0) { invokeError(std::to_underlying(StatusCodes::WRITE_ERROR)); return 0; } - if (n > 0) + if (bytes_written_system > 0) { - handle->tx_total += n; + handle->tx_total += bytes_written_system; } if (write_callback != nullptr) { - write_callback(static_cast(n)); + write_callback(static_cast(bytes_written_system)); } - return static_cast(n); + return static_cast(bytes_written_system); } int serialReadUntil(int64_t handlePtr, void* buffer, int bufferSize, int timeout, int /*multiplier*/, void* untilCharPtr) @@ -358,23 +357,23 @@ int serialReadUntil(int64_t handlePtr, void* buffer, int bufferSize, int timeout return 0; } - char until_char = *static_cast(untilCharPtr); + char until_character = *static_cast(untilCharPtr); int total = 0; - auto* buf = static_cast(buffer); + auto* char_buffer = static_cast(buffer); while (total < bufferSize) { - int res = serialRead(handlePtr, buf + total, 1, timeout, 1); - if (res <= 0) + int read_result = serialRead(handlePtr, char_buffer + total, 1, timeout, 1); + if (read_result <= 0) { break; // timeout or error } - if (buf[total] == until_char) + if (char_buffer[total] == until_character) { total += 1; break; } - total += res; + total += read_result; } if (read_callback != nullptr) @@ -407,9 +406,9 @@ int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) continue; } - std::error_code ec; - fs::path canonical = fs::canonical(entry.path(), ec); - if (ec) + std::error_code error_code; + fs::path canonical = fs::canonical(entry.path(), error_code); + if (error_code) { continue; // skip entries we cannot resolve } @@ -451,7 +450,7 @@ void serialClearBufferIn(int64_t handlePtr) { return; } - tcflush(handle->fd, TCIFLUSH); + tcflush(handle->file_descriptor, TCIFLUSH); // reset peek buffer handle->has_peek = false; } @@ -463,7 +462,7 @@ void serialClearBufferOut(int64_t handlePtr) { return; } - tcflush(handle->fd, TCOFLUSH); + tcflush(handle->file_descriptor, TCOFLUSH); } void serialAbortRead(int64_t handlePtr) @@ -521,9 +520,9 @@ int serialWriteLine(int64_t handlePtr, const void* buffer, int bufferSize, int t return written; // error path, propagate } // Append newline (\n) - char nl = '\n'; - int w_nl = serialWrite(handlePtr, &nl, 1, timeout, 1); - if (w_nl != 1) + char new_line_char = '\n'; + int newline_result = serialWrite(handlePtr, &new_line_char, 1, timeout, 1); + if (newline_result != 1) { return written; // newline failed, but payload written } @@ -532,7 +531,7 @@ int serialWriteLine(int64_t handlePtr, const void* buffer, int bufferSize, int t int serialReadUntilToken(int64_t handlePtr, void* buffer, int bufferSize, int timeout, void* tokenPtr) { - auto* token_cstr = static_cast(tokenPtr); + const auto* token_cstr = static_cast(tokenPtr); if (token_cstr == nullptr) { invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); @@ -545,22 +544,22 @@ int serialReadUntilToken(int64_t handlePtr, void* buffer, int bufferSize, int ti return 0; } - auto* buf = static_cast(buffer); + auto* char_buffer = static_cast(buffer); int total = 0; int matched = 0; // how many chars of token matched so far while (total < bufferSize) { - int res = serialRead(handlePtr, buf + total, 1, timeout, 1); - if (res <= 0) + int read_result = serialRead(handlePtr, char_buffer + total, 1, timeout, 1); + if (read_result <= 0) { break; // timeout or error } - char c = buf[total]; + char current_char = char_buffer[total]; total += 1; - if (c == token[matched]) + if (current_char == token[matched]) { matched += 1; if (matched == token_len) @@ -570,7 +569,7 @@ int serialReadUntilToken(int64_t handlePtr, void* buffer, int bufferSize, int ti } else { - matched = (c == token[0]) ? 1 : 0; // restart match search + matched = (current_char == token[0]) ? 1 : 0; // restart match search } } @@ -583,35 +582,33 @@ int serialReadUntilToken(int64_t handlePtr, void* buffer, int bufferSize, int ti int serialReadFrame(int64_t handlePtr, void* buffer, int bufferSize, int timeout, char startByte, char endByte) { - auto* buf = static_cast(buffer); + auto* char_buffer = static_cast(buffer); int total = 0; bool in_frame = false; while (total < bufferSize) { - char byte; - int res = serialRead(handlePtr, &byte, 1, timeout, 1); - if (res <= 0) + char current_byte; + int read_result = serialRead(handlePtr, ¤t_byte, 1, timeout, 1); + if (read_result <= 0) { break; // timeout } if (!in_frame) { - if (byte == startByte) + if (current_byte == startByte) { in_frame = true; - buf[total++] = byte; + char_buffer[total++] = current_byte; } continue; // ignore bytes until start byte detected } - else + + char_buffer[total++] = current_byte; + if (current_byte == endByte) { - buf[total++] = byte; - if (byte == endByte) - { - break; // frame finished - } + break; // frame finished } } @@ -653,22 +650,22 @@ int serialPeek(int64_t handlePtr, void* outByte, int timeout) return 1; } - char c; - int res = serialRead(handlePtr, &c, 1, timeout, 1); - if (res <= 0) + char received_byte; + int read_outcome = serialRead(handlePtr, &received_byte, 1, timeout, 1); + if (read_outcome <= 0) { return 0; // nothing available } // Store into peek buffer and undo stats increment - handle->peek_char = c; + handle->peek_char = received_byte; handle->has_peek = true; if (handle->rx_total > 0) { handle->rx_total -= 1; // don't account peek } - *static_cast(outByte) = c; + *static_cast(outByte) = received_byte; return 1; } @@ -680,5 +677,5 @@ int serialDrain(int64_t handlePtr) invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); return 0; } - return (tcdrain(handle->fd) == 0) ? 1 : 0; + return (tcdrain(handle->file_descriptor) == 0) ? 1 : 0; } diff --git a/tests/serial_test.cpp b/tests/serial_test.cpp index 71a795f..2a8fe53 100644 --- a/tests/serial_test.cpp +++ b/tests/serial_test.cpp @@ -58,11 +58,12 @@ TEST(SerialEchoTest, EchoMessage) ASSERT_EQ(written, static_cast(test_msg.size())) << "Write failed"; // Read echo - char buffer[16] = {0}; - int read_bytes = serialRead(handle, buffer, static_cast(test_msg.size()), 500, 1); - ASSERT_EQ(read_bytes, static_cast(test_msg.size())) << "Read failed (got " << read_bytes << ")"; + std::array read_buffer{}; + int bytes_read = serialRead(handle, read_buffer.data(), static_cast(test_msg.size()), 500, 1); + ASSERT_EQ(bytes_read, static_cast(test_msg.size())) << "Read failed (got " << bytes_read << ")"; - ASSERT_EQ(std::strncmp(buffer, test_msg.c_str(), test_msg.size()), 0) << "Data mismatch: expected " << test_msg << ", got " << buffer; + ASSERT_EQ(std::strncmp(read_buffer.data(), test_msg.c_str(), test_msg.size()), 0) + << "Data mismatch: expected " << test_msg << ", got " << read_buffer.data(); serialClose(handle); } diff --git a/tests/serial_unit_tests.cpp b/tests/serial_unit_tests.cpp index 32e91da..6f87f4b 100644 --- a/tests/serial_unit_tests.cpp +++ b/tests/serial_unit_tests.cpp @@ -13,7 +13,6 @@ #include #include #include -#include namespace { @@ -59,7 +58,7 @@ struct SerialDevice } } - void writeToDevice(std::string_view data) + void writeToDevice(std::string_view data) const { serialWrite(handle, data.data(), static_cast(data.size()), 500, 1); } @@ -86,15 +85,15 @@ TEST(SerialOpenTest, InvalidPathInvokesErrorCallback) // ------------------------ serialGetPortsInfo checks ------------------------ TEST(SerialGetPortsInfoTest, BufferTooSmallTriggersError) { - char sep[] = ";"; - char buffer[4]; + constexpr std::string_view separator{";"}; + std::array info_buffer{}; std::atomic err_code{0}; g_err_ptr = &err_code; serialOnError(errorCallback); - int res = serialGetPortsInfo(buffer, sizeof(buffer), sep); - EXPECT_EQ(res, 0); // function indicates failure via 0 + int result = serialGetPortsInfo(info_buffer.data(), static_cast(info_buffer.size()), (void*)separator.data()); + EXPECT_EQ(result, 0); // function indicates failure via 0 EXPECT_EQ(err_code.load(), static_cast(StatusCodes::BUFFER_ERROR)); serialOnError(nullptr); @@ -102,17 +101,17 @@ TEST(SerialGetPortsInfoTest, BufferTooSmallTriggersError) TEST(SerialGetPortsInfoTest, LargeBufferReturnsZeroOrOne) { - char sep[] = ";"; - char buffer[4096] = {0}; + constexpr std::string_view separator{";"}; + std::array info_buffer{}; std::atomic err_code{0}; g_err_ptr = &err_code; serialOnError(errorCallback); - int res = serialGetPortsInfo(buffer, sizeof(buffer), sep); - EXPECT_GE(res, 0); + int result = serialGetPortsInfo(info_buffer.data(), static_cast(info_buffer.size()), (void*)separator.data()); + EXPECT_GE(result, 0); // res is 0 (no ports) or 1 (ports found) - EXPECT_LE(res, 1); + EXPECT_LE(result, 1); // Acceptable error codes: none or NOT_FOUND_ERROR (e.g., dir missing) if (err_code != 0) { @@ -125,27 +124,27 @@ TEST(SerialGetPortsInfoTest, LargeBufferReturnsZeroOrOne) // ---------------------------- Port listing helper --------------------------- TEST(SerialGetPortsInfoTest, PrintAvailablePorts) { - char sep[] = ";"; - char buffer[4096] = {0}; + constexpr std::string_view separator{";"}; + std::array info_buffer{}; - int res = serialGetPortsInfo(buffer, sizeof(buffer), sep); - EXPECT_GE(res, 0); + int result = serialGetPortsInfo(info_buffer.data(), static_cast(info_buffer.size()), (void*)separator.data()); + EXPECT_GE(result, 0); - std::string ports_str(buffer); + std::string ports_str(info_buffer.data()); if (!ports_str.empty()) { std::cout << "\nAvailable serial ports (by-id):\n"; size_t start = 0; while (true) { - size_t pos = ports_str.find(sep, start); + size_t pos = ports_str.find(separator.data(), start); std::string token = ports_str.substr(start, pos - start); std::cout << " " << token << "\n"; if (pos == std::string::npos) { break; } - start = pos + std::strlen(sep); + start = pos + std::strlen(separator.data()); } } else @@ -170,10 +169,10 @@ TEST(SerialHelpers, ReadLine) const std::string msg = "Hello World\n"; dev.writeToDevice(msg); - char buf[64] = {0}; - int n = serialReadLine(dev.handle, buf, sizeof(buf), 2000); - ASSERT_EQ(n, static_cast(msg.size())); - ASSERT_EQ(std::string_view(buf, n), msg); + std::array read_buffer{}; + int num_read = serialReadLine(dev.handle, read_buffer.data(), static_cast(read_buffer.size()), 2000); + ASSERT_EQ(num_read, static_cast(msg.size())); + ASSERT_EQ(std::string_view(read_buffer.data(), num_read), msg); } TEST(SerialHelpers, ReadUntilToken) @@ -182,11 +181,11 @@ TEST(SerialHelpers, ReadUntilToken) const std::string payload = "ABC_OK"; dev.writeToDevice(payload); - char buf[64] = {0}; - const char token[] = "OK"; - int n = serialReadUntilToken(dev.handle, buf, sizeof(buf), 2000, (void*)token); - ASSERT_EQ(n, static_cast(payload.size())); - ASSERT_EQ(std::string_view(buf, n), payload); + std::array read_buffer{}; + constexpr std::string_view ok_token{"OK"}; + int num_read = serialReadUntilToken(dev.handle, read_buffer.data(), static_cast(read_buffer.size()), 2000, (void*)ok_token.data()); + ASSERT_EQ(num_read, static_cast(payload.size())); + ASSERT_EQ(std::string_view(read_buffer.data(), num_read), payload); } TEST(SerialHelpers, Peek) @@ -195,15 +194,15 @@ TEST(SerialHelpers, Peek) const std::string payload = "XYZ"; dev.writeToDevice(payload); - char first = 0; - int res = serialPeek(dev.handle, &first, 2000); - ASSERT_EQ(res, 1); - ASSERT_EQ(first, 'X'); + char first_byte = 0; + int peek_result = serialPeek(dev.handle, &first_byte, 2000); + ASSERT_EQ(peek_result, 1); + ASSERT_EQ(first_byte, 'X'); - char buf[4] = {0}; - int n = serialRead(dev.handle, buf, 3, 2000, 1); - ASSERT_EQ(n, 3); - ASSERT_EQ(std::string_view(buf, 3), payload); + std::array read_buffer{}; + int num_read = serialRead(dev.handle, read_buffer.data(), 3, 2000, 1); + ASSERT_EQ(num_read, 3); + ASSERT_EQ(std::string_view(read_buffer.data(), 3), payload); } TEST(SerialHelpers, Statistics) @@ -218,9 +217,9 @@ TEST(SerialHelpers, Statistics) // Drain and read echo back serialDrain(dev.handle); - char buf[16] = {0}; - int read_bytes = serialRead(dev.handle, buf, static_cast(payload.size()), 2000, 1); - ASSERT_EQ(read_bytes, static_cast(payload.size())); + std::array read_buffer{}; + int bytes_read = serialRead(dev.handle, read_buffer.data(), static_cast(payload.size()), 2000, 1); + ASSERT_EQ(bytes_read, static_cast(payload.size())); ASSERT_EQ(serialGetTxBytes(dev.handle), static_cast(payload.size())); ASSERT_EQ(serialGetRxBytes(dev.handle), static_cast(payload.size())); From 88149e67c108b99e4f900764c5d321b286d0ca92 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Fri, 4 Jul 2025 19:09:19 +0000 Subject: [PATCH 03/18] refactor project name to lowercase and update references in CMake, README, CI, and examples --- .github/workflows/ci.yml | 8 ++++---- CMakeLists.txt | 2 +- README.md | 14 +++++++------- examples/serial_advanced.ts | 4 ++-- examples/serial_echo.ts | 2 +- tests/serial_test.cpp | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a66b50..d22169c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Build & Release CPP-Unix-Bindings +name: Build & Release cpp_unix_bindings on: push: @@ -34,12 +34,12 @@ jobs: - name: Upload library artifact uses: actions/upload-artifact@v4 with: - name: libCPP-Unix-Bindings - path: build/libCPP-Unix-Bindings.so + name: libcpp_unix_bindings + path: build/libcpp_unix_bindings.so retention-days: 14 - name: Attach library to release if: github.event_name == 'release' uses: softprops/action-gh-release@v1 with: - files: build/libCPP-Unix-Bindings.so + files: build/libcpp_unix_bindings.so diff --git a/CMakeLists.txt b/CMakeLists.txt index bd4a3dc..748b551 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ set(VERSION_MAJOR 0) set(VERSION_MINOR 2) set(VERSION_PATCH 0) -set(PROJECT_N CPP-Unix-Bindings) +set(PROJECT_N cpp_unix_bindings) project(${PROJECT_N} VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}) # Generate compile_commands.json for clang-based tooling (clangd / clang-tidy) diff --git a/README.md b/README.md index 1f43a6a..2d50175 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# CPP-Unix-Bindings +# cpp_unix_bindings A compact C++23 library for talking to serial devices on Linux (e.g. Arduino). -The project builds a **shared library `libCPP-Unix-Bindings.so`** that can be used via +The project builds a **shared library `libcpp_unix_bindings.so`** that can be used via Deno's native FFI. --- @@ -21,7 +21,7 @@ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j # The resulting library will be located at -# build/libCPP-Unix-Bindings.so +# build/libcpp_unix_bindings.so ``` > Because `CMAKE_EXPORT_COMPILE_COMMANDS` is enabled, the build also generates a @@ -36,7 +36,7 @@ Deno ships with a first-class FFI API. ```ts // serial_deno.ts -const lib = Deno.dlopen('./build/libCPP-Unix-Bindings.so', { +const lib = Deno.dlopen('./build/libcpp_unix_bindings.so', { serialOpen: { parameters: [ 'buffer', 'i32', 'i32', 'i32', 'i32' ], result: 'pointer' }, serialClose: { parameters: [ 'pointer' ], result: 'void' }, serialRead: { parameters: [ 'pointer', 'buffer', 'i32', 'i32', 'i32' ], result: 'i32' }, @@ -110,7 +110,7 @@ shared library. Run it with: ```bash deno run --allow-ffi --allow-read examples/serial_echo.ts \ - --lib ./build/libCPP-Unix-Bindings.so \ + --lib ./build/libcpp_unix_bindings.so \ --port /dev/ttyUSB0 ``` @@ -119,12 +119,12 @@ shared library. lines and then prints the TX/RX counters. ```bash deno run --allow-ffi --allow-read examples/serial_advanced.ts \ - --lib ./build/libCPP-Unix-Bindings.so \ + --lib ./build/libcpp_unix_bindings.so \ --port /dev/ttyUSB0 ``` Notes: -1. `--lib` defaults to `./build/libCPP-Unix-Bindings.so`; pass a custom path if +1. `--lib` defaults to `./build/libcpp_unix_bindings.so`; pass a custom path if you installed the library elsewhere. 2. `--port` defaults to `/dev/ttyUSB0`; adjust if your board shows up under a different device (e.g. `/dev/ttyACM0`). diff --git a/examples/serial_advanced.ts b/examples/serial_advanced.ts index d03fabd..69f8b9b 100644 --- a/examples/serial_advanced.ts +++ b/examples/serial_advanced.ts @@ -1,5 +1,5 @@ // @ts-nocheck -// deno run --allow-ffi --allow-read examples/serial_advanced.ts --lib ./build/libCPP-Unix-Bindings.so --port /dev/ttyUSB0 +// deno run --allow-ffi --allow-read examples/serial_advanced.ts --lib ./build/libcpp_unix_bindings.so --port /dev/ttyUSB0 /* Advanced Deno example showcasing the extended C-API helpers: @@ -17,7 +17,7 @@ interface CliOptions { } function parseArgs(): CliOptions { - const opts: CliOptions = { lib: "./build/libCPP-Unix-Bindings.so" }; + const opts: CliOptions = { lib: "./build/libcpp_unix_bindings.so" }; for (let i = 0; i < Deno.args.length; ++i) { const arg = Deno.args[i]; diff --git a/examples/serial_echo.ts b/examples/serial_echo.ts index 91c6fcd..d9b8905 100644 --- a/examples/serial_echo.ts +++ b/examples/serial_echo.ts @@ -7,7 +7,7 @@ interface CliOptions { } function parseArgs(): CliOptions { - const opts: CliOptions = { lib: "./build/libCPP-Unix-Bindings.so" }; + const opts: CliOptions = { lib: "./build/libcpp_unix_bindings.so" }; for (let i = 0; i < Deno.args.length; ++i) { const arg = Deno.args[i]; diff --git a/tests/serial_test.cpp b/tests/serial_test.cpp index 2a8fe53..73dd0ed 100644 --- a/tests/serial_test.cpp +++ b/tests/serial_test.cpp @@ -1,4 +1,4 @@ -// Simple integration test for CPP-Unix-Bindings +// Simple integration test for cpp_unix_bindings // ------------------------------------------------ // This executable opens the given serial port, sends a test // string and verifies that the same string is echoed back From ecca33e2a20bc420ad405c63a4b141e4bfc3987d Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 5 Jul 2025 23:21:49 +0000 Subject: [PATCH 04/18] refactor serial API to improve callback handling and update versioning structure --- .gitignore | 2 +- CMakeLists.txt | 2 +- src/serial.cpp | 164 ++++++++++++++----------------- src/serial.h | 28 +++--- src/version_config.cpp | 22 +++++ tests/serial_unit_tests.cpp | 26 +---- versioning/version_config.cpp.in | 14 --- versioning/version_config.h.in | 11 +++ 8 files changed, 130 insertions(+), 139 deletions(-) create mode 100644 src/version_config.cpp delete mode 100644 versioning/version_config.cpp.in create mode 100644 versioning/version_config.h.in diff --git a/.gitignore b/.gitignore index f80d786..33bb172 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .vscode/ build/ -src/version_config.cpp +src/version_config.h .cache/ compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 748b551..c2923d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ file(GLOB_RECURSE SRCS ${PROJECT_SOURCE_DIR}/src/**.cpp) set(LIB true) -configure_file(versioning/version_config.cpp.in ${PROJECT_SOURCE_DIR}/src/version_config.cpp) +configure_file(versioning/version_config.h.in ${PROJECT_SOURCE_DIR}/src/version_config.h) # a macro that gets all of the header containing directories. MACRO(header_directories return_list includes_base_folder extention ) diff --git a/src/serial.cpp b/src/serial.cpp index 8fcc98f..08c722d 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -18,9 +18,9 @@ // ----------------------------------------------------------------------------- // Global callback function pointers (default nullptr) // ----------------------------------------------------------------------------- -void (*error_callback)(int) = nullptr; -void (*read_callback)(int) = nullptr; -void (*write_callback)(int) = nullptr; +void (*on_error_callback)(int errorCode, const char* message) = nullptr; +void (*on_read_callback)(int bytes) = nullptr; +void (*on_write_callback)(int bytes) = nullptr; // ----------------------------------------------------------------------------- // Internal helpers & types @@ -36,9 +36,6 @@ struct SerialPortHandle int64_t rx_total{0}; // bytes received so far int64_t tx_total{0}; // bytes transmitted so far - bool has_peek{false}; - char peek_char{0}; - std::atomic abort_read{false}; std::atomic abort_write{false}; }; @@ -99,11 +96,11 @@ auto to_speed_t(int baud) -> speed_t } } -inline void invokeError(int code) +inline void invokeError(int code, const char* message) { - if (error_callback != nullptr) + if (on_error_callback != nullptr) { - error_callback(code); + on_error_callback(code, message); } } @@ -117,7 +114,7 @@ intptr_t serialOpen(void* port, int baudrate, int dataBits, int parity, int stop { if (port == nullptr) { - invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialOpen: Invalid handle"); return 0; } @@ -125,7 +122,7 @@ intptr_t serialOpen(void* port, int baudrate, int dataBits, int parity, int stop int device_descriptor = open(port_name.data(), O_RDWR | O_NOCTTY | O_SYNC); if (device_descriptor < 0) { - invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialOpen: Failed to open serial port"); return 0; } @@ -134,7 +131,7 @@ intptr_t serialOpen(void* port, int baudrate, int dataBits, int parity, int stop termios tty{}; if (tcgetattr(device_descriptor, &tty) != 0) { - invokeError(std::to_underlying(StatusCodes::GET_STATE_ERROR)); + invokeError(std::to_underlying(StatusCodes::GET_STATE_ERROR), "serialOpen: Failed to get serial attributes"); close(device_descriptor); delete handle; return 0; @@ -205,7 +202,7 @@ intptr_t serialOpen(void* port, int baudrate, int dataBits, int parity, int stop if (tcsetattr(device_descriptor, TCSANOW, &tty) != 0) { - invokeError(std::to_underlying(StatusCodes::SET_STATE_ERROR)); + invokeError(std::to_underlying(StatusCodes::SET_STATE_ERROR), "serialOpen: Failed to set serial attributes"); close(device_descriptor); delete handle; return 0; @@ -225,7 +222,7 @@ void serialClose(int64_t handlePtr) tcsetattr(handle->file_descriptor, TCSANOW, &handle->original); // restore if (close(handle->file_descriptor) != 0) { - invokeError(std::to_underlying(StatusCodes::CLOSE_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::CLOSE_HANDLE_ERROR), "serialClose: Failed to close serial port"); } delete handle; } @@ -252,7 +249,7 @@ int serialRead(int64_t handlePtr, void* buffer, int bufferSize, int timeout, int auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) { - invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialRead: Invalid handle"); return 0; } @@ -264,25 +261,6 @@ int serialRead(int64_t handlePtr, void* buffer, int bufferSize, int timeout, int int total_copied = 0; - // First deliver byte from internal peek buffer if present - if (handle->has_peek && bufferSize > 0) - { - static_cast(buffer)[0] = handle->peek_char; - handle->has_peek = false; - handle->rx_total += 1; - total_copied = 1; - buffer = static_cast(buffer) + 1; - bufferSize -= 1; - if (bufferSize == 0) - { - if (read_callback != nullptr) - { - read_callback(total_copied); - } - return total_copied; - } - } - if (waitFdReady(handle->file_descriptor, timeout, false) <= 0) { return total_copied; // return what we may have already copied (could be 0) @@ -291,7 +269,7 @@ int serialRead(int64_t handlePtr, void* buffer, int bufferSize, int timeout, int ssize_t bytes_read_system = read(handle->file_descriptor, buffer, bufferSize); if (bytes_read_system < 0) { - invokeError(std::to_underlying(StatusCodes::READ_ERROR)); + invokeError(std::to_underlying(StatusCodes::READ_ERROR), "serialRead: Read error"); return total_copied; } @@ -302,9 +280,9 @@ int serialRead(int64_t handlePtr, void* buffer, int bufferSize, int timeout, int total_copied += static_cast(bytes_read_system); - if (read_callback != nullptr) + if (on_read_callback != nullptr) { - read_callback(total_copied); + on_read_callback(total_copied); } return total_copied; } @@ -314,7 +292,7 @@ int serialWrite(int64_t handlePtr, const void* buffer, int bufferSize, int timeo auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) { - invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialWrite: Invalid handle"); return 0; } @@ -332,7 +310,7 @@ int serialWrite(int64_t handlePtr, const void* buffer, int bufferSize, int timeo ssize_t bytes_written_system = write(handle->file_descriptor, buffer, bufferSize); if (bytes_written_system < 0) { - invokeError(std::to_underlying(StatusCodes::WRITE_ERROR)); + invokeError(std::to_underlying(StatusCodes::WRITE_ERROR), "serialWrite: Write error"); return 0; } @@ -341,9 +319,9 @@ int serialWrite(int64_t handlePtr, const void* buffer, int bufferSize, int timeo handle->tx_total += bytes_written_system; } - if (write_callback != nullptr) + if (on_write_callback != nullptr) { - write_callback(static_cast(bytes_written_system)); + on_write_callback(static_cast(bytes_written_system)); } return static_cast(bytes_written_system); } @@ -353,7 +331,7 @@ int serialReadUntil(int64_t handlePtr, void* buffer, int bufferSize, int timeout auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) { - invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialReadUntil: Invalid handle"); return 0; } @@ -376,9 +354,9 @@ int serialReadUntil(int64_t handlePtr, void* buffer, int bufferSize, int timeout total += read_result; } - if (read_callback != nullptr) + if (on_read_callback != nullptr) { - read_callback(total); + on_read_callback(total); } return total; } @@ -393,7 +371,7 @@ int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) const fs::path by_id_dir{"/dev/serial/by-id"}; if (!fs::exists(by_id_dir) || !fs::is_directory(by_id_dir)) { - invokeError(std::to_underlying(StatusCodes::NOT_FOUND_ERROR)); + invokeError(std::to_underlying(StatusCodes::NOT_FOUND_ERROR), "serialGetPortsInfo: Failed to get ports info"); return 0; } @@ -419,7 +397,7 @@ int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) } catch (const fs::filesystem_error&) { - invokeError(std::to_underlying(StatusCodes::NOT_FOUND_ERROR)); + invokeError(std::to_underlying(StatusCodes::NOT_FOUND_ERROR), "serialGetPortsInfo: Failed to get ports info"); return 0; } @@ -431,7 +409,7 @@ int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) if (static_cast(result.size()) + 1 > bufferSize) { - invokeError(std::to_underlying(StatusCodes::BUFFER_ERROR)); + invokeError(std::to_underlying(StatusCodes::BUFFER_ERROR), "serialGetPortsInfo: Buffer too small"); return 0; } @@ -448,11 +426,10 @@ void serialClearBufferIn(int64_t handlePtr) auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) { + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialClearBufferIn: Invalid handle"); return; } tcflush(handle->file_descriptor, TCIFLUSH); - // reset peek buffer - handle->has_peek = false; } void serialClearBufferOut(int64_t handlePtr) @@ -460,6 +437,7 @@ void serialClearBufferOut(int64_t handlePtr) auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) { + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialClearBufferOut: Invalid handle"); return; } tcflush(handle->file_descriptor, TCOFLUSH); @@ -470,6 +448,7 @@ void serialAbortRead(int64_t handlePtr) auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) { + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialAbortRead: Invalid handle"); return; } handle->abort_read = true; @@ -480,6 +459,7 @@ void serialAbortWrite(int64_t handlePtr) auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) { + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialAbortWrite: Invalid handle"); return; } handle->abort_write = true; @@ -488,17 +468,17 @@ void serialAbortWrite(int64_t handlePtr) // ----------------------------------------------------------------------------- // Callback registration -void serialOnError(void (*func)(int code)) +void serialOnError(void (*func)(int code, const char* message)) { - error_callback = func; + on_error_callback = func; } void serialOnRead(void (*func)(int bytes)) { - read_callback = func; + on_read_callback = func; } void serialOnWrite(void (*func)(int bytes)) { - write_callback = func; + on_write_callback = func; } // ----------------------------------------------------------------------------- @@ -529,24 +509,24 @@ int serialWriteLine(int64_t handlePtr, const void* buffer, int bufferSize, int t return written + 1; } -int serialReadUntilToken(int64_t handlePtr, void* buffer, int bufferSize, int timeout, void* tokenPtr) +int serialReadUntilSequence(int64_t handlePtr, void* buffer, int bufferSize, int timeout, void* sequencePtr) { - const auto* token_cstr = static_cast(tokenPtr); - if (token_cstr == nullptr) + const auto* sequence_cstr = static_cast(sequencePtr); + if (sequence_cstr == nullptr) { - invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialReadUntilSequence: Invalid sequence"); return 0; } - std::string token{token_cstr}; - int token_len = static_cast(token.size()); - if (token_len == 0 || bufferSize < token_len) + std::string sequence{sequence_cstr}; + int sequence_len = static_cast(sequence.size()); + if (sequence_len == 0 || bufferSize < sequence_len) { return 0; } auto* char_buffer = static_cast(buffer); int total = 0; - int matched = 0; // how many chars of token matched so far + int matched = 0; // how many chars of sequence matched so far while (total < bufferSize) { @@ -559,23 +539,23 @@ int serialReadUntilToken(int64_t handlePtr, void* buffer, int bufferSize, int ti char current_char = char_buffer[total]; total += 1; - if (current_char == token[matched]) + if (current_char == sequence[matched]) { matched += 1; - if (matched == token_len) + if (matched == sequence_len) { - break; // token fully matched + break; // sequence fully matched } } else { - matched = (current_char == token[0]) ? 1 : 0; // restart match search + matched = (current_char == sequence[0]) ? 1 : 0; // restart match search } } - if (read_callback != nullptr) + if (on_read_callback != nullptr) { - read_callback(total); + on_read_callback(total); } return total; } @@ -615,7 +595,7 @@ int serialReadFrame(int64_t handlePtr, void* buffer, int bufferSize, int timeout return total; } -int64_t serialGetRxBytes(int64_t handlePtr) +int64_t serialInBytesTotal(int64_t handlePtr) { auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) @@ -625,7 +605,7 @@ int64_t serialGetRxBytes(int64_t handlePtr) return handle->rx_total; } -int64_t serialGetTxBytes(int64_t handlePtr) +int64_t serialOutBytesTotal(int64_t handlePtr) { auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) @@ -635,38 +615,46 @@ int64_t serialGetTxBytes(int64_t handlePtr) return handle->tx_total; } -int serialPeek(int64_t handlePtr, void* outByte, int timeout) +// Bytes currently queued in the driver/OS buffers -------------------------------- +int serialInBytesWaiting(int64_t handlePtr) { auto* handle = reinterpret_cast(handlePtr); - if (handle == nullptr || outByte == nullptr) + if (handle == nullptr) { - invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialInBytesWaiting: Invalid handle"); return 0; } - if (handle->has_peek) + int bytes_available = 0; + if (ioctl(handle->file_descriptor, FIONREAD, &bytes_available) == -1) { - *static_cast(outByte) = handle->peek_char; - return 1; + invokeError(std::to_underlying(StatusCodes::GET_STATE_ERROR), "serialInBytesWaiting: Failed to get state"); + return 0; } + return bytes_available; +} - char received_byte; - int read_outcome = serialRead(handlePtr, &received_byte, 1, timeout, 1); - if (read_outcome <= 0) +int serialOutBytesWaiting(int64_t handlePtr) +{ + auto* handle = reinterpret_cast(handlePtr); + if (handle == nullptr) { - return 0; // nothing available + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialOutBytesWaiting: Invalid handle"); + return 0; } - // Store into peek buffer and undo stats increment - handle->peek_char = received_byte; - handle->has_peek = true; - if (handle->rx_total > 0) + int bytes_queued = 0; +#ifdef TIOCOUTQ + if (ioctl(handle->file_descriptor, TIOCOUTQ, &bytes_queued) == -1) { - handle->rx_total -= 1; // don't account peek + invokeError(std::to_underlying(StatusCodes::GET_STATE_ERROR), "serialOutBytesWaiting: Failed to get state"); + return 0; } - - *static_cast(outByte) = received_byte; - return 1; +#else + // TIOCOUTQ not available on this platform – fallback: 0 + bytes_queued = 0; +#endif + return bytes_queued; } int serialDrain(int64_t handlePtr) @@ -674,7 +662,7 @@ int serialDrain(int64_t handlePtr) auto* handle = reinterpret_cast(handlePtr); if (handle == nullptr) { - invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR)); + invokeError(std::to_underlying(StatusCodes::INVALID_HANDLE_ERROR), "serialDrain: Invalid handle"); return 0; } return (tcdrain(handle->file_descriptor) == 0) ? 1 : 0; diff --git a/src/serial.h b/src/serial.h index fc6285d..04f1ca5 100644 --- a/src/serial.h +++ b/src/serial.h @@ -1,4 +1,6 @@ #pragma once +#include "version_config.h" + #include #define MODULE_API __attribute__((visibility("default"))) @@ -8,10 +10,7 @@ extern "C" { #endif - // Version helpers generated at configure time - MODULE_API unsigned int getMajorVersion(); - MODULE_API unsigned int getMinorVersion(); - MODULE_API unsigned int getPatchVersion(); + MODULE_API void getVersion(Version* out); // Basic serial API MODULE_API intptr_t @@ -33,11 +32,11 @@ extern "C" MODULE_API void serialAbortWrite(int64_t handle); // Optional callback hooks (can be nullptr) - extern void (*error_callback)(int errorCode); - extern void (*read_callback)(int bytes); - extern void (*write_callback)(int bytes); + extern void (*on_read_callback)(int bytes); + extern void (*on_write_callback)(int bytes); + extern void (*on_error_callback)(int errorCode, const char* message); - MODULE_API void serialOnError(void (*func)(int code)); + MODULE_API void serialOnError(void (*func)(int code, const char* message)); MODULE_API void serialOnRead(void (*func)(int bytes)); MODULE_API void serialOnWrite(void (*func)(int bytes)); @@ -45,20 +44,21 @@ extern "C" MODULE_API int serialWriteLine(int64_t handle, const void* buffer, int bufferSize, int timeout /*ms*/); - MODULE_API int serialReadUntilToken(int64_t handle, void* buffer, int bufferSize, int timeout /*ms*/, void* token); + MODULE_API int serialReadUntilSequence(int64_t handle, void* buffer, int bufferSize, int timeout /*ms*/, void* sequence); MODULE_API int serialReadFrame(int64_t handle, void* buffer, int bufferSize, int timeout /*ms*/, char startByte, char endByte); // Byte statistics - MODULE_API int64_t serialGetRxBytes(int64_t handle); - MODULE_API int64_t serialGetTxBytes(int64_t handle); - - // Peek next byte without consuming - MODULE_API int serialPeek(int64_t handle, void* outByte, int timeout /*ms*/); + MODULE_API int64_t serialOutBytesTotal(int64_t handle); + MODULE_API int64_t serialInBytesTotal(int64_t handle); // Drain pending TX bytes (wait until sent) MODULE_API int serialDrain(int64_t handle); + // Bytes currently queued in the driver buffers + MODULE_API int serialInBytesWaiting(int64_t handle); + MODULE_API int serialOutBytesWaiting(int64_t handle); + #ifdef __cplusplus } #endif diff --git a/src/version_config.cpp b/src/version_config.cpp new file mode 100644 index 0000000..7bb2b9e --- /dev/null +++ b/src/version_config.cpp @@ -0,0 +1,22 @@ +#include "version_config.h" + +#include "serial.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + static const Version version{}; + + void getVersion(Version* out) + { + if (out != nullptr) + { + *out = version; + } + } + +#ifdef __cplusplus +} +#endif diff --git a/tests/serial_unit_tests.cpp b/tests/serial_unit_tests.cpp index 6f87f4b..db4867e 100644 --- a/tests/serial_unit_tests.cpp +++ b/tests/serial_unit_tests.cpp @@ -19,7 +19,7 @@ namespace // Helper storage for callback tests std::atomic* g_err_ptr = nullptr; -void errorCallback(int code) +void errorCallback(int code, const char* /*message*/) { if (g_err_ptr != nullptr) { @@ -183,28 +183,12 @@ TEST(SerialHelpers, ReadUntilToken) std::array read_buffer{}; constexpr std::string_view ok_token{"OK"}; - int num_read = serialReadUntilToken(dev.handle, read_buffer.data(), static_cast(read_buffer.size()), 2000, (void*)ok_token.data()); + int num_read = + serialReadUntilSequence(dev.handle, read_buffer.data(), static_cast(read_buffer.size()), 2000, (void*)ok_token.data()); ASSERT_EQ(num_read, static_cast(payload.size())); ASSERT_EQ(std::string_view(read_buffer.data(), num_read), payload); } -TEST(SerialHelpers, Peek) -{ - SerialDevice dev; - const std::string payload = "XYZ"; - dev.writeToDevice(payload); - - char first_byte = 0; - int peek_result = serialPeek(dev.handle, &first_byte, 2000); - ASSERT_EQ(peek_result, 1); - ASSERT_EQ(first_byte, 'X'); - - std::array read_buffer{}; - int num_read = serialRead(dev.handle, read_buffer.data(), 3, 2000, 1); - ASSERT_EQ(num_read, 3); - ASSERT_EQ(std::string_view(read_buffer.data(), 3), payload); -} - TEST(SerialHelpers, Statistics) { SerialDevice dev; @@ -221,8 +205,8 @@ TEST(SerialHelpers, Statistics) int bytes_read = serialRead(dev.handle, read_buffer.data(), static_cast(payload.size()), 2000, 1); ASSERT_EQ(bytes_read, static_cast(payload.size())); - ASSERT_EQ(serialGetTxBytes(dev.handle), static_cast(payload.size())); - ASSERT_EQ(serialGetRxBytes(dev.handle), static_cast(payload.size())); + ASSERT_EQ(serialOutBytesTotal(dev.handle), static_cast(payload.size())); + ASSERT_EQ(serialInBytesTotal(dev.handle), static_cast(payload.size())); } TEST(SerialHelpers, Drain) diff --git a/versioning/version_config.cpp.in b/versioning/version_config.cpp.in deleted file mode 100644 index 8d095d8..0000000 --- a/versioning/version_config.cpp.in +++ /dev/null @@ -1,14 +0,0 @@ -unsigned int getMajorVersion() -{ - return @VERSION_MAJOR@; -} - -unsigned int getMinorVersion() -{ - return @VERSION_MINOR@; -} - -unsigned int getPatchVersion() -{ - return @VERSION_PATCH@; -} \ No newline at end of file diff --git a/versioning/version_config.h.in b/versioning/version_config.h.in new file mode 100644 index 0000000..3b4cace --- /dev/null +++ b/versioning/version_config.h.in @@ -0,0 +1,11 @@ +#pragma once + +extern "C" +{ + struct Version + { + unsigned int major{@VERSION_MAJOR@}; + unsigned int minor{@VERSION_MINOR@}; + unsigned int patch{@VERSION_PATCH@}; + }; +} \ No newline at end of file From 93d603b5e7aef9379c6b9918e1bd3ca1ee493ef9 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 5 Jul 2025 23:31:48 +0000 Subject: [PATCH 05/18] enhance serial API to provide detailed port information via callback, updating example and tests accordingly --- examples/serial_echo.ts | 94 +++++++++++++++++++++++----- src/serial.cpp | 121 +++++++++++++++++++++++++++++------- src/serial.h | 9 ++- tests/serial_unit_tests.cpp | 75 +++++++++------------- 4 files changed, 214 insertions(+), 85 deletions(-) diff --git a/examples/serial_echo.ts b/examples/serial_echo.ts index d9b8905..e58edca 100644 --- a/examples/serial_echo.ts +++ b/examples/serial_echo.ts @@ -56,43 +56,100 @@ const dylib = Deno.dlopen( parameters: ["pointer", "pointer", "i32", "i32", "i32", "pointer"], result: "i32", }, - serialGetPortsInfo: { parameters: ["pointer", "i32", "pointer"], result: "i32" }, + serialGetPortsInfo: { parameters: ["function"], result: "i32" }, } as const, ); // ----------------------------------------------------------------------------- -// 1. List available ports +// 1. List available ports with extended info // ----------------------------------------------------------------------------- -const sepBuf = cString(";"); -const portsBuf = new Uint8Array(4096); -dylib.symbols.serialGetPortsInfo( - pointer(portsBuf), - portsBuf.length, - pointer(sepBuf), -); -const cPortsStr = decoder.decode(portsBuf.subarray(0, portsBuf.indexOf(0))); -const ports = cPortsStr ? cPortsStr.split(";") : []; -console.log("Available ports:"); -for (const p of ports) { - console.log(" •", p); +// Storage for callback results +interface PortInfo { + port: string; + path: string; + manufacturer: string; + serial: string; + pnpId: string; + locationId: string; + productId: string; + vendorId: string; +} + +const portsInfo: PortInfo[] = []; + +// Helper to read C string from pointer +function cstr(ptr: Deno.PointerValue): string { + if (ptr === 0n) return ""; + return Deno.UnsafePointerView.getCString(ptr); } -if (ports.length === 0) { - console.error("No serial ports found (ttyUSB). Exiting."); + +const portInfoCallback = new Deno.UnsafeCallback({ + parameters: [ + "pointer", // port + "pointer", // path + "pointer", // manufacturer + "pointer", // serialNumber + "pointer", // pnpId + "pointer", // locationId + "pointer", // productId + "pointer", // vendorId + ], + result: "void", +} as const, ( + portPtr, + pathPtr, + manufacturerPtr, + serialPtr, + pnpPtr, + locationPtr, + productPtr, + vendorPtr, +) => { + portsInfo.push({ + port: cstr(portPtr), + path: cstr(pathPtr), + manufacturer: cstr(manufacturerPtr), + serial: cstr(serialPtr), + pnpId: cstr(pnpPtr), + locationId: cstr(locationPtr), + productId: cstr(productPtr), + vendorId: cstr(vendorPtr), + }); +}); + +const numPorts = dylib.symbols.serialGetPortsInfo(portInfoCallback.pointer); + +if (numPorts === 0) { + console.error("No serial ports found. Exiting."); + portInfoCallback.close(); dylib.close(); Deno.exit(1); } +console.log("Available ports:"); +for (const p of portsInfo) { + console.log(` • ${p.port}`); + console.log(` alias : ${p.path}`); + if (p.manufacturer) console.log(` manufacturer : ${p.manufacturer}`); + if (p.serial) console.log(` serial : ${p.serial}`); + if (p.vendorId) console.log(` vendorId : ${p.vendorId}`); + if (p.productId) console.log(` productId : ${p.productId}`); + if (p.pnpId) console.log(` pnpId : ${p.pnpId}`); + if (p.locationId) console.log(` locationId : ${p.locationId}`); +} + // ----------------------------------------------------------------------------- // 2. Echo test on selected port // ----------------------------------------------------------------------------- -const portPath = cliPort ?? ports[0]; +const portPath = cliPort ?? portsInfo[0].port; console.log(`\nUsing port: ${portPath}`); const portBuf = cString(portPath); const handle = dylib.symbols.serialOpen(pointer(portBuf), 115200, 8, 0, 0); if (handle === null) { console.error("Failed to open port!"); + portInfoCallback.close(); dylib.close(); Deno.exit(1); } @@ -106,6 +163,7 @@ const written = dylib.symbols.serialWrite(handle, pointer(msgBuf), msgBuf.length if (written !== msgBuf.length) { console.error(`Write failed (wrote ${written}/${msgBuf.length})`); dylib.symbols.serialClose(handle); + portInfoCallback.close(); dylib.close(); Deno.exit(1); } @@ -117,6 +175,7 @@ const read = dylib.symbols.serialReadUntil(handle, pointer(readBuf), readBuf.len if (read <= 0) { console.error("Read failed or timed out."); dylib.symbols.serialClose(handle); + portInfoCallback.close(); dylib.close(); Deno.exit(1); } @@ -131,4 +190,5 @@ if (echo === msg) { } dylib.symbols.serialClose(handle); +portInfoCallback.close(); dylib.close(); diff --git a/src/serial.cpp b/src/serial.cpp index 08c722d..4b716b2 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -361,11 +362,15 @@ int serialReadUntil(int64_t handlePtr, void* buffer, int bufferSize, int timeout return total; } -int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) +int serialGetPortsInfo(void (*callback)(const char* port, + const char* path, + const char* manufacturer, + const char* serialNumber, + const char* pnpId, + const char* locationId, + const char* productId, + const char* vendorId)) { - auto sep = std::string_view{static_cast(separatorPtr)}; - std::string result; - namespace fs = std::filesystem; const fs::path by_id_dir{"/dev/serial/by-id"}; @@ -375,6 +380,20 @@ int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) return 0; } + int count = 0; + + auto read_attr = [](const fs::path& p, const std::string& attr) -> std::string + { + std::ifstream file(p / attr); + if (!file.is_open()) + { + return ""; + } + std::string value; + std::getline(file, value); + return value; + }; + try { for (const auto& entry : fs::directory_iterator{by_id_dir}) @@ -384,15 +403,84 @@ int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) continue; } - std::error_code error_code; - fs::path canonical = fs::canonical(entry.path(), error_code); - if (error_code) + std::error_code ec; + fs::path canonical = fs::canonical(entry.path(), ec); + if (ec) { continue; // skip entries we cannot resolve } - result += canonical.string(); - result += sep; + // Canonical port (e.g., /dev/ttyUSB0) + const std::string port_path = canonical.string(); + const std::string symlink_path = entry.path().string(); + + // Attempt to resolve sysfs USB device directory + // /sys/class/tty/ttyUSB0/device -> ../../..//... + fs::path tty_sys = fs::path{"/sys/class/tty"} / canonical.filename() / "device"; + tty_sys = fs::canonical(tty_sys, ec); + if (ec) + { + // Not fatal – still report port with empty attributes + tty_sys.clear(); + } + + fs::path usb_device_dir; + if (!tty_sys.empty()) + { + // Climb up until we find idVendor attribute + fs::path cur = tty_sys; + while (cur.has_relative_path() && cur != cur.root_path()) + { + if (fs::exists(cur / "idVendor")) + { + usb_device_dir = cur; + break; + } + cur = cur.parent_path(); + } + } + + std::string manufacturer; + std::string serial_number; + std::string vendor_id; + std::string product_id; + std::string pnp_id; + std::string location_id; + + if (!usb_device_dir.empty()) + { + manufacturer = read_attr(usb_device_dir, "manufacturer"); + serial_number = read_attr(usb_device_dir, "serial"); + vendor_id = read_attr(usb_device_dir, "idVendor"); + product_id = read_attr(usb_device_dir, "idProduct"); + + if (!vendor_id.empty() && !product_id.empty()) + { + pnp_id = "USB\\VID_" + vendor_id + "&PID_" + product_id; + } + + // locationId: busnum-portpath (best effort) + std::string busnum = read_attr(usb_device_dir, "busnum"); + std::string devpath = read_attr(usb_device_dir, "devpath"); + if (!busnum.empty() && !devpath.empty()) + { + location_id = busnum + ":" + devpath; + } + } + + if (callback != nullptr) + { + callback(port_path.c_str(), + symlink_path.c_str(), + manufacturer.c_str(), + serial_number.c_str(), + pnp_id.c_str(), + location_id.c_str(), + product_id.c_str(), + vendor_id.c_str()); + } + + ++count; } } catch (const fs::filesystem_error&) @@ -401,20 +489,7 @@ int serialGetPortsInfo(void* buffer, int bufferSize, void* separatorPtr) return 0; } - if (!result.empty()) - { - // Remove the trailing separator - result.erase(result.size() - sep.size()); - } - - if (static_cast(result.size()) + 1 > bufferSize) - { - invokeError(std::to_underlying(StatusCodes::BUFFER_ERROR), "serialGetPortsInfo: Buffer too small"); - return 0; - } - - std::memcpy(buffer, result.c_str(), result.size() + 1); - return result.empty() ? 0 : 1; // number of ports not easily counted here + return count; // number of ports discovered } // ----------------------------------------------------------------------------- diff --git a/src/serial.h b/src/serial.h index 04f1ca5..c5c4275 100644 --- a/src/serial.h +++ b/src/serial.h @@ -24,7 +24,14 @@ extern "C" MODULE_API int serialWrite(int64_t handle, const void* buffer, int bufferSize, int timeout, int multiplier); - MODULE_API int serialGetPortsInfo(void* buffer, int bufferSize, void* separator); + MODULE_API int serialGetPortsInfo(void (*function)(const char* port, + const char* path, + const char* manufacturer, + const char* serialNumber, + const char* pnpId, + const char* locationId, + const char* productId, + const char* vendorId)); MODULE_API void serialClearBufferIn(int64_t handle); MODULE_API void serialClearBufferOut(int64_t handle); diff --git a/tests/serial_unit_tests.cpp b/tests/serial_unit_tests.cpp index db4867e..dfc87d9 100644 --- a/tests/serial_unit_tests.cpp +++ b/tests/serial_unit_tests.cpp @@ -83,36 +83,31 @@ TEST(SerialOpenTest, InvalidPathInvokesErrorCallback) } // ------------------------ serialGetPortsInfo checks ------------------------ -TEST(SerialGetPortsInfoTest, BufferTooSmallTriggersError) +TEST(SerialGetPortsInfoTest, CallbackReceivesPortInfo) { - constexpr std::string_view separator{";"}; - std::array info_buffer{}; - std::atomic err_code{0}; + // Counter for how many times the callback is invoked + static int callback_count = 0; + callback_count = 0; + + auto callbackFunc = [](const char* /*port*/, + const char* /*path*/, + const char* /*manufacturer*/, + const char* /*serialNumber*/, + const char* /*pnpId*/, + const char* /*locationId*/, + const char* /*productId*/, + const char* /*vendorId*/) { ++callback_count; }; + std::atomic err_code{0}; g_err_ptr = &err_code; serialOnError(errorCallback); - int result = serialGetPortsInfo(info_buffer.data(), static_cast(info_buffer.size()), (void*)separator.data()); - EXPECT_EQ(result, 0); // function indicates failure via 0 - EXPECT_EQ(err_code.load(), static_cast(StatusCodes::BUFFER_ERROR)); - - serialOnError(nullptr); -} - -TEST(SerialGetPortsInfoTest, LargeBufferReturnsZeroOrOne) -{ - constexpr std::string_view separator{";"}; - std::array info_buffer{}; + int result = serialGetPortsInfo(callbackFunc); - std::atomic err_code{0}; - g_err_ptr = &err_code; - serialOnError(errorCallback); + // result should match the number of times our callback ran + EXPECT_EQ(result, callback_count); - int result = serialGetPortsInfo(info_buffer.data(), static_cast(info_buffer.size()), (void*)separator.data()); - EXPECT_GE(result, 0); - // res is 0 (no ports) or 1 (ports found) - EXPECT_LE(result, 1); - // Acceptable error codes: none or NOT_FOUND_ERROR (e.g., dir missing) + // Acceptable error codes: none or NOT_FOUND_ERROR (e.g., dir missing on CI) if (err_code != 0) { EXPECT_EQ(err_code.load(), static_cast(StatusCodes::NOT_FOUND_ERROR)); @@ -124,32 +119,24 @@ TEST(SerialGetPortsInfoTest, LargeBufferReturnsZeroOrOne) // ---------------------------- Port listing helper --------------------------- TEST(SerialGetPortsInfoTest, PrintAvailablePorts) { - constexpr std::string_view separator{";"}; - std::array info_buffer{}; - - int result = serialGetPortsInfo(info_buffer.data(), static_cast(info_buffer.size()), (void*)separator.data()); + auto print_callback = [](const char* port, + const char* path, + const char* /*manufacturer*/, + const char* /*serialNumber*/, + const char* /*pnpId*/, + const char* /*locationId*/, + const char* /*productId*/, + const char* /*vendorId*/) { std::cout << " " << port << " (alias: " << path << ")\n"; }; + + int result = serialGetPortsInfo(print_callback); EXPECT_GE(result, 0); - - std::string ports_str(info_buffer.data()); - if (!ports_str.empty()) + if (result == 0) { - std::cout << "\nAvailable serial ports (by-id):\n"; - size_t start = 0; - while (true) - { - size_t pos = ports_str.find(separator.data(), start); - std::string token = ports_str.substr(start, pos - start); - std::cout << " " << token << "\n"; - if (pos == std::string::npos) - { - break; - } - start = pos + std::strlen(separator.data()); - } + std::cout << "\nNo serial devices found in /dev/serial/by-id\n"; } else { - std::cout << "\nNo serial devices found in /dev/serial/by-id\n"; + std::cout << "\nAvailable serial ports (by-id):\n"; } } From f2c3d921b9930dc3fca2876f498f532679add074 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 5 Jul 2025 23:37:06 +0000 Subject: [PATCH 06/18] remove unnecessary output for available serial ports in unit test --- tests/serial_unit_tests.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/serial_unit_tests.cpp b/tests/serial_unit_tests.cpp index dfc87d9..e9f4cac 100644 --- a/tests/serial_unit_tests.cpp +++ b/tests/serial_unit_tests.cpp @@ -134,10 +134,6 @@ TEST(SerialGetPortsInfoTest, PrintAvailablePorts) { std::cout << "\nNo serial devices found in /dev/serial/by-id\n"; } - else - { - std::cout << "\nAvailable serial ports (by-id):\n"; - } } // --------------------------- Stubbed no-op APIs ---------------------------- From fc3e54a065b16ab407055f7cfed9d3717c77fb7d Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 5 Jul 2025 23:45:40 +0000 Subject: [PATCH 07/18] implement serialGetPortsInfo in a new file, refactoring the logic for collecting USB device information and updating the callback mechanism; remove redundant implementation from serial.cpp --- src/serial.cpp | 131 ---------------------------- src/serial.h | 2 +- src/serial_ports_info.cpp | 166 ++++++++++++++++++++++++++++++++++++ tests/serial_unit_tests.cpp | 19 ++--- 4 files changed, 176 insertions(+), 142 deletions(-) create mode 100644 src/serial_ports_info.cpp diff --git a/src/serial.cpp b/src/serial.cpp index 4b716b2..b1c0228 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -362,136 +361,6 @@ int serialReadUntil(int64_t handlePtr, void* buffer, int bufferSize, int timeout return total; } -int serialGetPortsInfo(void (*callback)(const char* port, - const char* path, - const char* manufacturer, - const char* serialNumber, - const char* pnpId, - const char* locationId, - const char* productId, - const char* vendorId)) -{ - namespace fs = std::filesystem; - - const fs::path by_id_dir{"/dev/serial/by-id"}; - if (!fs::exists(by_id_dir) || !fs::is_directory(by_id_dir)) - { - invokeError(std::to_underlying(StatusCodes::NOT_FOUND_ERROR), "serialGetPortsInfo: Failed to get ports info"); - return 0; - } - - int count = 0; - - auto read_attr = [](const fs::path& p, const std::string& attr) -> std::string - { - std::ifstream file(p / attr); - if (!file.is_open()) - { - return ""; - } - std::string value; - std::getline(file, value); - return value; - }; - - try - { - for (const auto& entry : fs::directory_iterator{by_id_dir}) - { - if (!entry.is_symlink()) - { - continue; - } - - std::error_code ec; - fs::path canonical = fs::canonical(entry.path(), ec); - if (ec) - { - continue; // skip entries we cannot resolve - } - - // Canonical port (e.g., /dev/ttyUSB0) - const std::string port_path = canonical.string(); - const std::string symlink_path = entry.path().string(); - - // Attempt to resolve sysfs USB device directory - // /sys/class/tty/ttyUSB0/device -> ../../..//... - fs::path tty_sys = fs::path{"/sys/class/tty"} / canonical.filename() / "device"; - tty_sys = fs::canonical(tty_sys, ec); - if (ec) - { - // Not fatal – still report port with empty attributes - tty_sys.clear(); - } - - fs::path usb_device_dir; - if (!tty_sys.empty()) - { - // Climb up until we find idVendor attribute - fs::path cur = tty_sys; - while (cur.has_relative_path() && cur != cur.root_path()) - { - if (fs::exists(cur / "idVendor")) - { - usb_device_dir = cur; - break; - } - cur = cur.parent_path(); - } - } - - std::string manufacturer; - std::string serial_number; - std::string vendor_id; - std::string product_id; - std::string pnp_id; - std::string location_id; - - if (!usb_device_dir.empty()) - { - manufacturer = read_attr(usb_device_dir, "manufacturer"); - serial_number = read_attr(usb_device_dir, "serial"); - vendor_id = read_attr(usb_device_dir, "idVendor"); - product_id = read_attr(usb_device_dir, "idProduct"); - - if (!vendor_id.empty() && !product_id.empty()) - { - pnp_id = "USB\\VID_" + vendor_id + "&PID_" + product_id; - } - - // locationId: busnum-portpath (best effort) - std::string busnum = read_attr(usb_device_dir, "busnum"); - std::string devpath = read_attr(usb_device_dir, "devpath"); - if (!busnum.empty() && !devpath.empty()) - { - location_id = busnum + ":" + devpath; - } - } - - if (callback != nullptr) - { - callback(port_path.c_str(), - symlink_path.c_str(), - manufacturer.c_str(), - serial_number.c_str(), - pnp_id.c_str(), - location_id.c_str(), - product_id.c_str(), - vendor_id.c_str()); - } - - ++count; - } - } - catch (const fs::filesystem_error&) - { - invokeError(std::to_underlying(StatusCodes::NOT_FOUND_ERROR), "serialGetPortsInfo: Failed to get ports info"); - return 0; - } - - return count; // number of ports discovered -} - // ----------------------------------------------------------------------------- // Buffer & abort helpers implementations // ----------------------------------------------------------------------------- diff --git a/src/serial.h b/src/serial.h index c5c4275..4e9c04e 100644 --- a/src/serial.h +++ b/src/serial.h @@ -24,7 +24,7 @@ extern "C" MODULE_API int serialWrite(int64_t handle, const void* buffer, int bufferSize, int timeout, int multiplier); - MODULE_API int serialGetPortsInfo(void (*function)(const char* port, + MODULE_API int serialGetPortsInfo(void (*callback)(const char* port, const char* path, const char* manufacturer, const char* serialNumber, diff --git a/src/serial_ports_info.cpp b/src/serial_ports_info.cpp new file mode 100644 index 0000000..923ded2 --- /dev/null +++ b/src/serial_ports_info.cpp @@ -0,0 +1,166 @@ +#include "serial.h" +#include "status_codes.h" + +#include +#include +#include +#include + +namespace +{ +namespace fs = std::filesystem; + +inline void invokeError(int code, const char* message) +{ + if (on_error_callback != nullptr) + { + on_error_callback(code, message); + } +} + +// Reads a single attribute file from a given directory. Returns empty string on error. +std::string readAttr(const fs::path& dir, const std::string& attr) +{ + std::ifstream file(dir / attr); + if (!file.is_open()) + { + return ""; + } + std::string value; + std::getline(file, value); + return value; +} + +struct UsbInfo +{ + std::string manufacturer; + std::string serial_number; + std::string vendor_id; + std::string product_id; + std::string pnp_id; + std::string location_id; +}; + +// Attempts to locate the USB device directory for a tty canonical path and collect attributes. +UsbInfo collectUsbInfo(const fs::path& canonicalPath) +{ + UsbInfo info{}; + + std::error_code error_code; + fs::path tty_sys = fs::path{"/sys/class/tty"} / canonicalPath.filename() / "device"; + tty_sys = fs::canonical(tty_sys, error_code); + if (error_code) + { + return info; // return empty info if we cannot resolve + } + + fs::path usb_dir; + fs::path cur = tty_sys; + while (cur.has_relative_path() && cur != cur.root_path()) + { + if (fs::exists(cur / "idVendor")) + { + usb_dir = cur; + break; + } + cur = cur.parent_path(); + } + + if (usb_dir.empty()) + { + return info; // not a USB device (e.g., built-in tty) + } + + info.manufacturer = readAttr(usb_dir, "manufacturer"); + info.serial_number = readAttr(usb_dir, "serial"); + info.vendor_id = readAttr(usb_dir, "idVendor"); + info.product_id = readAttr(usb_dir, "idProduct"); + + if (!info.vendor_id.empty() && !info.product_id.empty()) + { + info.pnp_id = "USB\\VID_" + info.vendor_id + "&PID_" + info.product_id; + } + + const std::string busnum = readAttr(usb_dir, "busnum"); + const std::string devpath = readAttr(usb_dir, "devpath"); + if (!busnum.empty() && !devpath.empty()) + { + info.location_id = busnum + ":" + devpath; + } + + return info; +} + +// Handles a single directory entry. Returns true if a port was reported. +bool handleEntry(const fs::directory_entry& entry, + void (*callback)(const char*, const char*, const char*, const char*, const char*, const char*, const char*, const char*)) +{ + if (!entry.is_symlink()) + { + return false; + } + + std::error_code error_code; + fs::path canonical = fs::canonical(entry.path(), error_code); + if (error_code) + { + return false; + } + + const std::string port_path = canonical.string(); + const std::string symlink_path = entry.path().string(); + + UsbInfo info = collectUsbInfo(canonical); + + if (callback != nullptr) + { + callback(port_path.c_str(), + symlink_path.c_str(), + info.manufacturer.c_str(), + info.serial_number.c_str(), + info.pnp_id.c_str(), + info.location_id.c_str(), + info.product_id.c_str(), + info.vendor_id.c_str()); + } + return true; +} + +} // namespace + +int serialGetPortsInfo(void (*callback)(const char* port, + const char* path, + const char* manufacturer, + const char* serialNumber, + const char* pnpId, + const char* locationId, + const char* productId, + const char* vendorId)) +{ + const fs::path by_id_dir{"/dev/serial/by-id"}; + if (!fs::exists(by_id_dir) || !fs::is_directory(by_id_dir)) + { + invokeError(std::to_underlying(StatusCodes::NOT_FOUND_ERROR), "serialGetPortsInfo: Failed to get ports info"); + return 0; + } + + int count = 0; + + try + { + for (const auto& entry : fs::directory_iterator{by_id_dir}) + { + if (handleEntry(entry, callback)) + { + ++count; + } + } + } + catch (const fs::filesystem_error&) + { + invokeError(std::to_underlying(StatusCodes::NOT_FOUND_ERROR), "serialGetPortsInfo: Failed to get ports info"); + return 0; + } + + return count; +} diff --git a/tests/serial_unit_tests.cpp b/tests/serial_unit_tests.cpp index e9f4cac..9096677 100644 --- a/tests/serial_unit_tests.cpp +++ b/tests/serial_unit_tests.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -89,20 +88,20 @@ TEST(SerialGetPortsInfoTest, CallbackReceivesPortInfo) static int callback_count = 0; callback_count = 0; - auto callbackFunc = [](const char* /*port*/, - const char* /*path*/, - const char* /*manufacturer*/, - const char* /*serialNumber*/, - const char* /*pnpId*/, - const char* /*locationId*/, - const char* /*productId*/, - const char* /*vendorId*/) { ++callback_count; }; + auto callback_func = [](const char* /*port*/, + const char* /*path*/, + const char* /*manufacturer*/, + const char* /*serialNumber*/, + const char* /*pnpId*/, + const char* /*locationId*/, + const char* /*productId*/, + const char* /*vendorId*/) { ++callback_count; }; std::atomic err_code{0}; g_err_ptr = &err_code; serialOnError(errorCallback); - int result = serialGetPortsInfo(callbackFunc); + int result = serialGetPortsInfo(callback_func); // result should match the number of times our callback ran EXPECT_EQ(result, callback_count); From a779b3bdf14d420244f187b503d819434284bee1 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 5 Jul 2025 23:46:57 +0000 Subject: [PATCH 08/18] update version to 1.0.0 in CMakeLists.txt and remove unused includes from serial.cpp --- CMakeLists.txt | 4 ++-- src/serial.cpp | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2923d9..2e34811 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.30) -set(VERSION_MAJOR 0) -set(VERSION_MINOR 2) +set(VERSION_MAJOR 1) +set(VERSION_MINOR 0) set(VERSION_PATCH 0) set(PROJECT_N cpp_unix_bindings) diff --git a/src/serial.cpp b/src/serial.cpp index b1c0228..1c9feaf 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -5,8 +5,6 @@ #include #include #include -#include -#include #include #include #include From dd728f48a25649bb5a9fe4e41c88bc8e61ae7e81 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Sat, 5 Jul 2025 23:58:39 +0000 Subject: [PATCH 09/18] Add comprehensive documentation for C API, building instructions, Deno examples, and usage guidelines --- docs/api_reference.md | 68 +++++++++++++++ docs/building.md | 55 +++++++++++++ docs/examples.md | 25 ++++++ docs/ffi_deno.md | 107 ++++++++++++++++++++++++ docs/overview.md | 23 ++++++ examples/serial_callbacks.ts | 155 +++++++++++++++++++++++++++++++++++ 6 files changed, 433 insertions(+) create mode 100644 docs/api_reference.md create mode 100644 docs/building.md create mode 100644 docs/examples.md create mode 100644 docs/ffi_deno.md create mode 100644 docs/overview.md create mode 100644 examples/serial_callbacks.ts diff --git a/docs/api_reference.md b/docs/api_reference.md new file mode 100644 index 0000000..c6e676b --- /dev/null +++ b/docs/api_reference.md @@ -0,0 +1,68 @@ +# C API Reference + +All functions are declared in `src/serial.h`. **Return values ≤ 0** indicate an error; the numeric code maps to `StatusCodes` (see bottom). + +--- + +## Connection + +| Function | Description | +|----------|-------------| +| `intptr_t serialOpen(void* port, int baud, int dataBits, int parity, int stopBits)` | Open a device (e.g. `"/dev/ttyUSB0\0"`) and receive an opaque handle. Parity: `0` = none, `1` = even, `2` = odd. Stop bits: `0` = 1 bit, `2` = 2 bits. | +| `void serialClose(int64_t handle)` | Restore original tty settings and close the file descriptor. | + +## Basic I/O + +| Function | Description | +|----------|-------------| +| `int serialRead(handle, buf, len, timeoutMs, multiplier)` | Read up to *len* bytes. Returns actual bytes read or `0` on timeout. | +| `int serialWrite(handle, buf, len, timeoutMs, multiplier)` | Write *len* bytes and return the number of bytes written. | +| `int serialReadUntil(handle, buf, len, timeoutMs, multiplier, untilCharPtr)` | Read until the delimiter byte (inclusive). | + +## Extended helpers + +| Function | Description | +|----------|-------------| +| `int serialReadLine(handle, buf, len, timeoutMs)` | Read until `\n`. | +| `int serialWriteLine(handle, buf, len, timeoutMs)` | Write buffer followed by `\n`. | +| `int serialReadUntilSequence(handle, buf, len, timeoutMs, seqPtr)` | Read until the UTF-8 sequence is encountered. | +| `int serialReadFrame(handle, buf, len, timeoutMs, startByte, endByte)` | Read a frame delimited by *startByte* / *endByte*. | + +## Statistics & Buffer control + +| Function | Description | +|----------|-------------| +| `int64_t serialInBytesTotal(handle)` / `serialOutBytesTotal(handle)` | Cumulative RX / TX counters. | +| `int serialInBytesWaiting(handle)` / `serialOutBytesWaiting(handle)` | Bytes currently queued in driver buffers. | +| `int serialDrain(handle)` | Block until OS transmit buffer is empty. | +| `void serialClearBufferIn/Out(handle)` | Flush RX / TX buffers. | +| `void serialAbortRead/Write(handle)` | Cancel in-flight I/O from another thread. | + +## Enumeration & Callbacks + +| Function | Description | +|----------|-------------| +| `int serialGetPortsInfo(cb)` | Call *cb* for every entry under `/dev/serial/by-id`. | +| `void serialOnError(cb)` | Register global error callback. | +| `void serialOnRead(cb)` / `serialOnWrite(cb)` | Called after each successful read/write. | + +--- + +## StatusCodes + +| Constant | Value | Meaning | +|----------|------:|---------| +| `SUCCESS` | 0 | No error | +| `CLOSE_HANDLE_ERROR` | -1 | Closing the device failed | +| `INVALID_HANDLE_ERROR` | -2 | Handle was null / invalid | +| `READ_ERROR` | -3 | `read()` syscall failed | +| `WRITE_ERROR` | -4 | `write()` syscall failed | +| `GET_STATE_ERROR` | -5 | Could not query tty state | +| `SET_STATE_ERROR` | -6 | Could not set tty state | +| `SET_TIMEOUT_ERROR` | -7 | Could not configure timeout | +| `BUFFER_ERROR` | -8 | Internal buffer issue | +| `NOT_FOUND_ERROR` | -9 | Resource not found (`/dev/serial/by-id`) | +| `CLEAR_BUFFER_IN_ERROR` | -10 | Flushing RX failed | +| `CLEAR_BUFFER_OUT_ERROR` | -11 | Flushing TX failed | +| `ABORT_READ_ERROR` | -12 | Abort flag could not be set | +| `ABORT_WRITE_ERROR` | -13 | Abort flag could not be set | diff --git a/docs/building.md b/docs/building.md new file mode 100644 index 0000000..fc447a9 --- /dev/null +++ b/docs/building.md @@ -0,0 +1,55 @@ +# Building the shared library + +## 1. Prerequisites + +* **Compiler:** `g++` or `clang++` with C++23 support +* **CMake ≥ 3.30** +* POSIX development headers (`termios.h`, `fcntl.h`, …) + +Example install on Debian/Ubuntu: + +```bash +sudo apt-get update +sudo apt-get install build-essential cmake clang make +``` + +--- + +## 2. Configure & Compile + +```bash +# From the repository root +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j$(nproc) +``` + +This produces: + +* `build/libcpp_unix_bindings.so` – the shared library +* `build/tests` – aggregated gtest binary (run via `ctest` or `./tests`) +* `compile_commands.json` – copied to project root for clang-tools + +### Debug build + +```bash +cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug +cmake --build build/debug -j +``` + +### Installing system-wide + +```bash +sudo cmake --install build +``` + +Set `CMAKE_INSTALL_PREFIX` to control the destination directory. + +--- + +## 3. Troubleshooting + +| Symptom | Possible Fix | +|---------|--------------| +| *Linker cannot find `stdc++fs`* | Ensure GCC ≥ 8 or Clang ≥ 10 | +| *Undefined reference to `cfsetspeed`* | Make sure `-pthread` is not stripped | +| *`EACCES` when opening `/dev/ttyUSB0`* | Add your user to the `dialout` (Ubuntu) or `uucp` (Arch) group or run via `sudo` | diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..6beccb7 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,25 @@ +# Deno example scripts + +| Script | Purpose | +|--------|---------| +| `serial_echo.ts` | Minimal echo test: enumerate ports, open the first one and verify that each line sent is echoed back by the MCU. | +| `serial_advanced.ts` | Demonstrates helpers (`serialReadLine`, statistics, peek, `serialDrain`) and prints TX/RX counters. | +| `serial_callbacks.ts` | Shows how to register error / read / write callbacks and prints diagnostics for every transfer. | + +--- + +## Running an example + +```bash +deno run --allow-ffi --allow-read examples/