From d72278937f0f4f43336eeab67139f64f2d75b6f6 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sat, 3 Jan 2026 19:20:50 +0100 Subject: [PATCH 01/44] fix: Add missing include --- libs/util/inc/util/Socket.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/util/inc/util/Socket.h b/libs/util/inc/util/Socket.h index 5a63d5e..a75e5ed 100644 --- a/libs/util/inc/util/Socket.h +++ b/libs/util/inc/util/Socket.h @@ -3,6 +3,7 @@ #include #include +#include namespace doip { From b17bf3b2e6d68bd89870fe06b08232c8c2596c79 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sat, 3 Jan 2026 19:23:19 +0100 Subject: [PATCH 02/44] feat: Begin client renovation, add minimal example --- examples/CMakeLists.txt | 2 + examples/minimal/CMakeLists.txt | 25 +++ examples/minimal/MinimalDoIPClient.cpp | 27 +++ examples/minimal/MinimalDoIPServer.cpp | 26 +++ inc/DoIPClient.h | 59 +++--- inc/DoIPClientModel.h | 37 ++++ inc/DoIPMessage.h | 3 + src/DoIPClient.cpp | 250 ++++++++++++++----------- 8 files changed, 302 insertions(+), 127 deletions(-) create mode 100644 examples/minimal/CMakeLists.txt create mode 100644 examples/minimal/MinimalDoIPClient.cpp create mode 100644 examples/minimal/MinimalDoIPServer.cpp create mode 100644 inc/DoIPClientModel.h diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 379a223..7c48e7e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,3 +3,5 @@ if (LINUX) add_subdirectory(socket-can) endif() +add_subdirectory(minimal) + diff --git a/examples/minimal/CMakeLists.txt b/examples/minimal/CMakeLists.txt new file mode 100644 index 0000000..1be848c --- /dev/null +++ b/examples/minimal/CMakeLists.txt @@ -0,0 +1,25 @@ +# Examples CMakeLists.txt + +set (DEMO_SOURCES + MinimalDoIPServer.cpp + MinimalDoIPClient.cpp +) + +foreach(demo_source ${DEMO_SOURCES}) + get_filename_component(demo_name ${demo_source} NAME_WE) + add_executable(${demo_name} ${demo_source}) + target_link_libraries(${demo_name} + PRIVATE + ${DOIP_SERVER_LIB_NAME} + ) + + # Set properties for the example + set_target_properties(${demo_name} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + ) + + # Disable switch-default warning for examples using spdlog + target_compile_options(${demo_name} PRIVATE -Wno-switch-default) +endforeach() diff --git a/examples/minimal/MinimalDoIPClient.cpp b/examples/minimal/MinimalDoIPClient.cpp new file mode 100644 index 0000000..60dda4f --- /dev/null +++ b/examples/minimal/MinimalDoIPClient.cpp @@ -0,0 +1,27 @@ +#include + +#include "DoIPClient.h" + +using namespace doip; + +struct MyDoIPClientModel : public DoIPClientModel { + void messageReceived(DoIPClient& client, const DoIPMessage& msg) override { + (void)client; + std::cout << "Message received: " << msg << std::endl; + } +}; + + +int main() { + DoIPClient client(std::make_unique()); + if (!client.startTcpConnection()) { + std::cerr << "Failed to start TCP connection to DoIP server" << std::endl; + return 1; + } + + while(client.isTcpConnected()) { + // Main loop can perform other tasks or just sleep + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + +} \ No newline at end of file diff --git a/examples/minimal/MinimalDoIPServer.cpp b/examples/minimal/MinimalDoIPServer.cpp new file mode 100644 index 0000000..9babf91 --- /dev/null +++ b/examples/minimal/MinimalDoIPServer.cpp @@ -0,0 +1,26 @@ +#include "DoIPServer.h" + +using namespace doip; + +int main(int argc, char *argv[]) { + (void)argc; + (void)argv; + + ServerConfig cfg; + auto console = spdlog::stdout_color_mt("doip-server"); + + console->info("Starting DoIP Minimal Server"); + + auto server = std::make_unique(cfg); + if (!server->setupTcpSocket()) { + console->error("Failed to start DoIP Discovery Server"); + return 1; + } + + console->info("DoIP Minimal Server is running"); + + while (server->isRunning()) { + // Main loop can perform other tasks or just sleep + std::this_thread::sleep_for(std::chrono::seconds(1)); + } +} \ No newline at end of file diff --git a/inc/DoIPClient.h b/inc/DoIPClient.h index 1697099..d1b7271 100644 --- a/inc/DoIPClient.h +++ b/inc/DoIPClient.h @@ -2,6 +2,13 @@ #ifndef DOIPCLIENT_H #define DOIPCLIENT_H +#include "DoIPConfig.h" +#include "DoIPClientModel.h" +#include "DoIPMessage.h" +#include "util/Logger.h" +#include "util/Socket.h" +#include "util/ThreadSafeQueue.h" + #include #include #include @@ -9,34 +16,37 @@ #include #include -#include "DoIPConfig.h" -#include "DoIPMessage.h" -#include "util/Logger.h" - namespace doip { const int _maxDataSize = 64; -using DoIPRequest = std::pair; + class DoIPClient { public: - DoIPClient() {m_receiveBuf.reserve(DOIP_MAXIMUM_MTU);} + DoIPClient(UniqueDoIPClientModelPtr model = std::make_unique()) : m_model(std::move(model)) { m_receiveBuf.reserve(DOIP_MAXIMUM_MTU); } + + [[nodiscard]] bool startTcpConnection(); + + [[nodiscard]] bool isTcpConnected() const noexcept { return m_connected.valid(); } + [[nodiscard]] bool reconnectServer(); + void closeTcpConnection(); + + void sendMessage(const DoIPMessage &msg); - void startTcpConnection(); void startUdpConnection(); void startAnnouncementListener(); + void closeUdpConnection(); + + ssize_t sendRoutingActivationRequest(); - ssize_t sendVehicleIdentificationRequest(const char *inet_address); - void receiveRoutingActivationResponse(); + std::optional receiveRoutingActivationResponse(); + void receiveUdpMessage(); + ssize_t sendVehicleIdentificationRequest(const char *inet_address); [[nodiscard]] bool receiveVehicleAnnouncement(); - /* - * Send the builded request over the tcp-connection to server - */ - void receiveMessage(); /** * Sends a diagnostic message to the server @@ -50,29 +60,34 @@ class DoIPClient { ssize_t sendAliveCheckResponse(); void setSourceAddress(const DoIPAddress &address); void printVehicleInformationResponse(); - void closeTcpConnection(); - void closeUdpConnection(); - void reconnectServer(); - - int getSockFd(); - int getConnected(); private: + UniqueDoIPClientModelPtr m_model; ByteArray m_receiveBuf; - int m_tcpSocket{-1}, m_udpSocket{-1}, m_udpAnnouncementSocket{-1}, m_connected{-1}; + Socket m_tcpSocket, m_udpSocket, m_udpAnnouncementSocket, m_connected; + ThreadSafeQueue m_messageQueue; + std::atomic m_tcpRunning{false}; + std::thread m_tcpThread{}; int m_broadcast = 1; struct sockaddr_in m_serverAddress, m_clientAddress, m_announcementAddress; - DoIPAddress m_sourceAddress = DoIPAddress(0xE000); + std::shared_ptr m_log = spdlog::stdout_color_mt("doip-client"); + DoIPAddress m_sourceAddress = DoIPAddress(0xE000); Vin m_vin{0}; DoIPAddress m_logicalAddress = ZERO_ADDRESS; EntityId m_eid{0}; GroupId m_gid{0}; DoIPFurtherAction m_furtherActionReqResult = DoIPFurtherAction::NoFurtherAction; - void parseVehicleIdentificationResponse(const DoIPMessage& msg); + void tcpThreadFunction(); + bool activateRouting(); + ssize_t sendDoIPMessage(const DoIPMessage &msg); + std::optional receiveMessage(); + + + void parseVehicleIdentificationResponse(const DoIPMessage &msg); int emptyMessageCounter = 0; }; diff --git a/inc/DoIPClientModel.h b/inc/DoIPClientModel.h new file mode 100644 index 0000000..05b69be --- /dev/null +++ b/inc/DoIPClientModel.h @@ -0,0 +1,37 @@ +#ifndef DOIPCLIENTMODEL_H +#define DOIPCLIENTMODEL_H + +#include "DoIPMessage.h" + +#include + +namespace doip { + class DoIPClient; + + + struct DoIPClientModel { + virtual ~DoIPClientModel() = default; + + // TODO: Add UDP methods + + virtual void routingActivated(DoIPClient& client, bool activated, DoIPAddress logicalAddress = ZERO_ADDRESS) { + (void)client; + (void)activated; + (void)logicalAddress; + } + + virtual void messageReceived(DoIPClient& client, const DoIPMessage& msg) { + (void)client; + (void)msg; + } + + virtual void messageSent(DoIPClient& client, const DoIPMessage& msg) { + (void)client; + (void)msg; + } + }; + + using UniqueDoIPClientModelPtr = std::unique_ptr; +} // namespace doip + +#endif /* DOIPCLIENTMODEL_H */ diff --git a/inc/DoIPMessage.h b/inc/DoIPMessage.h index 8f2484c..77bd587 100644 --- a/inc/DoIPMessage.h +++ b/inc/DoIPMessage.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "util/AnsiColors.h" @@ -510,6 +511,8 @@ class DoIPMessage { } }; +using DoIPMessageHandler = std::function; + /** * @brief Factory functions for creating specific DoIP message types. * diff --git a/src/DoIPClient.cpp b/src/DoIPClient.cpp index a9d241e..0fe04e7 100644 --- a/src/DoIPClient.cpp +++ b/src/DoIPClient.cpp @@ -10,10 +10,11 @@ using namespace doip; /* *Set up the connection between client and server */ -void DoIPClient::startTcpConnection() { - m_tcpSocket = socket(AF_INET, SOCK_STREAM, 0); +bool DoIPClient::startTcpConnection() { + int tmpSocket = socket(AF_INET, SOCK_STREAM, 0); - if (m_tcpSocket >= 0) { + if (tmpSocket >= 0) { + m_tcpSocket.reset(tmpSocket); m_log->info("Client TCP-Socket created successfully"); bool connectedFlag = false; @@ -22,21 +23,40 @@ void DoIPClient::startTcpConnection() { m_serverAddress.sin_port = htons(DOIP_UDP_DISCOVERY_PORT); inet_aton(ipAddr, &(m_serverAddress.sin_addr)); - while (!connectedFlag) { - m_connected = connect(m_tcpSocket, reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); - if (m_connected != -1) { + int retries = 3; + while (!connectedFlag && retries > 0) { + int tmpConnSocket = connect(m_tcpSocket.get(), reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); + if (tmpConnSocket != -1) { + m_connected.reset(tmpConnSocket); connectedFlag = true; m_log->info("Connection to server established"); + + if (!activateRouting()) { + m_model->routingActivated(*this, false, m_logicalAddress); + m_log->error("Routing activation failed"); + return false; + } + + m_model->routingActivated(*this, true, m_logicalAddress); + + m_tcpRunning.store(true); + m_tcpThread = std::thread(&DoIPClient::tcpThreadFunction, this); + return true; } + std::this_thread::sleep_for(std::chrono::seconds(1)); + retries--; } } + + return false; } void DoIPClient::startUdpConnection() { - m_udpSocket = socket(AF_INET, SOCK_DGRAM, 0); + int tmpUdpSocket = socket(AF_INET, SOCK_DGRAM, 0); - if (m_udpSocket >= 0) { + if (tmpUdpSocket >= 0) { + m_udpSocket.reset(tmpUdpSocket); m_log->info("Client-UDP-Socket created successfully"); m_serverAddress.sin_family = AF_INET; @@ -48,28 +68,28 @@ void DoIPClient::startUdpConnection() { m_clientAddress.sin_addr.s_addr = htonl(INADDR_ANY); // binds the socket to any IP DoIPAddress and the Port Number 13400 - auto rc = bind(m_udpSocket, reinterpret_cast(&m_clientAddress), sizeof(m_clientAddress)); + auto rc = bind(m_udpSocket.get(), reinterpret_cast(&m_clientAddress), sizeof(m_clientAddress)); if (!rc) { m_log->error("Bind failed: {}", strerror(errno)); - close(m_udpSocket); - m_udpSocket = -1; + close(m_udpSocket.get()); } } } void DoIPClient::startAnnouncementListener() { - m_udpAnnouncementSocket = socket(AF_INET, SOCK_DGRAM, 0); + int tmpUdpAnnouncementSocket = socket(AF_INET, SOCK_DGRAM, 0); - if (m_udpAnnouncementSocket >= 0) { + if (tmpUdpAnnouncementSocket >= 0) { + m_udpAnnouncementSocket.reset(tmpUdpAnnouncementSocket); m_log->info("Client-Announcement-Socket created successfully"); // Allow socket reuse for broadcast int reuse = 1; - setsockopt(m_udpAnnouncementSocket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + setsockopt(m_udpAnnouncementSocket.get(), SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); // Enable broadcast reception int broadcast = 1; - if (setsockopt(m_udpAnnouncementSocket, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { + if (setsockopt(m_udpAnnouncementSocket.get(), SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { m_log->error("Failed to enable broadcast reception: {}", strerror(errno)); } else { m_log->info("Broadcast reception enabled for announcements"); @@ -80,7 +100,7 @@ void DoIPClient::startAnnouncementListener() { m_announcementAddress.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to port 13401 for Vehicle Announcements - if (bind(m_udpAnnouncementSocket, reinterpret_cast(&m_announcementAddress), sizeof(m_announcementAddress)) < 0) { + if (bind(m_udpAnnouncementSocket.get(), reinterpret_cast(&m_announcementAddress), sizeof(m_announcementAddress)) < 0) { m_log->error("Failed to bind announcement socket to port {}: {}", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT, strerror(errno)); } else { m_log->info("Announcement socket bound to port {} successfully", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); @@ -90,54 +110,122 @@ void DoIPClient::startAnnouncementListener() { } } -/* - * closes the client-socket - */ +void DoIPClient::tcpThreadFunction() { + int sendRetries = 5; + int receiveRetries = 5; + + while (m_tcpRunning.load() && sendRetries > 0 && receiveRetries > 0) { + if (m_messageQueue.empty()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + DoIPMessage msg; + m_messageQueue.pop(msg); + if (sendDoIPMessage(msg) < 0) { + --sendRetries; + m_log->error("Failed to send DoIP message from queue, retries left: {}", sendRetries); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } else { + m_model->messageSent(*this, msg); + } + + sendRetries = 5; + + auto optMsg = receiveMessage(); + if (optMsg == std::nullopt) { + --receiveRetries; + m_log->error("Failed to receive DoIP message in TCP thread, retries left: {}", receiveRetries); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } else { + receiveRetries = 5; + m_model->messageReceived(*this, optMsg.value()); + } + } +} + +bool DoIPClient::activateRouting() { + ssize_t result = sendRoutingActivationRequest(); + if (result < 0) { + m_log->error("Failed to send Routing Activation Request: {}", strerror(errno)); + return false; + } + + auto optMsg = receiveMessage(); + if (optMsg == std::nullopt) { + m_log->error("Failed to receive Routing Activation Response"); + return false; + } + + DoIPMessage msg = optMsg.value(); + if (msg.getPayloadType() != DoIPPayloadType::RoutingActivationResponse) { + m_log->error("Received unexpected message type ({}) instead of Routing Activation Response", fmt::streamed(msg.getPayloadType())); + return false; + } + + auto optLogicalAddress = msg.getLogicalAddress(); + if (!optLogicalAddress) { + m_log->error("Routing Activation Response missing logical address"); + return false; + } + + m_logicalAddress = optLogicalAddress.value(); + + return true; +} + +void DoIPClient::sendMessage(const DoIPMessage &msg) { + m_messageQueue.push(msg); +} + void DoIPClient::closeTcpConnection() { - close(m_tcpSocket); + m_tcpRunning.store(false); + m_tcpThread.join(); + m_connected.close(); + m_tcpSocket.close(); } void DoIPClient::closeUdpConnection() { - close(m_udpSocket); - if (m_udpAnnouncementSocket >= 0) { - close(m_udpAnnouncementSocket); + m_udpSocket.close(); + if (m_udpAnnouncementSocket.get() >= 0) { + m_udpAnnouncementSocket.close(); } } -void DoIPClient::reconnectServer() { +bool DoIPClient::reconnectServer() { closeTcpConnection(); - startTcpConnection(); + return startTcpConnection(); +} + +ssize_t DoIPClient::sendDoIPMessage(const DoIPMessage &msg) { + m_log->info("TX: {}", fmt::streamed(msg)); + return write(m_tcpSocket.get(), msg.data(), msg.size()); } ssize_t DoIPClient::sendRoutingActivationRequest() { - DoIPMessage routingActReq = message::makeRoutingActivationRequest(m_sourceAddress); - m_log->info("TX: {}", fmt::streamed(routingActReq)); - return write(m_tcpSocket, routingActReq.data(), routingActReq.size()); + return sendDoIPMessage(message::makeRoutingActivationRequest(m_sourceAddress)); } ssize_t DoIPClient::sendDiagnosticMessage(const ByteArray &payload) { - DoIPMessage msg = message::makeDiagnosticMessage(m_sourceAddress, m_logicalAddress, payload); - m_log->info("TX: {}", fmt::streamed(msg)); - - return write(m_tcpSocket, msg.data(), msg.size()); + return sendDoIPMessage(message::makeDiagnosticMessage(m_sourceAddress, m_logicalAddress, payload)); } ssize_t DoIPClient::sendAliveCheckResponse() { - DoIPMessage msg = message::makeAliveCheckResponse(m_sourceAddress); - m_log->info("TX: {}", fmt::streamed(msg)); - return write(m_tcpSocket, msg.data(), msg.size()); + return sendDoIPMessage(message::makeAliveCheckResponse(m_sourceAddress)); } /* * Receive a message from server */ -void DoIPClient::receiveMessage() { +std::optional DoIPClient::receiveMessage() { - ssize_t bytesRead = recv(m_tcpSocket, m_receiveBuf.data(), _maxDataSize, 0); + ssize_t bytesRead = recv(m_tcpSocket.get(), m_receiveBuf.data(), _maxDataSize, 0); if (bytesRead < 0) { m_log->error("Error receiving data from server"); - return; + return std::nullopt; } if (!bytesRead) // if server is disconnected from client; client gets empty messages @@ -147,18 +235,22 @@ void DoIPClient::receiveMessage() { if (emptyMessageCounter == 5) { m_log->warn("Received too many empty messages. Reconnect TCP connection"); emptyMessageCounter = 0; - reconnectServer(); + if (!reconnectServer()) { + m_log->error("Reconnection failed"); + } } - return; + return std::nullopt; } auto optMmsg = DoIPMessage::tryParse(m_receiveBuf.data(), static_cast(bytesRead)); if (!optMmsg.has_value()) { m_log->error("Failed to parse DoIP message from received data"); - return; + return std::nullopt; } + DoIPMessage msg = optMmsg.value(); m_log->info("RX: {}", fmt::streamed(msg)); + return msg; } void DoIPClient::receiveUdpMessage() { @@ -169,10 +261,10 @@ void DoIPClient::receiveUdpMessage() { struct timeval timeout; timeout.tv_sec = 3; timeout.tv_usec = 0; - setsockopt(m_udpSocket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); int bytesRead; - bytesRead = recvfrom(m_udpSocket, m_receiveBuf.data(), _maxDataSize, 0, reinterpret_cast(&m_clientAddress), &length); + bytesRead = recvfrom(m_udpSocket.get(), m_receiveBuf.data(), _maxDataSize, 0, reinterpret_cast(&m_clientAddress), &length); if (bytesRead < 0) { if (errno == EAGAIN) { @@ -206,9 +298,9 @@ bool DoIPClient::receiveVehicleAnnouncement() { struct timeval timeout; timeout.tv_sec = 2; // 2 second timeout timeout.tv_usec = 0; - setsockopt(m_udpAnnouncementSocket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + setsockopt(m_udpAnnouncementSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - bytesRead = recvfrom(m_udpAnnouncementSocket, m_receiveBuf.data(), _maxDataSize, 0, + bytesRead = recvfrom(m_udpAnnouncementSocket.get(), m_receiveBuf.data(), _maxDataSize, 0, reinterpret_cast(&m_announcementAddress), &length); if (bytesRead < 0) { if (errno == EAGAIN) { @@ -245,7 +337,7 @@ ssize_t DoIPClient::sendVehicleIdentificationRequest(const char *inet_address) { m_log->error("Could not set address. Try again"); } - int socketError = setsockopt(m_udpSocket, SOL_SOCKET, SO_BROADCAST, &m_broadcast, sizeof(m_broadcast)); + int socketError = setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_BROADCAST, &m_broadcast, sizeof(m_broadcast)); if (socketError == 0) { m_log->info("Broadcast Option set successfully"); @@ -253,7 +345,7 @@ ssize_t DoIPClient::sendVehicleIdentificationRequest(const char *inet_address) { DoIPMessage vehicleIdReq = message::makeVehicleIdentificationRequest(); - ssize_t bytesSent = sendto(m_udpSocket, vehicleIdReq.data(), vehicleIdReq.size(), 0, reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); + ssize_t bytesSent = sendto(m_udpSocket.get(), vehicleIdReq.data(), vehicleIdReq.size(), 0, reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); m_log->info("Sent Vehicle Identification Request to {}:{}", inet_address, ntohs(m_serverAddress.sin_port)); if (bytesSent > 0) { @@ -271,20 +363,6 @@ void DoIPClient::setSourceAddress(const DoIPAddress &address) { m_sourceAddress = address; } -/* - * Getter for _sockFD - */ -int DoIPClient::getSockFd() { - return m_tcpSocket; -} - -/* - * Getter for m_connected - */ -int DoIPClient::getConnected() { - return m_connected; -} - void DoIPClient::parseVehicleIdentificationResponse(const DoIPMessage &msg) { auto optVin = msg.getVin(); auto optEid = msg.getEid(); @@ -304,48 +382,10 @@ void DoIPClient::parseVehicleIdentificationResponse(const DoIPMessage &msg) { } void DoIPClient::printVehicleInformationResponse() { - std::ostringstream ss; - // output VIN - ss << "VIN: " ; - if (Logger::colorsSupported()) { - ss << ansi::bold_green; - } - ss << m_vin << ansi::reset ; - m_log->info(ss.str()); - - // output LogicalAddress - ss = std::ostringstream{}; - ss << "LA : "; - if (Logger::colorsSupported()) { - ss << ansi::bold_green; - } - ss << m_logicalAddress << ansi::reset; - m_log->info(ss.str()); - - // output EID - ss = std::ostringstream{}; - ss << "EID: "; - if (Logger::colorsSupported()) { - ss << ansi::bold_green; - } - ss << m_eid << ansi::reset; - m_log->info(ss.str()); - - // output GID - ss = std::ostringstream{}; - ss << "GID: "; - if (Logger::colorsSupported()) { - ss << ansi::bold_green; - } - ss << m_gid << ansi::reset; - m_log->info(ss.str()); - - // output FurtherActionRequest - ss = std::ostringstream{}; - ss << "FAR: "; - if (Logger::colorsSupported()) { - ss << ansi::bold_green; - } - ss << m_furtherActionReqResult << ansi::reset; - m_log->info(ss.str()); + m_log->info("Vehicle Identification Response:"); + m_log->info("VIN: {}", fmt::streamed(m_vin)); + m_log->info("EID: {}", fmt::streamed(m_eid)); + m_log->info("GID: {}", fmt::streamed(m_gid)); + m_log->info("Logical Address: 0x{:04X}", m_logicalAddress); + m_log->info("Further Action Request Result: {}", static_cast(m_furtherActionReqResult)); } From 594a12f672efeb4ed32cc399c98959bf664f7357 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sat, 3 Jan 2026 21:31:28 +0100 Subject: [PATCH 03/44] fix: Improve client, add isUdpRunnining and isTcpRunnining (WIP) --- examples/minimal/MinimalDoIPClient.cpp | 29 +++++++++++++++---- examples/minimal/MinimalDoIPServer.cpp | 2 +- examples/socket-can/DoIPCanIsoTpServer.cpp | 3 +- inc/DoIPClient.h | 10 +++---- inc/DoIPClientModel.h | 3 +- inc/DoIPServer.h | 12 ++++++-- src/DoIPClient.cpp | 28 ++++++++++++------ test/integration/discover/Discover_Server.cpp | 2 +- .../uds-download/DoIP_Uds_Server.cpp | 2 +- test/unit/DoIPServer_Test.cpp | 2 +- 10 files changed, 65 insertions(+), 28 deletions(-) diff --git a/examples/minimal/MinimalDoIPClient.cpp b/examples/minimal/MinimalDoIPClient.cpp index 60dda4f..eb39ce8 100644 --- a/examples/minimal/MinimalDoIPClient.cpp +++ b/examples/minimal/MinimalDoIPClient.cpp @@ -5,9 +5,26 @@ using namespace doip; struct MyDoIPClientModel : public DoIPClientModel { - void messageReceived(DoIPClient& client, const DoIPMessage& msg) override { + void routingActivated(DoIPClient& client, bool activated, DoIPAddress logicalAddress) override { + (void)client; + if (activated) { + std::cout << "Routing activated with logical address: " << logicalAddress << std::endl; + } else { + std::cout << "Routing activation failed" << std::endl; + } + client.sendDiagnosticMessage({0x10, 0x02}); // Example UDS message + } + + void messageSent(DoIPClient& client, const DoIPMessage& msg) override { + (void)client; + std::cout << "Message sent: " << msg << std::endl; + } + + bool messageReceived(DoIPClient& client, const DoIPMessage& msg) override { (void)client; std::cout << "Message received: " << msg << std::endl; + + return false; // return false to indicate quit } }; @@ -19,9 +36,11 @@ int main() { return 1; } - while(client.isTcpConnected()) { - // Main loop can perform other tasks or just sleep - std::this_thread::sleep_for(std::chrono::seconds(1)); - } + // Wait a bit for communication to complete + std::this_thread::sleep_for(std::chrono::seconds(3)); + + // Clean up: close the connection (this will join the thread) + client.closeTcpConnection(); + return 0; } \ No newline at end of file diff --git a/examples/minimal/MinimalDoIPServer.cpp b/examples/minimal/MinimalDoIPServer.cpp index 9babf91..15de9d5 100644 --- a/examples/minimal/MinimalDoIPServer.cpp +++ b/examples/minimal/MinimalDoIPServer.cpp @@ -19,7 +19,7 @@ int main(int argc, char *argv[]) { console->info("DoIP Minimal Server is running"); - while (server->isRunning()) { + while (server->isTcpRunnining()) { // Main loop can perform other tasks or just sleep std::this_thread::sleep_for(std::chrono::seconds(1)); } diff --git a/examples/socket-can/DoIPCanIsoTpServer.cpp b/examples/socket-can/DoIPCanIsoTpServer.cpp index 1722f97..bc8e786 100644 --- a/examples/socket-can/DoIPCanIsoTpServer.cpp +++ b/examples/socket-can/DoIPCanIsoTpServer.cpp @@ -69,7 +69,8 @@ int main(int argc, char *argv[]) { doipReceiver.push_back(thread(&listenTcp)); logger->info("Starting TCP listener threads"); - while (server->isRunning()) { + while (server->isUdpRunning()) { + FIXME: std::this_thread::, check for tcp, too sleep(1); } diff --git a/inc/DoIPClient.h b/inc/DoIPClient.h index d1b7271..fd36ea4 100644 --- a/inc/DoIPClient.h +++ b/inc/DoIPClient.h @@ -20,8 +20,6 @@ namespace doip { const int _maxDataSize = 64; - - class DoIPClient { public: @@ -52,7 +50,7 @@ class DoIPClient { * Sends a diagnostic message to the server * @param payload data that will be given to the ecu */ - ssize_t sendDiagnosticMessage(const ByteArray &payload); + void sendDiagnosticMessage(const ByteArray &payload); /** * Sends a alive check response containing the clients source address to the server @@ -82,9 +80,9 @@ class DoIPClient { DoIPFurtherAction m_furtherActionReqResult = DoIPFurtherAction::NoFurtherAction; void tcpThreadFunction(); - bool activateRouting(); - ssize_t sendDoIPMessage(const DoIPMessage &msg); - std::optional receiveMessage(); + [[nodiscard]] bool activateRouting(); + [[nodiscard]] ssize_t sendDoIPMessage(const DoIPMessage &msg); + [[nodiscard]] std::optional receiveMessage(); void parseVehicleIdentificationResponse(const DoIPMessage &msg); diff --git a/inc/DoIPClientModel.h b/inc/DoIPClientModel.h index 05b69be..506ad19 100644 --- a/inc/DoIPClientModel.h +++ b/inc/DoIPClientModel.h @@ -20,9 +20,10 @@ namespace doip { (void)logicalAddress; } - virtual void messageReceived(DoIPClient& client, const DoIPMessage& msg) { + virtual bool messageReceived(DoIPClient& client, const DoIPMessage& msg) { (void)client; (void)msg; + return false; // return false to indicate quit } virtual void messageSent(DoIPClient& client, const DoIPMessage& msg) { diff --git a/inc/DoIPServer.h b/inc/DoIPServer.h index 7c0fe1d..aa00697 100644 --- a/inc/DoIPServer.h +++ b/inc/DoIPServer.h @@ -85,10 +85,18 @@ class DoIPServer { bool setupUdpSocket(); /** - * @brief Check if the server is currently running + * @brief Check if the UDP server is currently running */ [[nodiscard]] - bool isRunning() const noexcept { return m_udpRunning.load(); } + bool isUdpRunnining() const noexcept { return m_udpRunning.load(); } + + /** + * @brief Check if the TCP server is currently running + */ + [[nodiscard]] + bool isTcpRunnining() const noexcept { return m_tcpRunning.load(); } + + FIXME: Add isRunning() /** * @brief Set the number of vehicle announcements to send. diff --git a/src/DoIPClient.cpp b/src/DoIPClient.cpp index 0fe04e7..a186228 100644 --- a/src/DoIPClient.cpp +++ b/src/DoIPClient.cpp @@ -4,6 +4,7 @@ #include "util/Logger.h" #include // for errno #include // for strerror +#include using namespace doip; @@ -141,7 +142,10 @@ void DoIPClient::tcpThreadFunction() { continue; } else { receiveRetries = 5; - m_model->messageReceived(*this, optMsg.value()); + if (!m_model->messageReceived(*this, optMsg.value())) { + m_log->info("Model requested to close TCP connection"); + m_tcpRunning.store(false); + } } } } @@ -165,7 +169,7 @@ bool DoIPClient::activateRouting() { return false; } - auto optLogicalAddress = msg.getLogicalAddress(); + auto optLogicalAddress = msg.getSourceAddress(); if (!optLogicalAddress) { m_log->error("Routing Activation Response missing logical address"); return false; @@ -176,13 +180,15 @@ bool DoIPClient::activateRouting() { return true; } -void DoIPClient::sendMessage(const DoIPMessage &msg) { - m_messageQueue.push(msg); -} - void DoIPClient::closeTcpConnection() { m_tcpRunning.store(false); - m_tcpThread.join(); + + // Only join if we're not being called from the TCP thread itself + // This prevents deadlock when called from within a callback + if (m_tcpThread.get_id() != std::this_thread::get_id() && m_tcpThread.joinable()) { + m_tcpThread.join(); + } + m_connected.close(); m_tcpSocket.close(); } @@ -199,6 +205,10 @@ bool DoIPClient::reconnectServer() { return startTcpConnection(); } +void DoIPClient::sendMessage(const DoIPMessage &msg) { + m_messageQueue.push(msg); +} + ssize_t DoIPClient::sendDoIPMessage(const DoIPMessage &msg) { m_log->info("TX: {}", fmt::streamed(msg)); return write(m_tcpSocket.get(), msg.data(), msg.size()); @@ -208,8 +218,8 @@ ssize_t DoIPClient::sendRoutingActivationRequest() { return sendDoIPMessage(message::makeRoutingActivationRequest(m_sourceAddress)); } -ssize_t DoIPClient::sendDiagnosticMessage(const ByteArray &payload) { - return sendDoIPMessage(message::makeDiagnosticMessage(m_sourceAddress, m_logicalAddress, payload)); +void DoIPClient::sendDiagnosticMessage(const ByteArray &payload) { + sendMessage(message::makeDiagnosticMessage(m_sourceAddress, m_logicalAddress, payload)); } ssize_t DoIPClient::sendAliveCheckResponse() { diff --git a/test/integration/discover/Discover_Server.cpp b/test/integration/discover/Discover_Server.cpp index 9104102..5729742 100644 --- a/test/integration/discover/Discover_Server.cpp +++ b/test/integration/discover/Discover_Server.cpp @@ -79,7 +79,7 @@ int main(int argc, char *argv[]) { console->info("DoIP Server is running. Waiting for connections..."); - while (server->isRunning()) { + while (server->isUdpRunnining()) { if (stopRequested.load()) { server->stop(); break; diff --git a/test/integration/uds-download/DoIP_Uds_Server.cpp b/test/integration/uds-download/DoIP_Uds_Server.cpp index f16fe42..c2f14af 100644 --- a/test/integration/uds-download/DoIP_Uds_Server.cpp +++ b/test/integration/uds-download/DoIP_Uds_Server.cpp @@ -76,7 +76,7 @@ int main(int argc, char *argv[]) { console->info("DoIP Server is running. Waiting for connections..."); - while (server->isRunning()) { + while (server->isUdpRunnining()) { if (stopRequested.load()) { server->stop(); break; diff --git a/test/unit/DoIPServer_Test.cpp b/test/unit/DoIPServer_Test.cpp index 7665ea4..07e01b6 100644 --- a/test/unit/DoIPServer_Test.cpp +++ b/test/unit/DoIPServer_Test.cpp @@ -16,7 +16,7 @@ TEST_SUITE("DoIPServer Tests") { DoIPServerFixture() { // Setup code here if needed - CHECK(server.isRunning() == false); + CHECK(server.isUdpRunnining() == false); CHECK(server.getVin() == Vin::Zero); CHECK(server.getEid() == EntityId::Zero); CHECK(server.getGid() == GroupId::Zero); From 4c40d94c7fa44e517b4921d13fe73d556c4874ec Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 16:05:13 +0100 Subject: [PATCH 04/44] refactor: Merge DoIPNegativeDiagnosticAck and positive ack with DoIPDiagnosticAck --- inc/DoIPConnection.h | 4 +- inc/DoIPDiagnosticAck.h | 73 +++++++++++++++++++++++++ inc/DoIPDownstreamServerModel.h | 4 +- inc/DoIPMessage.h | 18 +++++-- inc/DoIPNegativeDiagnosticAck.h | 94 --------------------------------- inc/IConnectionContext.h | 6 +-- src/DoIPConnection.cpp | 2 +- src/DoIPDefaultConnection.cpp | 18 +++---- test/unit/DoIPMessage_Test.cpp | 2 +- test/unit/Stream_Test.cpp | 4 +- 10 files changed, 108 insertions(+), 117 deletions(-) create mode 100644 inc/DoIPDiagnosticAck.h delete mode 100644 inc/DoIPNegativeDiagnosticAck.h diff --git a/inc/DoIPConnection.h b/inc/DoIPConnection.h index ae6ef14..376e522 100644 --- a/inc/DoIPConnection.h +++ b/inc/DoIPConnection.h @@ -5,7 +5,7 @@ #include "DoIPConfig.h" #include "DoIPMessage.h" #include "DoIPNegativeAck.h" -#include "DoIPNegativeDiagnosticAck.h" +#include "DoIPDiagnosticAck.h" #include "DoIPServerModel.h" #include "DoIPDefaultConnection.h" #include @@ -34,7 +34,7 @@ class DoIPConnection : public DoIPDefaultConnection { bool isSocketActive() { return m_isOpen; }; void sendDiagnosticAck(const DoIPAddress &sourceAddress); - void sendDiagnosticNegativeAck(const DoIPAddress &sourceAddress, DoIPNegativeDiagnosticAck ackCode); + void sendDiagnosticNegativeAck(const DoIPAddress &sourceAddress, DoIPDiagnosticAck ackCode); // === IConnectionContext interface implementation === diff --git a/inc/DoIPDiagnosticAck.h b/inc/DoIPDiagnosticAck.h new file mode 100644 index 0000000..4b207ce --- /dev/null +++ b/inc/DoIPDiagnosticAck.h @@ -0,0 +1,73 @@ +#ifndef DOIPNEGATIVEDIAGNOSTICACK_H +#define DOIPNEGATIVEDIAGNOSTICACK_H + +#include +#include +#include +#include + +namespace doip { + +// Table 24/26 +enum class DoIPDiagnosticAck : uint8_t { + PositiveAck = 0, // Table 24 + // 1: reserved + InvalidSourceAddress = 2, + UnknownTargetAddress = 3, + DiagnosticMessageTooLarge = 4, + OutOfMemory = 5, + TargetUnreachable = 6, // optional for Table 26 + UnknownNetwork = 7, // optional for Table 26 + TransportProtocolError = 8, // optional for Table 26, also use if other error codes do not apply + TargetBusy = 9, // optional for Table 26 +}; + +/** + * @brief Stream output operator for DoIPDiagnosticAck + * + * @param os the output stream + * @param ackCode the acknowledgment code + * @return std::ostream& the output stream + */ +inline std::ostream& operator<<(std::ostream& os, doip::DoIPDiagnosticAck ackCode) { + const char* name = nullptr; + switch (ackCode) { + case doip::DoIPDiagnosticAck::PositiveAck: + name = "Positive Ack"; + break; + case doip::DoIPDiagnosticAck::InvalidSourceAddress: + name = "NACK: Invalid Source Address"; + break; + case doip::DoIPDiagnosticAck::UnknownTargetAddress: + name = "NACK: Unknown Target Address"; + break; + case doip::DoIPDiagnosticAck::DiagnosticMessageTooLarge: + name = "NACK: Diagnostic Message Too Large"; + break; + case doip::DoIPDiagnosticAck::OutOfMemory: + name = "NACK: Out Of Memory"; + break; + case doip::DoIPDiagnosticAck::TargetUnreachable: + name = "NACK: Target Unreachable"; + break; + case doip::DoIPDiagnosticAck::UnknownNetwork: + name = "NACK: Unknown Network"; + break; + case doip::DoIPDiagnosticAck::TransportProtocolError: + name = "NACK: Transport Protocol Error"; + break; + case doip::DoIPDiagnosticAck::TargetBusy: + name = "NACK: Target Busy"; + break; + default: + name = "Unknown"; + break; + } + os << name << " (0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') + << static_cast(static_cast(ackCode)) << std::dec << ")"; + + return os; +} + +} +#endif /* DOIPNEGATIVEDIAGNOSTICACK_H */ diff --git a/inc/DoIPDownstreamServerModel.h b/inc/DoIPDownstreamServerModel.h index ef60dbe..08cbfa2 100644 --- a/inc/DoIPDownstreamServerModel.h +++ b/inc/DoIPDownstreamServerModel.h @@ -34,10 +34,10 @@ class DoIPDownstreamServerModel : public DoIPServerModel { // auto payload = msg.getDiagnosticMessagePayload(); // if (payload.second >= 3 && payload.first[0] == 0x22 && payload.first[1] == 0xF1 && payload.first[2] == 0x90) { // m_log->info(" - Detected Read Data by Identifier for VIN (0xF190) -> send NACK"); - // return DoIPNegativeDiagnosticAck::UnknownTargetAddress; + // return DoIPDiagnosticAck::UnknownTargetAddress; // } - return std::nullopt; + return DoIPDiagnosticAck::PositiveAck; }; onDiagnosticNotification = [this](IConnectionContext &ctx, DoIPDiagnosticAck ack) noexcept { diff --git a/inc/DoIPMessage.h b/inc/DoIPMessage.h index 77bd587..504ffb9 100644 --- a/inc/DoIPMessage.h +++ b/inc/DoIPMessage.h @@ -15,7 +15,7 @@ #include "DoIPIdentifiers.h" #include "DoIPNegativeAck.h" -#include "DoIPNegativeDiagnosticAck.h" +#include "DoIPDiagnosticAck.h" #include "DoIPPayloadType.h" #include "DoIPRoutingActivationType.h" #include "DoIPSyncStatus.h" @@ -195,6 +195,18 @@ class DoIPMessage { return {m_data.data() + DOIP_DIAG_HEADER_SIZE, m_data.size() - DOIP_DIAG_HEADER_SIZE}; } + std::optional getDiagnosticAck() const noexcept { + auto payloadRef = getPayload(); + if (getPayloadType() == DoIPPayloadType::DiagnosticMessageNegativeAck && payloadRef.second >= 1) { + return static_cast(payloadRef.first[0]); + } + + if (getPayloadType() == DoIPPayloadType::DiagnosticMessageAck && payloadRef.second >= 1) { + return DoIPDiagnosticAck::PositiveAck; + } + return std::nullopt; + } + /** * @brief Gets the payload size in bytes (without header). * @@ -631,7 +643,7 @@ inline DoIPMessage makeDiagnosticPositiveResponse( inline DoIPMessage makeDiagnosticNegativeResponse( const DoIPAddress &sa, const DoIPAddress &ta, - DoIPNegativeDiagnosticAck nack, + DoIPDiagnosticAck nack, const ByteArray &msg_payload) { ByteArray payload; @@ -737,7 +749,7 @@ inline std::ostream &operator<<(std::ostream &os, const DoIPMessage &msg) { os << ansi::red << "|Diag NACK "; return os; } - os << ansi::red << "|Diag NACK " << static_cast(payload.first[0]); + os << ansi::red << "|Diag NACK " << static_cast(payload.first[0]); } else if (msg.getPayloadType() == DoIPPayloadType::AliveCheckRequest) { os << ansi::yellow << "|Alive Check?"; } else if (msg.getPayloadType() == DoIPPayloadType::AliveCheckResponse) { diff --git a/inc/DoIPNegativeDiagnosticAck.h b/inc/DoIPNegativeDiagnosticAck.h deleted file mode 100644 index 924d085..0000000 --- a/inc/DoIPNegativeDiagnosticAck.h +++ /dev/null @@ -1,94 +0,0 @@ -#ifndef DOIPNEGATIVEDIAGNOSTICACK_H -#define DOIPNEGATIVEDIAGNOSTICACK_H - -#include -#include -#include -#include - -namespace doip { - -// Table 26 -enum class DoIPNegativeDiagnosticAck : uint8_t { - // 0 and 1: reserved - InvalidSourceAddress = 2, - UnknownTargetAddress = 3, - DiagnosticMessageTooLarge = 4, - OutOfMemory = 5, - TargetUnreachable = 6, - UnknownNetwork = 7, - TransportProtocolError = 8, // also use if other error codes do not apply - TargetBusy = 9, -}; - -/** - * @brief Alias for diagnostic acknowledgment type. - * - * This type represents either a successful acknowledgment (std::nullopt) or - * a negative acknowledgment (DoIPNegativeDiagnosticAck). - * This is to circumvent the reserved value '0' in DoIPNegativeDiagnosticAck. - */ -using DoIPDiagnosticAck = std::optional; - -/** - * @brief Stream output operator for DoIPNegativeDiagnosticAck - * - * @param os the output stream - * @param nack the negative acknowledgment code - * @return std::ostream& the output stream - */ -inline std::ostream& operator<<(std::ostream& os, doip::DoIPNegativeDiagnosticAck nack) { - const char* name = nullptr; - switch (nack) { - case doip::DoIPNegativeDiagnosticAck::InvalidSourceAddress: - name = "InvalidSourceAddress"; - break; - case doip::DoIPNegativeDiagnosticAck::UnknownTargetAddress: - name = "UnknownTargetAddress"; - break; - case doip::DoIPNegativeDiagnosticAck::DiagnosticMessageTooLarge: - name = "DiagnosticMessageTooLarge"; - break; - case doip::DoIPNegativeDiagnosticAck::OutOfMemory: - name = "OutOfMemory"; - break; - case doip::DoIPNegativeDiagnosticAck::TargetUnreachable: - name = "TargetUnreachable"; - break; - case doip::DoIPNegativeDiagnosticAck::UnknownNetwork: - name = "UnknownNetwork"; - break; - case doip::DoIPNegativeDiagnosticAck::TransportProtocolError: - name = "TransportProtocolError"; - break; - case doip::DoIPNegativeDiagnosticAck::TargetBusy: - name = "TargetBusy"; - break; - default: - name = "Unknown"; - break; - } - os << name << " (0x" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') - << static_cast(static_cast(nack)) << std::dec << ")"; - - return os; -} - -/** - * @brief Stream output operator for DoIPNegativeDiagnosticAck - * - * @param os the output stream - * @param ack the negative acknowledgment code - * @return std::ostream& the output stream - */ -inline std::ostream& operator<<(std::ostream& os, doip::DoIPDiagnosticAck ack) { - if (!ack.has_value()) { - os << "PositiveAck (0x00)"; - return os; - } - - return os << ack.value(); -} - -} -#endif /* DOIPNEGATIVEDIAGNOSTICACK_H */ diff --git a/inc/IConnectionContext.h b/inc/IConnectionContext.h index 271d3c9..6126afc 100644 --- a/inc/IConnectionContext.h +++ b/inc/IConnectionContext.h @@ -5,7 +5,7 @@ #include "DoIPCloseReason.h" #include "DoIPDownstreamResult.h" #include "DoIPMessage.h" -#include "DoIPNegativeDiagnosticAck.h" +#include "DoIPDiagnosticAck.h" #include @@ -40,7 +40,7 @@ class IConnectionContext { [[nodiscard]] virtual ssize_t sendProtocolMessage(const DoIPMessage &msg) = 0; [[nodiscard]] virtual std::optional receiveProtocolMessage() = 0; - + /** * @brief Close the TCP connection * @@ -98,7 +98,7 @@ class IConnectionContext { * This forwards the diagnostic message to the application layer for processing. * The application returns either: * - std::nullopt: Send positive ACK - * - DoIPNegativeDiagnosticAck value: Send negative ACK with this code + * - DoIPDiagnosticAck value: Send negative ACK with this code * * @param msg The diagnostic message received * @return std::nullopt for ACK, or NACK code diff --git a/src/DoIPConnection.cpp b/src/DoIPConnection.cpp index c08f6b0..59b17f4 100644 --- a/src/DoIPConnection.cpp +++ b/src/DoIPConnection.cpp @@ -63,7 +63,7 @@ DoIPDiagnosticAck DoIPConnection::notifyDiagnosticMessage(const DoIPMessage &msg return m_serverModel->onDiagnosticMessage(*this, msg); } // Default: ACK - return std::nullopt; + return DoIPDiagnosticAck::PositiveAck; } void DoIPConnection::notifyConnectionClosed(DoIPCloseReason reason) { diff --git a/src/DoIPDefaultConnection.cpp b/src/DoIPDefaultConnection.cpp index a1439a4..9867174 100644 --- a/src/DoIPDefaultConnection.cpp +++ b/src/DoIPDefaultConnection.cpp @@ -242,7 +242,7 @@ void DoIPDefaultConnection::handleRoutingActivated(DoIPServerEvent event, OptDoI return; default: m_log->warn("Received unsupported message type {} in Routing Activated state", fmt::streamed(message.getPayloadType())); - sendDiagnosticMessageResponse(ZERO_ADDRESS, DoIPNegativeDiagnosticAck::TransportProtocolError); + sendDiagnosticMessageResponse(ZERO_ADDRESS, DoIPDiagnosticAck::TransportProtocolError); // closeConnection(DoIPCloseReason::InvalidMessage); return; } @@ -253,7 +253,7 @@ void DoIPDefaultConnection::handleRoutingActivated(DoIPServerEvent event, OptDoI } if (sourceAddress.value() != getClientAddress()) { m_log->warn("Received diagnostic message from unexpected source address {}", fmt::streamed(sourceAddress.value())); - sendDiagnosticMessageResponse(sourceAddress.value(), DoIPNegativeDiagnosticAck::InvalidSourceAddress); + sendDiagnosticMessageResponse(sourceAddress.value(), DoIPDiagnosticAck::InvalidSourceAddress); // closeConnection(DoIPCloseReason::InvalidMessage); return; } @@ -265,7 +265,7 @@ void DoIPDefaultConnection::handleRoutingActivated(DoIPServerEvent event, OptDoI restartStateTimer(); // nack -> stop here - if (ack.has_value()) { + if (ack != DoIPDiagnosticAck::PositiveAck) { transitionTo(DoIPServerState::RoutingActivated); return; } @@ -281,7 +281,7 @@ void DoIPDefaultConnection::handleRoutingActivated(DoIPServerEvent event, OptDoI transitionTo(DoIPServerState::RoutingActivated); } else if (result == DoIPDownstreamResult::Error) { // request could not be handled -> issue error and back to idle - sendDiagnosticMessageResponse(sourceAddress.value(), DoIPNegativeDiagnosticAck::TargetUnreachable); + sendDiagnosticMessageResponse(sourceAddress.value(), DoIPDiagnosticAck::TargetUnreachable); transitionTo(DoIPServerState::RoutingActivated); } } @@ -308,7 +308,7 @@ void DoIPDefaultConnection::handleWaitAliveCheckResponse(DoIPServerEvent event, return; default: m_log->warn("Received unsupported message type {} in Wait Alive Check Response state", fmt::streamed(message.getPayloadType())); - sendDiagnosticMessageResponse(ZERO_ADDRESS, DoIPNegativeDiagnosticAck::TransportProtocolError); + sendDiagnosticMessageResponse(ZERO_ADDRESS, DoIPDiagnosticAck::TransportProtocolError); return; } } @@ -386,11 +386,11 @@ ssize_t DoIPDefaultConnection::sendDiagnosticMessageResponse(const DoIPAddress & DoIPAddress targetAddress = getServerAddress(); DoIPMessage message; - if (ack.has_value()) { + if (ack != DoIPDiagnosticAck::PositiveAck) { message = message::makeDiagnosticNegativeResponse( sourceAddress, targetAddress, - ack.value(), + ack, ByteArray{} // Empty payload ); } else { @@ -416,7 +416,7 @@ DoIPDiagnosticAck DoIPDefaultConnection::notifyDiagnosticMessage(const DoIPMessa if (m_serverModel->onDiagnosticMessage) { return m_serverModel->onDiagnosticMessage(*this, msg); } - return std::nullopt; + return DoIPDiagnosticAck::PositiveAck; } void DoIPDefaultConnection::notifyConnectionClosed(DoIPCloseReason reason) { @@ -452,7 +452,7 @@ void DoIPDefaultConnection::receiveDownstreamResponse(const ByteArray &response, if (result == DoIPDownstreamResult::Handled) { sendProtocolMessage(message::makeDiagnosticMessage(sa, ta, response)); } else { - sendProtocolMessage(message::makeDiagnosticNegativeResponse(sa, ta, DoIPNegativeDiagnosticAck::TargetUnreachable, {})); + sendProtocolMessage(message::makeDiagnosticNegativeResponse(sa, ta, DoIPDiagnosticAck::TargetUnreachable, {})); } transitionTo(DoIPServerState::RoutingActivated); } diff --git a/test/unit/DoIPMessage_Test.cpp b/test/unit/DoIPMessage_Test.cpp index d847cee..d968dab 100644 --- a/test/unit/DoIPMessage_Test.cpp +++ b/test/unit/DoIPMessage_Test.cpp @@ -89,7 +89,7 @@ TEST_SUITE("DoIPMessage") { DoIPMessage msg = message::makeDiagnosticNegativeResponse( DoIPAddress(0xcafe), DoIPAddress(0xbabe), - DoIPNegativeDiagnosticAck::TargetBusy, + DoIPDiagnosticAck::TargetBusy, {0xde, 0xad, 0xbe, 0xef}); ByteArray expected{ diff --git a/test/unit/Stream_Test.cpp b/test/unit/Stream_Test.cpp index 6e72876..23c7520 100644 --- a/test/unit/Stream_Test.cpp +++ b/test/unit/Stream_Test.cpp @@ -6,7 +6,7 @@ #include "DoIPCloseReason.h" #include "DoIPDownstreamResult.h" #include "DoIPFurtherAction.h" -#include "DoIPNegativeDiagnosticAck.h" +#include "DoIPDiagnosticAck.h" #include "DoIPPayloadType.h" #include "DoIPServerState.h" #include "DoIPDefaultConnection.h" @@ -53,7 +53,7 @@ TEST_SUITE("Overloaded Stream Operator Tests") { } // Template test case for various enum types -> basically shots in the dark, assuming sequential types with less than 30 entries - TEST_CASE_TEMPLATE("Stream Operator with Various Types", T, DoIPCloseReason, DoIPDownstreamResult, DoIPFurtherAction, DoIPNegativeDiagnosticAck, DoIPServerState, ConnectionTimers) { + TEST_CASE_TEMPLATE("Stream Operator with Various Types", T, DoIPCloseReason, DoIPDownstreamResult, DoIPFurtherAction, DoIPDiagnosticAck, DoIPServerState, ConnectionTimers) { for (uint8_t i = 0; i < 30; ++i) { std::stringstream ss; T value = static_cast(i); From a9fbc4ea7b0acc38223bfea0ca13d9624cd9eb7b Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 16:06:15 +0100 Subject: [PATCH 05/44] fix: Implement proper RX state handling --- examples/minimal/MinimalDoIPClient.cpp | 2 +- examples/minimal/MinimalDoIPServer.cpp | 13 ++- examples/socket-can/DoIPCanIsoTpServer.cpp | 5 +- inc/DoIPClient.h | 20 ++++- inc/DoIPClientModel.h | 13 ++- inc/DoIPServer.h | 10 ++- inc/DoIPServerModel.h | 4 +- inc/uds/UdsServerModel.h | 29 +++++++ src/DoIPClient.cpp | 80 +++++++++++++++++-- test/integration/discover/Discover_Server.cpp | 2 +- .../uds-download/DoIP_Uds_Server.cpp | 2 +- test/unit/DoIPServer_Test.cpp | 2 +- 12 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 inc/uds/UdsServerModel.h diff --git a/examples/minimal/MinimalDoIPClient.cpp b/examples/minimal/MinimalDoIPClient.cpp index eb39ce8..05ec4c1 100644 --- a/examples/minimal/MinimalDoIPClient.cpp +++ b/examples/minimal/MinimalDoIPClient.cpp @@ -20,7 +20,7 @@ struct MyDoIPClientModel : public DoIPClientModel { std::cout << "Message sent: " << msg << std::endl; } - bool messageReceived(DoIPClient& client, const DoIPMessage& msg) override { + bool diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) override { (void)client; std::cout << "Message received: " << msg << std::endl; diff --git a/examples/minimal/MinimalDoIPServer.cpp b/examples/minimal/MinimalDoIPServer.cpp index 15de9d5..3748366 100644 --- a/examples/minimal/MinimalDoIPServer.cpp +++ b/examples/minimal/MinimalDoIPServer.cpp @@ -1,25 +1,24 @@ #include "DoIPServer.h" +#include "uds/UdsServerModel.h" using namespace doip; +using doip::uds::UdsServerModel; int main(int argc, char *argv[]) { (void)argc; (void)argv; ServerConfig cfg; - auto console = spdlog::stdout_color_mt("doip-server"); - - console->info("Starting DoIP Minimal Server"); auto server = std::make_unique(cfg); - if (!server->setupTcpSocket()) { - console->error("Failed to start DoIP Discovery Server"); + if (!server->setupTcpSocket([]() { return std::make_unique(); })) { + std::cerr << "Failed to setup DoIP TCP server" << std::endl; return 1; } - console->info("DoIP Minimal Server is running"); + std::cout << "DoIP Minimal Server is running" << std::endl; - while (server->isTcpRunnining()) { + while (server->isTcpRunning()) { // Main loop can perform other tasks or just sleep std::this_thread::sleep_for(std::chrono::seconds(1)); } diff --git a/examples/socket-can/DoIPCanIsoTpServer.cpp b/examples/socket-can/DoIPCanIsoTpServer.cpp index bc8e786..d65dd2b 100644 --- a/examples/socket-can/DoIPCanIsoTpServer.cpp +++ b/examples/socket-can/DoIPCanIsoTpServer.cpp @@ -69,9 +69,8 @@ int main(int argc, char *argv[]) { doipReceiver.push_back(thread(&listenTcp)); logger->info("Starting TCP listener threads"); - while (server->isUdpRunning()) { - FIXME: std::this_thread::, check for tcp, too - sleep(1); + while (server->isRunning()) { + std::this_thread::sleep_for(std::chrono::seconds(1)); } doipReceiver.at(0).join(); diff --git a/inc/DoIPClient.h b/inc/DoIPClient.h index fd36ea4..643281d 100644 --- a/inc/DoIPClient.h +++ b/inc/DoIPClient.h @@ -2,8 +2,8 @@ #ifndef DOIPCLIENT_H #define DOIPCLIENT_H -#include "DoIPConfig.h" #include "DoIPClientModel.h" +#include "DoIPConfig.h" #include "DoIPMessage.h" #include "util/Logger.h" #include "util/Socket.h" @@ -21,6 +21,11 @@ namespace doip { const int _maxDataSize = 64; class DoIPClient { + enum class ReceiveState { + WaitForAckOrAliveCheck, + WaitForDiagnosticMessage, + Quit, + }; public: DoIPClient(UniqueDoIPClientModelPtr model = std::make_unique()) : m_model(std::move(model)) { m_receiveBuf.reserve(DOIP_MAXIMUM_MTU); } @@ -37,7 +42,6 @@ class DoIPClient { void startAnnouncementListener(); void closeUdpConnection(); - ssize_t sendRoutingActivationRequest(); std::optional receiveRoutingActivationResponse(); @@ -59,6 +63,14 @@ class DoIPClient { void setSourceAddress(const DoIPAddress &address); void printVehicleInformationResponse(); + protected: + void updateReceiveState(ReceiveState newState) { + if (m_receiveState != newState) { + m_log->info("Receive state changed from {} to {}", static_cast(m_receiveState), static_cast(newState)); + m_receiveState = newState; + } + } + private: UniqueDoIPClientModelPtr m_model; ByteArray m_receiveBuf; @@ -69,7 +81,6 @@ class DoIPClient { int m_broadcast = 1; struct sockaddr_in m_serverAddress, m_clientAddress, m_announcementAddress; - std::shared_ptr m_log = spdlog::stdout_color_mt("doip-client"); DoIPAddress m_sourceAddress = DoIPAddress(0xE000); @@ -79,11 +90,14 @@ class DoIPClient { GroupId m_gid{0}; DoIPFurtherAction m_furtherActionReqResult = DoIPFurtherAction::NoFurtherAction; + ReceiveState m_receiveState = ReceiveState::WaitForAckOrAliveCheck; + void tcpThreadFunction(); [[nodiscard]] bool activateRouting(); [[nodiscard]] ssize_t sendDoIPMessage(const DoIPMessage &msg); [[nodiscard]] std::optional receiveMessage(); + void reactToMessage(const DoIPMessage &msg); void parseVehicleIdentificationResponse(const DoIPMessage &msg); diff --git a/inc/DoIPClientModel.h b/inc/DoIPClientModel.h index 506ad19..dd3d312 100644 --- a/inc/DoIPClientModel.h +++ b/inc/DoIPClientModel.h @@ -4,6 +4,7 @@ #include "DoIPMessage.h" #include +#include namespace doip { class DoIPClient; @@ -20,7 +21,12 @@ namespace doip { (void)logicalAddress; } - virtual bool messageReceived(DoIPClient& client, const DoIPMessage& msg) { + virtual void diagMessageAcked(DoIPClient& client, DoIPDiagnosticAck ack) { + (void)client; + (void)ack; + } + + virtual bool diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) { (void)client; (void)msg; return false; // return false to indicate quit @@ -30,6 +36,11 @@ namespace doip { (void)client; (void)msg; } + + virtual void error(DoIPClient& client, const std::string& errorMsg) { + (void)client; + std::cerr << "DoIPClientModel Error: " << errorMsg << std::endl; + } }; using UniqueDoIPClientModelPtr = std::unique_ptr; diff --git a/inc/DoIPServer.h b/inc/DoIPServer.h index aa00697..26e2eb9 100644 --- a/inc/DoIPServer.h +++ b/inc/DoIPServer.h @@ -88,15 +88,19 @@ class DoIPServer { * @brief Check if the UDP server is currently running */ [[nodiscard]] - bool isUdpRunnining() const noexcept { return m_udpRunning.load(); } + bool isUdpRunning() const noexcept { return m_udpRunning.load(); } /** * @brief Check if the TCP server is currently running */ [[nodiscard]] - bool isTcpRunnining() const noexcept { return m_tcpRunning.load(); } + bool isTcpRunning() const noexcept { return m_tcpRunning.load(); } - FIXME: Add isRunning() + /** + * @brief Check if UDP and/or TCP server is currently running + */ + [[nodiscard]] + bool isRunning() const noexcept { return isTcpRunning() || isUdpRunning(); } /** * @brief Set the number of vehicle announcements to send. diff --git a/inc/DoIPServerModel.h b/inc/DoIPServerModel.h index a2f1d4c..d14eee0 100644 --- a/inc/DoIPServerModel.h +++ b/inc/DoIPServerModel.h @@ -7,7 +7,7 @@ #include "DoIPCloseReason.h" #include "DoIPDownstreamResult.h" #include "DoIPMessage.h" -#include "DoIPNegativeDiagnosticAck.h" +#include "DoIPDiagnosticAck.h" #include "DoIPServerEvent.h" #include "DoIPServerState.h" #include "util/Logger.h" @@ -134,7 +134,7 @@ struct DefaultDoIPServerModel : public DoIPServerModel { (void)msg; //LOG_DOIP_DEBUG("Diagnostic message received on DefaultDoIPServerModel"); // Default: always ACK - return std::nullopt; + return DoIPDiagnosticAck::PositiveAck; }; onDiagnosticNotification = [](IConnectionContext &ctx, DoIPDiagnosticAck ack) noexcept { diff --git a/inc/uds/UdsServerModel.h b/inc/uds/UdsServerModel.h new file mode 100644 index 0000000..81d8ba7 --- /dev/null +++ b/inc/uds/UdsServerModel.h @@ -0,0 +1,29 @@ +#ifndef UDSSERVERMODEL_H +#define UDSSERVERMODEL_H + +#include "DoIPDownstreamServerModel.h" +#include "DoIPServerModel.h" +#include "uds/UdsMockProvider.h" +#include "uds/UdsResponseCode.h" +#include "util/ThreadSafeQueue.h" + +namespace doip::uds { + +/** + * @brief UDS DoIP Server Model with UdsMockProvider + * + */ +class UdsServerModel : public DoIPDownstreamServerModel { + public: + UdsServerModel() : DoIPDownstreamServerModel("uds", m_uds) { + // Customize callbacks if needed + } + + virtual std::string_view getModelName() const override { return "UdsServerModel"; } + + private: + uds::UdsMockProvider m_uds; +}; +} // namespace doip::uds + +#endif /* UDSSERVERMODEL_H */ diff --git a/src/DoIPClient.cpp b/src/DoIPClient.cpp index a186228..be11565 100644 --- a/src/DoIPClient.cpp +++ b/src/DoIPClient.cpp @@ -33,8 +33,9 @@ bool DoIPClient::startTcpConnection() { m_log->info("Connection to server established"); if (!activateRouting()) { + m_log->error("Routing activation failed - connection closed"); + m_connected.close(); m_model->routingActivated(*this, false, m_logicalAddress); - m_log->error("Routing activation failed"); return false; } @@ -115,7 +116,7 @@ void DoIPClient::tcpThreadFunction() { int sendRetries = 5; int receiveRetries = 5; - while (m_tcpRunning.load() && sendRetries > 0 && receiveRetries > 0) { + while (m_tcpRunning.load()) { if (m_messageQueue.empty()) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; @@ -125,6 +126,11 @@ void DoIPClient::tcpThreadFunction() { m_messageQueue.pop(msg); if (sendDoIPMessage(msg) < 0) { --sendRetries; + if (sendRetries == 0) { + m_log->error("Exceeded maximum send retries, close connection"); + m_tcpRunning.store(false); + break; + } m_log->error("Failed to send DoIP message from queue, retries left: {}", sendRetries); std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; @@ -134,20 +140,82 @@ void DoIPClient::tcpThreadFunction() { sendRetries = 5; + // Receive response (ack or alive check) + auto optAck = receiveMessage(); + if (optAck == std::nullopt) { + m_log->error("Failed to receive DoIP ack"); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } else { + reactToMessage(optAck.value()); + } + + if (m_receiveState == ReceiveState::Quit) { + m_log->info("Receive state set to Quit, closing TCP thread"); + break; + } + + if (m_receiveState == ReceiveState::WaitForAckOrAliveCheck) { + continue; + } + auto optMsg = receiveMessage(); if (optMsg == std::nullopt) { --receiveRetries; - m_log->error("Failed to receive DoIP message in TCP thread, retries left: {}", receiveRetries); + if (receiveRetries == 0) { + m_log->error("Exceeded maximum receive retries, close connection"); + m_tcpRunning.store(false); + break; + } + m_log->error("Failed to receive DoIP message, retries left: {}", receiveRetries); std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } else { receiveRetries = 5; - if (!m_model->messageReceived(*this, optMsg.value())) { - m_log->info("Model requested to close TCP connection"); - m_tcpRunning.store(false); + reactToMessage(optMsg.value()); + } + } +} + +void DoIPClient::reactToMessage(const DoIPMessage &msg) { + if (m_receiveState == ReceiveState::WaitForAckOrAliveCheck) { + switch (msg.getPayloadType()) { + case DoIPPayloadType::AliveCheckRequest: + m_log->info("Received Alive Check Request, sending Alive Check Response"); + if (sendAliveCheckResponse() <= 0) { + m_log->error("Failed to send Alive Check Response"); + m_model->error(*this, "Failed to send Alive Check Response"); } + break; + case DoIPPayloadType::DiagnosticMessageNegativeAck: + m_model->diagMessageAcked(*this, msg.getDiagnosticAck().value_or(DoIPDiagnosticAck::TransportProtocolError)); + break; + case DoIPPayloadType::DiagnosticMessageAck: + // received diag msg ack -> proceed to receive diag messages + updateReceiveState(ReceiveState::WaitForDiagnosticMessage); + m_model->diagMessageAcked(*this, DoIPDiagnosticAck::PositiveAck); + break; + default: + m_log->warn("Received unexpected message type ({}) while waiting for Ack or Alive Check", fmt::streamed(msg.getPayloadType())); + m_model->error(*this, "Received unexpected message type"); + break; + } + } else if (m_receiveState == ReceiveState::WaitForDiagnosticMessage) { + // Process diagnostic message + auto payloadType = msg.getPayloadType(); + switch (payloadType) { + case DoIPPayloadType::DiagnosticMessage: { + m_model->diagMessageReceived(*this, msg); + break; } + default: + m_log->warn("Received unhandled DoIP message type: {}", fmt::streamed(payloadType)); + m_model->error(*this, "Received unexpected message type"); + break; + } + updateReceiveState(ReceiveState::WaitForAckOrAliveCheck); } + } bool DoIPClient::activateRouting() { diff --git a/test/integration/discover/Discover_Server.cpp b/test/integration/discover/Discover_Server.cpp index 5729742..cd3ac1d 100644 --- a/test/integration/discover/Discover_Server.cpp +++ b/test/integration/discover/Discover_Server.cpp @@ -79,7 +79,7 @@ int main(int argc, char *argv[]) { console->info("DoIP Server is running. Waiting for connections..."); - while (server->isUdpRunnining()) { + while (server->isUdpRunning()) { if (stopRequested.load()) { server->stop(); break; diff --git a/test/integration/uds-download/DoIP_Uds_Server.cpp b/test/integration/uds-download/DoIP_Uds_Server.cpp index c2f14af..d06ecaf 100644 --- a/test/integration/uds-download/DoIP_Uds_Server.cpp +++ b/test/integration/uds-download/DoIP_Uds_Server.cpp @@ -76,7 +76,7 @@ int main(int argc, char *argv[]) { console->info("DoIP Server is running. Waiting for connections..."); - while (server->isUdpRunnining()) { + while (server->isUdpRunning()) { if (stopRequested.load()) { server->stop(); break; diff --git a/test/unit/DoIPServer_Test.cpp b/test/unit/DoIPServer_Test.cpp index 07e01b6..8e19975 100644 --- a/test/unit/DoIPServer_Test.cpp +++ b/test/unit/DoIPServer_Test.cpp @@ -16,7 +16,7 @@ TEST_SUITE("DoIPServer Tests") { DoIPServerFixture() { // Setup code here if needed - CHECK(server.isUdpRunnining() == false); + CHECK(server.isUdpRunning() == false); CHECK(server.getVin() == Vin::Zero); CHECK(server.getEid() == EntityId::Zero); CHECK(server.getGid() == GroupId::Zero); From a6251bd452b7ac0e2ade712693815ff952c3a62a Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 16:10:36 +0100 Subject: [PATCH 06/44] fix: Show addresses as hex --- inc/DoIPMessage.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inc/DoIPMessage.h b/inc/DoIPMessage.h index 504ffb9..4891f13 100644 --- a/inc/DoIPMessage.h +++ b/inc/DoIPMessage.h @@ -757,19 +757,19 @@ inline std::ostream &operator<<(std::ostream &os, const DoIPMessage &msg) { os << ansi::green << "|Alive Check " << sa.value() << " ✓"; } else if (msg.getPayloadType() == DoIPPayloadType::RoutingActivationRequest) { auto sa = msg.getSourceAddress(); - os << ansi::yellow << "|Routing activation? " << sa.value(); + os << ansi::yellow << "|Routing activation? " << std::hex << sa.value() << std::dec; } else if (msg.getPayloadType() == DoIPPayloadType::RoutingActivationResponse) { auto sa = msg.getSourceAddress(); - os << ansi::green << "|Routing activation " << sa.value() << " ✓"; + os << ansi::green << "|Routing activation " << std::hex << sa.value() << std::dec << " ✓"; } else if (msg.getPayloadType() == DoIPPayloadType::DiagnosticMessage) { auto payload = msg.getDiagnosticMessagePayload(); auto sa = msg.getSourceAddress(); auto ta = msg.getTargetAddress(); os << "|Diag "; - os << ansi::bold_magenta << sa.value(); + os << ansi::bold_magenta << std::hex << sa.value() << std::dec; os << ansi::reset << " -> "; - os << ansi::bold_magenta << ta.value(); + os << ansi::bold_magenta << std::hex << ta.value() << std::dec; os << ansi::reset << ": "; os << ansi::bold_blue; From 1a80e73b8847698dc64e763eaaca1923f2a00e63 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 18:45:12 +0100 Subject: [PATCH 07/44] test: Use test script instead of cmake --- test/integration/uds-download/CMakeLists.txt | 122 +++++------------- .../uds-download/DoIP_Uds_Server.cpp | 5 +- .../uds-download/ExampleDoIPServerModel.h | 32 ----- test/integration/uds-download/run-test.sh | 26 ++++ 4 files changed, 62 insertions(+), 123 deletions(-) delete mode 100644 test/integration/uds-download/ExampleDoIPServerModel.h create mode 100644 test/integration/uds-download/run-test.sh diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index 2cc1049..fc3033a 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -30,17 +30,41 @@ foreach(demo_source ${DISCOVER_SOURCES}) target_link_options(${demo_name} PRIVATE --coverage) endif() - # Install examples (optional) - install(TARGETS ${demo_name} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}/examples - ) - endforeach() +# Paths +set(VENV_DIR ${CMAKE_CURRENT_BINARY_DIR}/venv) +set(PYTHON_EXECUTABLE python3) # or specify a full path + +# Create venv +add_custom_command( + OUTPUT ${VENV_DIR} + COMMAND ${PYTHON_EXECUTABLE} -m venv ${VENV_DIR} + COMMENT "Creating Python virtual environment" +) + +# Install packages +add_custom_command( + OUTPUT ${VENV_DIR}/installed + COMMAND ${VENV_DIR}/bin/pip install udsoncan doipclient + DEPENDS ${VENV_DIR} + COMMENT "Installing Python packages in venv" + BYPRODUCTS ${VENV_DIR}/installed +) + +# Custom target to set up venv and packages +add_custom_target( + setup_venv ALL + DEPENDS ${VENV_DIR}/installed +) + + # Define the source and destination paths set(SOURCE_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/test-local-client.py) set(DESTINATION_SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/test-local-client.py) +configure_file(${SOURCE_SCRIPT} ${DESTINATION_SCRIPT} COPYONLY) +configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) # Add a custom command to copy the file only if it changes add_custom_command( @@ -57,88 +81,8 @@ add_custom_target(copy_script ALL DEPENDS ${DESTINATION_SCRIPT}) message(STATUS "Adding integration tests for examples") -# Integration fixture: start DoIPServer as a daemon -add_test(NAME Integration_StartDiscoverServer - COMMAND bash -c "$ --daemon && sleep 1.0" -) -set_tests_properties(Integration_StartDiscoverServer PROPERTIES - FIXTURES_SETUP demo_server - TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE -) - -# Wait until both UDP 13400 and TCP 13400 are bound before running client tests -# This prevents race conditions where tests start before the server is ready -# Simplified script that works reliably in CI environments -add_test(NAME Integration_WaitForServerReady - COMMAND bash -c " -echo 'Waiting for server to bind ports...' -sleep 5 - -# First check if server process exists -if ! pgrep -f Discover_Server >/dev/null 2>&1; then - echo 'ERROR: Discover_Server process not running!' >&2 - exit 1 -fi - -for i in {1..150}; do - if command -v ss >/dev/null 2>&1; then - # Linux: use ss (without -p to avoid privilege restrictions) - udp_check=`ss -uln 2>/dev/null | grep -c ':13400'` - tcp_check=`ss -tln 2>/dev/null | grep -c ':13400'` - else - # macOS/BSD: use netstat - udp_check=`netstat -an 2>/dev/null | grep -c 'udp.*\\.13400'` - tcp_check=`netstat -an 2>/dev/null | grep -c '\\.13400.*LISTEN'` - fi - - if [ \$udp_check -gt 0 ] && [ \$tcp_check -gt 0 ]; then - echo 'Server ready: both UDP and TCP port 13400 are bound' - # Give server additional time to start sending announcements (important for sanitizers) - sleep 2 - exit 0 - fi - - if [ \$i -eq 50 ]; then - echo \"Still waiting... (UDP check: \$udp_check, TCP check: \$tcp_check)\" - pgrep -f Discover_Server >/dev/null 2>&1 || echo 'WARNING: Server process died!' - fi - - sleep 0.1 -done - -echo 'ERROR: Server not ready after 15 seconds (UDP: '\$udp_check', TCP: '\$tcp_check')' >&2 -pgrep -f Discover_Server >/dev/null 2>&1 || echo 'Server process is NOT running' >&2 -exit 1 -" -) -set_tests_properties(Integration_WaitForServerReady PROPERTIES - FIXTURES_REQUIRED demo_server - FIXTURES_SETUP demo_ready - TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE -) - -# Integration test: run tcp client against the running server -add_test(NAME Integration_DiagSessionRuns - COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/test-local-client.py -) -set_tests_properties(Integration_DiagSessionRuns PROPERTIES - FIXTURES_REQUIRED demo_ready - TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE -) - -# Teardown fixture: stop server and wait for ports to be released -add_test(NAME Integration_StopDiscoverServer - COMMAND bash -c "if [ -f /tmp/doip-discover.pid ]; then kill $(cat /tmp/doip-discover.pid) 2>/dev/null; rm -f /tmp/doip-discover.pid; else pkill -f Discover_Server 2>/dev/null || true; fi; for i in $(seq 1 50); do if command -v ss >/dev/null 2>&1; then ss -tlpn 2>/dev/null | grep -q ':13400' || exit 0; else netstat -an 2>/dev/null | grep -q '\\.13400.*LISTEN' || exit 0; fi; sleep 0.1; done; echo 'Warning: Port 13400 still in use' >&2; exit 0" -) -set_tests_properties(Integration_StopDiscoverServer PROPERTIES - FIXTURES_CLEANUP demo_server - TIMEOUT 10 - LABELS integration - RUN_SERIAL TRUE +add_test( + NAME uds-download-test + COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) diff --git a/test/integration/uds-download/DoIP_Uds_Server.cpp b/test/integration/uds-download/DoIP_Uds_Server.cpp index d06ecaf..966e0a9 100644 --- a/test/integration/uds-download/DoIP_Uds_Server.cpp +++ b/test/integration/uds-download/DoIP_Uds_Server.cpp @@ -1,6 +1,6 @@ #include "DoIPAddress.h" #include "DoIPServer.h" -#include "ExampleDoIPServerModel.h" +#include "uds/UdsServerModel.h" #include #include @@ -31,6 +31,7 @@ static void handle_signal(int) { int main(int argc, char *argv[]) { ServerConfig cfg; cfg.loopback = true; // For testing, use loopback announcements + cfg.logicalAddress = DoIPAddress(0x00E0); // Match client logical address used in tests cfg.daemonize = argc > 1 && std::string(argv[1]) == "--daemon"; // For testing, run as daemon auto console = spdlog::stdout_color_mt("doip-server"); @@ -62,7 +63,7 @@ int main(int argc, char *argv[]) { server = std::make_unique(cfg); // Set up TCP first to ensure transport creates/binds both TCP and UDP sockets - if (!server->setupTcpSocket([]() { return std::make_unique(); })) { + if (!server->setupTcpSocket([]() { return std::make_unique(); })) { console->critical("Failed to set up TCP socket"); return 1; } diff --git a/test/integration/uds-download/ExampleDoIPServerModel.h b/test/integration/uds-download/ExampleDoIPServerModel.h deleted file mode 100644 index 4133db4..0000000 --- a/test/integration/uds-download/ExampleDoIPServerModel.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @brief Example DoIPServerModel with custom callbacks - * - */ - -#ifndef EXAMPLEDOIPSERVERMODEL_H -#define EXAMPLEDOIPSERVERMODEL_H - -#include "DoIPServerModel.h" -#include "DoIPDownstreamServerModel.h" -#include "util/ThreadSafeQueue.h" -#include "uds/UdsMockProvider.h" -#include "uds/UdsResponseCode.h" - -using namespace doip; -using namespace doip::uds; - -class ExampleDoIPServerModel : public DoIPDownstreamServerModel { - public: - ExampleDoIPServerModel() : DoIPDownstreamServerModel("uds-session", m_uds) { - // Customize callbacks if needed - - } - - virtual std::string_view getModelName() const override { return "ExampleDoIPServerModel"; } - - private: - uds::UdsMockProvider m_uds; -}; - - -#endif /* EXAMPLEDOIPSERVERMODEL_H */ diff --git a/test/integration/uds-download/run-test.sh b/test/integration/uds-download/run-test.sh new file mode 100644 index 0000000..bd10826 --- /dev/null +++ b/test/integration/uds-download/run-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Activate virtual environment +source venv/bin/activate + +# Start server in background, log to server.log +./DoIP_Uds_Server > server.log 2>&1 & +SERVER_PID=$! + +# Give the server a moment to start +sleep 1 + +# Run client, log to client.log +python3 ./test-local-client.py > client.log 2>&1 +CLIENT_EXIT=$? + +# Kill server +kill $SERVER_PID + +# Print logs for debugging +echo "=== Server Log ===" +tail server.log +echo "=== Client Log ===" +cat client.log + +# Return client's exit code +exit $CLIENT_EXIT \ No newline at end of file From 9db90fefa17f4d07d98c0f5dc3c2a590d5152c12 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 18:45:57 +0100 Subject: [PATCH 08/44] test: Use std UDS model --- test/integration/discover/Discover_Server.cpp | 3 +- .../discover/ExampleDoIPServerModel.h | 31 ------------------- 2 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 test/integration/discover/ExampleDoIPServerModel.h diff --git a/test/integration/discover/Discover_Server.cpp b/test/integration/discover/Discover_Server.cpp index cd3ac1d..b12a480 100644 --- a/test/integration/discover/Discover_Server.cpp +++ b/test/integration/discover/Discover_Server.cpp @@ -6,7 +6,6 @@ #include "util/Logger.h" #include "DoIPServer.h" -#include "ExampleDoIPServerModel.h" #include "util/Daemonize.h" @@ -65,7 +64,7 @@ int main(int argc, char *argv[]) { server->setAnnounceNum(100); // Send 100 announcements = 50 seconds of announcements (enough for parallel test execution) // Set up TCP first to ensure transport creates/binds both TCP and UDP sockets - if (!server->setupTcpSocket([]() { return std::make_unique(); })) { + if (!server->setupTcpSocket()) { console->critical("Failed to set up TCP socket"); return 1; } diff --git a/test/integration/discover/ExampleDoIPServerModel.h b/test/integration/discover/ExampleDoIPServerModel.h deleted file mode 100644 index f92e07c..0000000 --- a/test/integration/discover/ExampleDoIPServerModel.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @brief Example DoIPServerModel with custom callbacks - * - */ - -#ifndef DoIPServerMODEL_H -#define DoIPServerMODEL_H - -#include "DoIPServerModel.h" -#include "DoIPDownstreamServerModel.h" -#include "util/ThreadSafeQueue.h" -#include "uds/UdsMockProvider.h" -#include "uds/UdsResponseCode.h" - -using namespace doip; -using namespace doip::uds; - -class ExampleDoIPServerModel : public DoIPDownstreamServerModel { - public: - ExampleDoIPServerModel() : DoIPDownstreamServerModel("exmod", m_uds) { - // Customize callbacks if needed - - } - - virtual std::string_view getModelName() const override { return "ExampleDoIPServerModel"; } - - private: - uds::UdsMockProvider m_uds; // TODO: Implement a NULL provider for testing -}; - -#endif /* DoIPServerMODEL_H */ From 08f4da7375a3f4e29d9bb748c84eaa5fbcc5e3f0 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 18:46:49 +0100 Subject: [PATCH 09/44] feat: Add TCP port and UDS max payload to config --- inc/gen/DoIPConfig.h.in | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/inc/gen/DoIPConfig.h.in b/inc/gen/DoIPConfig.h.in index 669f986..716c488 100644 --- a/inc/gen/DoIPConfig.h.in +++ b/inc/gen/DoIPConfig.h.in @@ -1,11 +1,14 @@ #pragma once #include +#include #define DOIP_TIMEOUT_MS @DOIP_TIMEOUT_MS@ #define DOIP_MAX_CONNECTIONS @DOIP_MAX_CONNECTIONS@ +namespace { + /** * @brief Number of retries for DoIP alive check messages. * @details Used in DoIPServerStateMachine for retrying alive check requests. @@ -20,6 +23,34 @@ constexpr uint8_t DOIP_ALIVE_CHECK_RETRIES = @DOIP_ALIVE_CHECK_RETRIES@; */ constexpr uint32_t DOIP_MAXIMUM_MTU = @DOIP_MAXIMUM_MTU@; +/** + * @brief Positive ack for diagnostic message (table 24) + */ +constexpr uint8_t DIAGNOSTIC_MESSAGE_ACK = 0; + +/** + * @brief Size of the DoIP header + */ +constexpr size_t DOIP_HEADER_SIZE = 8; + +/** + * @brief Size of the DoIP diagnostic message header + */ +constexpr size_t DOIP_DIAG_HEADER_SIZE = DOIP_HEADER_SIZE + 4; + +/** + * @brief Maximum Transmission Unit (MTU) size for DoIP diagnostic message payload. + * This value is derived by subtracting the diagnostic header size from the overall DoIP MTU. + * If you need the **maximum UDS payload size**, use this constant. + */ +constexpr uint32_t DOIP_MAXIMUM_DIAG_PAYLOAD = DOIP_MAXIMUM_MTU - DOIP_DIAG_HEADER_SIZE; + + +// Table 46: TCP ports for DoIP +/** + * @brief Default TCP port for DoIP as per ISO 13400-2:2019 + */ +constexpr int DOIP_TCP_DEFAULT_PORT = 13400; // Table 48: UDP Ports for DoIP /** @@ -30,4 +61,6 @@ constexpr int DOIP_UDP_DISCOVERY_PORT = 13400; /** * @brief UDP test equipment request port for DoIP as per ISO 13400-2:2019. */ -constexpr int DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT = 13401; \ No newline at end of file +constexpr int DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT = 13401; + +} // namespace \ No newline at end of file From a47d5e239dc8665790fb33dabd8783f1e0041f2b Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 18:47:29 +0100 Subject: [PATCH 10/44] feat: Use TCP port of config --- inc/DoIPServer.h | 2 -- src/DoIPServer.cpp | 10 +++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/inc/DoIPServer.h b/inc/DoIPServer.h index 26e2eb9..cb35039 100644 --- a/inc/DoIPServer.h +++ b/inc/DoIPServer.h @@ -30,8 +30,6 @@ class DoIPDefaultConnection; const ServerConfig DefaultServerConfig{}; -constexpr int DOIP_SERVER_TCP_PORT = 13400; - /** * @brief Callback invoked when a new TCP connection is established * @return DoIPServerModel to use for this connection, or std::nullopt to reject diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index 3af9170..df73172 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -81,14 +81,14 @@ void DoIPServer::connectionHandlerThread(std::unique_ptr * Set up a tcp socket, so the socket is ready to accept a connection */ bool DoIPServer::setupTcpSocket(std::function modelFactory) { - m_doipLog->debug("Setting up TCP transport on port {}", DOIP_SERVER_TCP_PORT); + m_doipLog->debug("Setting up TCP transport on port {}", DOIP_TCP_DEFAULT_PORT); if (!m_transport) { m_doipLog->error("Transport not initialized"); return false; } - if (!m_transport->setup(DOIP_SERVER_TCP_PORT)) { + if (!m_transport->setup(DOIP_TCP_DEFAULT_PORT)) { m_doipLog->error("Failed to setup transport"); return false; } @@ -100,7 +100,7 @@ bool DoIPServer::setupTcpSocket(std::function modelFacto tcpListenerThread(std::move(factory)); }); - m_tcpLog->info("TCP transport ready and listening on port {}", DOIP_SERVER_TCP_PORT); + m_tcpLog->info("TCP transport ready and listening on port {}", DOIP_TCP_DEFAULT_PORT); return true; } @@ -230,6 +230,10 @@ std::unique_ptr DoIPServer::waitForTcpConnection(std::function(); + // Ensure server model uses configured logical address + if (model) { + model->serverAddress = m_config.logicalAddress; + } m_tcpLog->info("Accepted new TCP connection, model {} (factory {})", model->getModelName(), modelFactory ? "provided" : "default"); From 2b592e4fcfcdf112c215c0b02fc47d4e54348126 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 18:47:55 +0100 Subject: [PATCH 11/44] feat: Use header sizes from config --- inc/DoIPMessage.h | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/inc/DoIPMessage.h b/inc/DoIPMessage.h index 4891f13..deae7e0 100644 --- a/inc/DoIPMessage.h +++ b/inc/DoIPMessage.h @@ -1,24 +1,26 @@ #ifndef DOIPMESSAGE_IMPROVED_H #define DOIPMESSAGE_IMPROVED_H -#include -#include -#include -#include -#include -#include "util/AnsiColors.h" -#include "util/ByteArray.h" +#include "DoIPConfig.h" #include "DoIPAddress.h" +#include "DoIPDiagnosticAck.h" #include "DoIPFurtherAction.h" -#include "Vin.h" - +#include "DoIPIdentifiers.h" #include "DoIPIdentifiers.h" #include "DoIPNegativeAck.h" -#include "DoIPDiagnosticAck.h" #include "DoIPPayloadType.h" #include "DoIPRoutingActivationType.h" #include "DoIPSyncStatus.h" +#include "util/AnsiColors.h" +#include "util/ByteArray.h" +#include "Vin.h" + +#include +#include +#include +#include +#include namespace doip { @@ -48,20 +50,6 @@ constexpr uint8_t ISO_13400_2025 = 4; constexpr uint8_t PROTOCOL_VERSION = ISO_13400_2019; constexpr uint8_t PROTOCOL_VERSION_INV = static_cast(~PROTOCOL_VERSION); -/** - * @brief Positive ack for diagnostic message (table 24) - */ -constexpr uint8_t DIAGNOSTIC_MESSAGE_ACK = 0; - -/** - * @brief Size of the DoIP header - */ -constexpr size_t DOIP_HEADER_SIZE = 8; - -/** - * @brief Size of the DoIP diagnostic message header - */ -constexpr size_t DOIP_DIAG_HEADER_SIZE = DOIP_HEADER_SIZE + 4; class DoIPMessage; using OptDoIPMessage = std::optional; From 3e05b7431d360ef034f5157fd0fda2f01b46996d Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Sun, 4 Jan 2026 18:48:55 +0100 Subject: [PATCH 12/44] fix: Add option to pass IP address to client, rename/comment client model callbacks --- examples/minimal/MinimalDoIPClient.cpp | 14 +++++-- inc/DoIPClient.h | 42 ++++++++++++++------ inc/DoIPClientModel.h | 54 +++++++++++++++++++++++--- src/DoIPClient.cpp | 20 +++++----- 4 files changed, 100 insertions(+), 30 deletions(-) diff --git a/examples/minimal/MinimalDoIPClient.cpp b/examples/minimal/MinimalDoIPClient.cpp index 05ec4c1..d0a98cc 100644 --- a/examples/minimal/MinimalDoIPClient.cpp +++ b/examples/minimal/MinimalDoIPClient.cpp @@ -5,7 +5,7 @@ using namespace doip; struct MyDoIPClientModel : public DoIPClientModel { - void routingActivated(DoIPClient& client, bool activated, DoIPAddress logicalAddress) override { + void routingActivationFinished(DoIPClient& client, bool activated, DoIPAddress logicalAddress) override { (void)client; if (activated) { std::cout << "Routing activated with logical address: " << logicalAddress << std::endl; @@ -20,11 +20,19 @@ struct MyDoIPClientModel : public DoIPClientModel { std::cout << "Message sent: " << msg << std::endl; } - bool diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) override { + void diagMessageAcked(DoIPClient& client, DoIPDiagnosticAck ack) override { + (void)client; + std::cout << "Diagnostic message acknowledged with: " << static_cast(ack) << std::endl; + } + + void diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) override { (void)client; std::cout << "Message received: " << msg << std::endl; + } - return false; // return false to indicate quit + void error(DoIPClient& client, const std::string& errorMsg) override { + (void)client; + std::cerr << "Error: " << errorMsg << std::endl; } }; diff --git a/inc/DoIPClient.h b/inc/DoIPClient.h index 643281d..6cb7228 100644 --- a/inc/DoIPClient.h +++ b/inc/DoIPClient.h @@ -18,7 +18,6 @@ namespace doip { -const int _maxDataSize = 64; class DoIPClient { enum class ReceiveState { @@ -30,39 +29,46 @@ class DoIPClient { public: DoIPClient(UniqueDoIPClientModelPtr model = std::make_unique()) : m_model(std::move(model)) { m_receiveBuf.reserve(DOIP_MAXIMUM_MTU); } - [[nodiscard]] bool startTcpConnection(); + [[nodiscard]] bool startTcpConnection(const char *inet_address = "127.0.0.1", uint16_t port = DOIP_TCP_DEFAULT_PORT); [[nodiscard]] bool isTcpConnected() const noexcept { return m_connected.valid(); } [[nodiscard]] bool reconnectServer(); void closeTcpConnection(); - void sendMessage(const DoIPMessage &msg); void startUdpConnection(); void startAnnouncementListener(); void closeUdpConnection(); - ssize_t sendRoutingActivationRequest(); - std::optional receiveRoutingActivationResponse(); - - void receiveUdpMessage(); + // TODO: Make private later + void receiveUdpMessage(); ssize_t sendVehicleIdentificationRequest(const char *inet_address); [[nodiscard]] bool receiveVehicleAnnouncement(); + /** - * Sends a diagnostic message to the server - * @param payload data that will be given to the ecu + * @brief Enqueues a DoIP message to the server + * @param msg DoIP message to send */ - void sendDiagnosticMessage(const ByteArray &payload); + void sendMessage(const DoIPMessage &msg); /** - * Sends a alive check response containing the clients source address to the server + * @brief Enqueues a diagnostic message to the server + * @param payload data that will be given to the ecu */ - ssize_t sendAliveCheckResponse(); + void sendDiagnosticMessage(const ByteArray &payload); + void setSourceAddress(const DoIPAddress &address); void printVehicleInformationResponse(); + // Request the client to quit gracefully + void requestQuit() { + updateReceiveState(ReceiveState::Quit); + m_tcpRunning.store(false); + m_udpRunning.store(false); + } + protected: void updateReceiveState(ReceiveState newState) { if (m_receiveState != newState) { @@ -77,6 +83,7 @@ class DoIPClient { Socket m_tcpSocket, m_udpSocket, m_udpAnnouncementSocket, m_connected; ThreadSafeQueue m_messageQueue; std::atomic m_tcpRunning{false}; + std::atomic m_udpRunning{false}; std::thread m_tcpThread{}; int m_broadcast = 1; struct sockaddr_in m_serverAddress, m_clientAddress, m_announcementAddress; @@ -92,6 +99,16 @@ class DoIPClient { ReceiveState m_receiveState = ReceiveState::WaitForAckOrAliveCheck; + ssize_t sendRoutingActivationRequest(); + std::optional receiveRoutingActivationResponse(); + + + + /** + * Sends a alive check response containing the clients source address to the server + */ + ssize_t sendAliveCheckResponse(); + void tcpThreadFunction(); [[nodiscard]] bool activateRouting(); [[nodiscard]] ssize_t sendDoIPMessage(const DoIPMessage &msg); @@ -104,6 +121,7 @@ class DoIPClient { int emptyMessageCounter = 0; }; + } // namespace doip #endif /* DOIPCLIENT_H */ diff --git a/inc/DoIPClientModel.h b/inc/DoIPClientModel.h index dd3d312..c788e3d 100644 --- a/inc/DoIPClientModel.h +++ b/inc/DoIPClientModel.h @@ -13,34 +13,78 @@ namespace doip { struct DoIPClientModel { virtual ~DoIPClientModel() = default; - // TODO: Add UDP methods - - virtual void routingActivated(DoIPClient& client, bool activated, DoIPAddress logicalAddress = ZERO_ADDRESS) { + /** + * @brief Called when routing activation has finished - either successfully or failed + * + * @param client the DoIP client + * @param activated true if routing was activated successfully, false otherwise + * @param logicalAddress the assigned logical address (if activated) + */ + virtual void routingActivationFinished(DoIPClient& client, bool activated, DoIPAddress logicalAddress = ZERO_ADDRESS) { (void)client; (void)activated; (void)logicalAddress; } + /** + * @brief Called when a diagnostic message has been acknowledged by the server. This + * includes both positive and negative acknowledgments. + * + * @param client the DoIP client + * @param ack the diagnostic acknowledgment received + */ virtual void diagMessageAcked(DoIPClient& client, DoIPDiagnosticAck ack) { (void)client; (void)ack; } - virtual bool diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) { + /** + * @brief Called when a diagnostic message has been received from the server. + * + * @param client the DoIP client + * @param msg the diagnostic message received + */ + virtual void diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) { (void)client; (void)msg; - return false; // return false to indicate quit } + /** + * @brief Called when a DoIP message has been sent to the server. + * + * @param client the DoIP client + * @param msg the DoIP message that was sent + */ virtual void messageSent(DoIPClient& client, const DoIPMessage& msg) { (void)client; (void)msg; } + /** + * @brief Called when an error occurs in the DoIP client. + * + * @param client the DoIP client + * @param errorMsg the error message + */ virtual void error(DoIPClient& client, const std::string& errorMsg) { (void)client; std::cerr << "DoIPClientModel Error: " << errorMsg << std::endl; } + + /** + * @brief Called when a vehicle announcement message is received. + * This callback is only used if the DoIP client is listening for + * vehicle announcements on UDP. + * + * @param client the DoIP client + * @param msg the vehicle announcement message received + * @param fromAddress the IP address the announcement was received from + */ + virtual void vehicleAnnouncementReceived(DoIPClient& client, const DoIPMessage& msg, const std::string &fromAddress) { + (void)client; + (void)msg; + (void)fromAddress; + } }; using UniqueDoIPClientModelPtr = std::unique_ptr; diff --git a/src/DoIPClient.cpp b/src/DoIPClient.cpp index be11565..f4ac554 100644 --- a/src/DoIPClient.cpp +++ b/src/DoIPClient.cpp @@ -11,7 +11,7 @@ using namespace doip; /* *Set up the connection between client and server */ -bool DoIPClient::startTcpConnection() { +bool DoIPClient::startTcpConnection(const char *inet_address, uint16_t port) { int tmpSocket = socket(AF_INET, SOCK_STREAM, 0); if (tmpSocket >= 0) { @@ -19,9 +19,9 @@ bool DoIPClient::startTcpConnection() { m_log->info("Client TCP-Socket created successfully"); bool connectedFlag = false; - const char *ipAddr = "127.0.0.1"; + const char *ipAddr = inet_address; m_serverAddress.sin_family = AF_INET; - m_serverAddress.sin_port = htons(DOIP_UDP_DISCOVERY_PORT); + m_serverAddress.sin_port = htons(port); inet_aton(ipAddr, &(m_serverAddress.sin_addr)); int retries = 3; @@ -35,11 +35,11 @@ bool DoIPClient::startTcpConnection() { if (!activateRouting()) { m_log->error("Routing activation failed - connection closed"); m_connected.close(); - m_model->routingActivated(*this, false, m_logicalAddress); + m_model->routingActivationFinished(*this, false, m_logicalAddress); return false; } - m_model->routingActivated(*this, true, m_logicalAddress); + m_model->routingActivationFinished(*this, true, m_logicalAddress); m_tcpRunning.store(true); m_tcpThread = std::thread(&DoIPClient::tcpThreadFunction, this); @@ -299,7 +299,7 @@ ssize_t DoIPClient::sendAliveCheckResponse() { */ std::optional DoIPClient::receiveMessage() { - ssize_t bytesRead = recv(m_tcpSocket.get(), m_receiveBuf.data(), _maxDataSize, 0); + ssize_t bytesRead = recv(m_tcpSocket.get(), m_receiveBuf.data(), DOIP_MAXIMUM_DIAG_PAYLOAD, 0); if (bytesRead < 0) { m_log->error("Error receiving data from server"); @@ -342,7 +342,7 @@ void DoIPClient::receiveUdpMessage() { setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); int bytesRead; - bytesRead = recvfrom(m_udpSocket.get(), m_receiveBuf.data(), _maxDataSize, 0, reinterpret_cast(&m_clientAddress), &length); + bytesRead = recvfrom(m_udpSocket.get(), m_receiveBuf.data(), DOIP_MAXIMUM_MTU, 0, reinterpret_cast(&m_clientAddress), &length); if (bytesRead < 0) { if (errno == EAGAIN) { @@ -372,13 +372,13 @@ bool DoIPClient::receiveVehicleAnnouncement() { m_log->debug("Listening for Vehicle Announcements on port {}", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); - // Set socket to non-blocking mode for timeout + // Set socket timeout for announcement reception struct timeval timeout; - timeout.tv_sec = 2; // 2 second timeout + timeout.tv_sec = 5; // increase to 5 seconds for robustness in CI timeout.tv_usec = 0; setsockopt(m_udpAnnouncementSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - bytesRead = recvfrom(m_udpAnnouncementSocket.get(), m_receiveBuf.data(), _maxDataSize, 0, + bytesRead = recvfrom(m_udpAnnouncementSocket.get(), m_receiveBuf.data(), DOIP_MAXIMUM_MTU, 0, reinterpret_cast(&m_announcementAddress), &length); if (bytesRead < 0) { if (errno == EAGAIN) { From 600f3c3e696c3ceb2e516033aa312290830403a9 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Mon, 5 Jan 2026 11:29:07 +0100 Subject: [PATCH 13/44] feat: Enhance DoIP client model, send multiple UDS messages in minimal client --- examples/minimal/MinimalDoIPClient.cpp | 46 +++++++++++++++++++++----- inc/DoIPClient.h | 4 ++- inc/DoIPClientModel.h | 9 ++++- src/DoIPClient.cpp | 9 +++-- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/examples/minimal/MinimalDoIPClient.cpp b/examples/minimal/MinimalDoIPClient.cpp index d0a98cc..d1009ed 100644 --- a/examples/minimal/MinimalDoIPClient.cpp +++ b/examples/minimal/MinimalDoIPClient.cpp @@ -4,38 +4,64 @@ using namespace doip; -struct MyDoIPClientModel : public DoIPClientModel { - void routingActivationFinished(DoIPClient& client, bool activated, DoIPAddress logicalAddress) override { +const std::vector UDS_MESSAGES = { + {0x10, 0x02}, // open diag session + {0x22, 0xF1, 0x90}, // read VIN + {0x10, 0x01}, // close diag session +}; + +/** + * @brief Example DoIP client model that handles callbacks. + * + */ +struct MyDoIPClientModel : public DoIPClientModel { + void routingActivationFinished(DoIPClient &client, bool activated, DoIPAddress logicalAddress) override { (void)client; if (activated) { std::cout << "Routing activated with logical address: " << logicalAddress << std::endl; + sendNextUdsMessage(client); } else { std::cout << "Routing activation failed" << std::endl; } - client.sendDiagnosticMessage({0x10, 0x02}); // Example UDS message } - void messageSent(DoIPClient& client, const DoIPMessage& msg) override { + void messageSent(DoIPClient &client, const DoIPMessage &msg) override { (void)client; std::cout << "Message sent: " << msg << std::endl; } - void diagMessageAcked(DoIPClient& client, DoIPDiagnosticAck ack) override { + void diagMessageAcked(DoIPClient &client, DoIPDiagnosticAck ack) override { (void)client; std::cout << "Diagnostic message acknowledged with: " << static_cast(ack) << std::endl; } - void diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) override { + CallbackResult diagMessageReceived(DoIPClient &client, const DoIPMessage &msg) override { (void)client; std::cout << "Message received: " << msg << std::endl; + std::cout << "--> index: " << udsMessageIndex << std::endl; + return sendNextUdsMessage(client); } - void error(DoIPClient& client, const std::string& errorMsg) override { + void error(DoIPClient &client, const std::string &errorMsg) override { (void)client; std::cerr << "Error: " << errorMsg << std::endl; } -}; + private: + size_t udsMessageIndex = 0; + + CallbackResult sendNextUdsMessage(DoIPClient &client) { + if (udsMessageIndex < UDS_MESSAGES.size()) { + std::cout << "Sending UDS message " << (udsMessageIndex + 1) << "/" << UDS_MESSAGES.size() << std::endl; + client.sendDiagnosticMessage(UDS_MESSAGES[udsMessageIndex]); + } + // wait until the last message has been received + udsMessageIndex++; + return (udsMessageIndex <= UDS_MESSAGES.size()) ? CallbackResult::Continue : CallbackResult::Stop; + } + + // 0 -> 1 +}; int main() { DoIPClient client(std::make_unique()); @@ -45,7 +71,9 @@ int main() { } // Wait a bit for communication to complete - std::this_thread::sleep_for(std::chrono::seconds(3)); + while (client.isTcpRunning()) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } // Clean up: close the connection (this will join the thread) client.closeTcpConnection(); diff --git a/inc/DoIPClient.h b/inc/DoIPClient.h index 6cb7228..c7123a1 100644 --- a/inc/DoIPClient.h +++ b/inc/DoIPClient.h @@ -31,12 +31,14 @@ class DoIPClient { [[nodiscard]] bool startTcpConnection(const char *inet_address = "127.0.0.1", uint16_t port = DOIP_TCP_DEFAULT_PORT); - [[nodiscard]] bool isTcpConnected() const noexcept { return m_connected.valid(); } + [[nodiscard]] bool isTcpRunning() const noexcept { return m_tcpRunning.load(); } + [[nodiscard]] bool reconnectServer(); void closeTcpConnection(); void startUdpConnection(); + [[nodiscard]] bool isUdpRunning() const noexcept { return m_udpRunning.load(); } void startAnnouncementListener(); void closeUdpConnection(); diff --git a/inc/DoIPClientModel.h b/inc/DoIPClientModel.h index c788e3d..c4fa937 100644 --- a/inc/DoIPClientModel.h +++ b/inc/DoIPClientModel.h @@ -9,6 +9,11 @@ namespace doip { class DoIPClient; + enum class CallbackResult { + Continue, + Stop, + }; + struct DoIPClientModel { virtual ~DoIPClientModel() = default; @@ -43,10 +48,12 @@ namespace doip { * * @param client the DoIP client * @param msg the diagnostic message received + * @return CallbackResult indicating whether to continue processing or stop. By default it returns Stop. */ - virtual void diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) { + virtual CallbackResult diagMessageReceived(DoIPClient& client, const DoIPMessage& msg) { (void)client; (void)msg; + return CallbackResult::Stop; } /** diff --git a/src/DoIPClient.cpp b/src/DoIPClient.cpp index f4ac554..46b7a71 100644 --- a/src/DoIPClient.cpp +++ b/src/DoIPClient.cpp @@ -202,10 +202,15 @@ void DoIPClient::reactToMessage(const DoIPMessage &msg) { } } else if (m_receiveState == ReceiveState::WaitForDiagnosticMessage) { // Process diagnostic message + auto newState = ReceiveState::WaitForAckOrAliveCheck; auto payloadType = msg.getPayloadType(); switch (payloadType) { case DoIPPayloadType::DiagnosticMessage: { - m_model->diagMessageReceived(*this, msg); + auto result = m_model->diagMessageReceived(*this, msg); + if (result == CallbackResult::Stop) { + newState = ReceiveState::Quit; + m_tcpRunning.store(false); + } break; } default: @@ -213,7 +218,7 @@ void DoIPClient::reactToMessage(const DoIPMessage &msg) { m_model->error(*this, "Received unexpected message type"); break; } - updateReceiveState(ReceiveState::WaitForAckOrAliveCheck); + updateReceiveState(newState); } } From f560728fb0df9639ab68c3879d90ac64bac4b3b1 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Mon, 5 Jan 2026 16:42:06 +0100 Subject: [PATCH 14/44] WIP: DoIP TP layer setup weird, UDP reception is missing --- examples/CMakeLists.txt | 1 + examples/discover/CMakeLists.txt | 8 + examples/discover/DoIPUdpServer.cpp | 41 +++ examples/discover/discover-client.py | 431 +++++++++++++++++++++++++ examples/minimal/MinimalDoIPClient.cpp | 34 +- inc/DoIPClient.h | 16 +- inc/DoIPPayloadType.h | 1 + src/DoIPClient.cpp | 327 ++++++++++--------- src/DoIPServer.cpp | 6 + src/tp/TcpServerTransport.cpp | 8 +- 10 files changed, 697 insertions(+), 176 deletions(-) create mode 100644 examples/discover/CMakeLists.txt create mode 100644 examples/discover/DoIPUdpServer.cpp create mode 100644 examples/discover/discover-client.py diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 7c48e7e..2d33785 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,4 +4,5 @@ if (LINUX) endif() add_subdirectory(minimal) +add_subdirectory(discover) diff --git a/examples/discover/CMakeLists.txt b/examples/discover/CMakeLists.txt new file mode 100644 index 0000000..6f4c6e5 --- /dev/null +++ b/examples/discover/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(DoIPUdpServer DoIPUdpServer.cpp) +target_link_libraries(DoIPUdpServer + PRIVATE + ${DOIP_SERVER_LIB_NAME} +) + +configure_file(discover-client.py ${CMAKE_CURRENT_BINARY_DIR}/discover-client.py COPYONLY) +configure_file(discover-client.py ${CMAKE_BINARY_DIR}/discover-client.py COPYONLY) \ No newline at end of file diff --git a/examples/discover/DoIPUdpServer.cpp b/examples/discover/DoIPUdpServer.cpp new file mode 100644 index 0000000..51a8120 --- /dev/null +++ b/examples/discover/DoIPUdpServer.cpp @@ -0,0 +1,41 @@ +#include "DoIPServer.h" +#include "uds/UdsServerModel.h" + +using namespace doip; +using doip::uds::UdsServerModel; + +int main(int argc, char *argv[]) { + (void)argc; + (void)argv; + + ServerConfig cfg; + cfg.loopback = true; // For testing, use loopback announcements + + auto server = std::make_unique(cfg); + auto console = spdlog::stdout_color_mt("doip-udp-server"); + + // for discovery check we use relaxed announcement settings + server->setAnnounceInterval(500); // Send announcements every 500ms for faster discovery + server->setAnnounceNum(100); // Send 100 announcements = 50 seconds of announcements (enough for parallel test execution) + + // Start announcement thread after sockets are bound + // Set up TCP first to ensure transport creates/binds both TCP and UDP sockets + if (!server->setupTcpSocket()) { + console->critical("Failed to set up TCP socket"); + return 1; + } + + // Start announcement thread after sockets are bound + if (!server->setupUdpSocket()) { + console->critical("Failed to set up UDP announcements"); + server->stop(); // Clean up before exiting + return 1; + } + + console->info("DoIP UDP Server is running"); + + while (server->isUdpRunning()) { + // Main loop can perform other tasks or just sleep + std::this_thread::sleep_for(std::chrono::seconds(1)); + } +} \ No newline at end of file diff --git a/examples/discover/discover-client.py b/examples/discover/discover-client.py new file mode 100644 index 0000000..1b67cf6 --- /dev/null +++ b/examples/discover/discover-client.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +""" +DoIP Test Client for ISO 13400 +Tests UDP vehicle discovery and TCP diagnostic communication +""" + +import socket +import struct +import sys +import time +from typing import Optional, Tuple + +# DoIP Protocol Constants +DOIP_PROTOCOL_VERSION = 0x03 +DOIP_INVERSE_PROTOCOL_VERSION = 0xFC + +# UDP Ports (per ISO 13400) +# - DoIP Entity (server) listens on 13400 for requests +# - Test Equipment (client) listens on 13401 for announcements/responses +DOIP_UDP_ENTITY_PORT = 13400 +DOIP_UDP_TEST_EQUIPMENT_PORT = 13401 + +# Payload Types +DOIP_VEHICLE_IDENTIFICATION_REQUEST = 0x0001 +DOIP_VEHICLE_IDENTIFICATION_RESPONSE_EID = 0x0004 +DOIP_VEHICLE_ANNOUNCEMENT = 0x0004 +DOIP_ROUTING_ACTIVATION_REQUEST = 0x0005 +DOIP_ROUTING_ACTIVATION_RESPONSE = 0x0006 +DOIP_DIAGNOSTIC_MESSAGE = 0x8001 +DOIP_DIAGNOSTIC_MESSAGE_ACK = 0x8002 +DOIP_DIAGNOSTIC_MESSAGE_NACK = 0x8003 + +# Routing Activation Types +ROUTING_ACTIVATION_DEFAULT = 0x00 +ROUTING_ACTIVATION_WWH_OBD = 0x01 + +# UDS Service IDs +UDS_READ_DATA_BY_ID = 0x22 + +# Timeouts +# Announcements are often sent at multi-second intervals; use generous defaults +UDP_ANNOUNCEMENT_TIMEOUT = 10.0 # seconds +UDP_RESPONSE_TIMEOUT = 5.0 # seconds +TCP_RESPONSE_TIMEOUT = 5.0 # seconds + + +class DoIPHeader: + """DoIP Generic Header""" + FORMAT = "!BBHI" + SIZE = 8 + + def __init__(self, payload_type: int, payload_length: int): + self.protocol_version = DOIP_PROTOCOL_VERSION + self.inverse_protocol_version = DOIP_INVERSE_PROTOCOL_VERSION + self.payload_type = payload_type + self.payload_length = payload_length + + def pack(self) -> bytes: + return struct.pack( + self.FORMAT, + self.protocol_version, + self.inverse_protocol_version, + self.payload_type, + self.payload_length + ) + + @staticmethod + def unpack(data: bytes) -> 'DoIPHeader': + if len(data) < DoIPHeader.SIZE: + raise ValueError("Data too short for DoIP header") + + proto_ver, inv_proto_ver, payload_type, payload_length = struct.unpack( + DoIPHeader.FORMAT, data[:DoIPHeader.SIZE] + ) + + header = DoIPHeader(payload_type, payload_length) + header.protocol_version = proto_ver + header.inverse_protocol_version = inv_proto_ver + return header + + +class DoIPClient: + def __init__(self, source_address: int = 0x0E80): + self.source_address = source_address + self.target_address: Optional[int] = None + self.server_ip: Optional[str] = None + self.tcp_port: int = 13400 + self.tcp_socket: Optional[socket.socket] = None + + def listen_for_announcement(self, timeout: float = UDP_ANNOUNCEMENT_TIMEOUT) -> bool: + """Listen for DoIP vehicle announcements on UDP""" + print(f"Listening for vehicle announcements (timeout: {timeout}s) on fixed port {DOIP_UDP_TEST_EQUIPMENT_PORT}...") + + udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if hasattr(socket, "SO_REUSEPORT"): + try: + udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except OSError: + pass + + try: + # Announcements are sent by DoIP entities to port 13401 + udp_socket.bind(('', DOIP_UDP_TEST_EQUIPMENT_PORT)) + # Poll in short intervals up to the provided timeout + deadline = time.time() + timeout + udp_socket.settimeout(0.5) + while time.time() < deadline: + try: + data, addr = udp_socket.recvfrom(4096) + except socket.timeout: + continue + + print(f"Received announcement from {addr[0]}:{addr[1]}") + + if self._parse_vehicle_announcement(data, addr[0]): + return True + + except socket.timeout: + print("No announcement received within timeout") + return False + except Exception as e: + print(f"Error listening for announcement: {e}") + return False + finally: + udp_socket.close() + + return False + + def send_vehicle_identification_request(self, broadcast_addr: str = '255.255.255.255') -> bool: + """Send vehicle identification request via UDP broadcast""" + print("Sending vehicle identification request...") + + udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # Prefer binding to 13401 so responses arrive on the standard test equipment port + udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if hasattr(socket, "SO_REUSEPORT"): + try: + udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except OSError: + pass + bound_to_13401 = False + try: + udp_socket.bind(('', DOIP_UDP_TEST_EQUIPMENT_PORT)) + bound_to_13401 = True + except Exception as e: + print(f"Warning: bind to UDP {DOIP_UDP_TEST_EQUIPMENT_PORT} failed ({e}); using ephemeral source port for send.") + + udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + udp_socket.settimeout(UDP_RESPONSE_TIMEOUT) + + # Create a dedicated receive socket bound to 13401 if the send socket couldn't bind + recv_socket = None + if not bound_to_13401: + try: + recv_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if hasattr(socket, "SO_REUSEPORT"): + try: + recv_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except OSError: + pass + recv_socket.bind(('', DOIP_UDP_TEST_EQUIPMENT_PORT)) + except Exception as e: + print(f"Error: unable to bind a receive socket to UDP {DOIP_UDP_TEST_EQUIPMENT_PORT} ({e}). Announcements/responses to {DOIP_UDP_TEST_EQUIPMENT_PORT} will not be received.") + # Fall back to using the send socket for any responses (ephemeral port) + recv_socket = udp_socket + + try: + # Build vehicle identification request (no payload) + header = DoIPHeader(DOIP_VEHICLE_IDENTIFICATION_REQUEST, 0) + message = header.pack() + + # Requests go to the DoIP Entity port 13400 + udp_socket.sendto(message, (broadcast_addr, DOIP_UDP_ENTITY_PORT)) + print(f"Request sent to {broadcast_addr}:{DOIP_UDP_ENTITY_PORT}") + + # Wait for response(s) up to timeout window + deadline = time.time() + UDP_RESPONSE_TIMEOUT + (recv_socket or udp_socket).settimeout(0.5) + while time.time() < deadline: + try: + data, addr = (recv_socket or udp_socket).recvfrom(4096) + except socket.timeout: + continue + + print(f"Received response from {addr[0]}:{addr[1]}") + if self._parse_vehicle_announcement(data, addr[0]): + return True + + except socket.timeout: + print("No response received to identification request") + return False + except Exception as e: + print(f"Error during identification request: {e}") + return False + finally: + udp_socket.close() + if recv_socket and recv_socket is not udp_socket: + recv_socket.close() + + return False + + def _parse_vehicle_announcement(self, data: bytes, server_ip: str) -> bool: + """Parse vehicle announcement/identification response""" + try: + header = DoIPHeader.unpack(data) + + if header.payload_type not in [DOIP_VEHICLE_ANNOUNCEMENT, DOIP_VEHICLE_IDENTIFICATION_RESPONSE_EID]: + print(f"Unexpected payload type: 0x{header.payload_type:04X}") + return False + + payload = data[DoIPHeader.SIZE:] + + if len(payload) < 32: # Minimum length for vehicle announcement + print("Payload too short for vehicle announcement") + return False + + # Parse VIN, Logical Address, EID, GID + vin = payload[0:17].decode('ascii', errors='ignore') + logical_address = struct.unpack("!H", payload[17:19])[0] + eid = payload[19:25].hex() + gid = payload[25:31].hex() + + print(f" VIN: {vin}") + print(f" Logical Address: 0x{logical_address:04X}") + print(f" EID: {eid}") + print(f" GID: {gid}") + + self.server_ip = server_ip + self.target_address = logical_address + + # Further Action Byte (if present) + if len(payload) > 31: + further_action = payload[31] + print(f" Further Action: 0x{further_action:02X}") + + return True + + except Exception as e: + print(f"Error parsing vehicle announcement: {e}") + return False + + def connect_tcp(self) -> bool: + """Open TCP connection to DoIP server""" + if not self.server_ip or not self.target_address: + print("No server IP or target address available") + return False + + print(f"\nConnecting to {self.server_ip}:{self.tcp_port}...") + + try: + self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.tcp_socket.settimeout(TCP_RESPONSE_TIMEOUT) + self.tcp_socket.connect((self.server_ip, self.tcp_port)) + print("TCP connection established") + return True + except Exception as e: + print(f"Failed to connect: {e}") + return False + + def send_routing_activation(self) -> bool: + """Send routing activation request""" + if not self.tcp_socket: + print("No TCP connection") + return False + + print("\nSending routing activation request...") + + try: + # Payload: Source Address (2) + Activation Type (1) + Reserved (4) + payload = struct.pack("!HBI", self.source_address, ROUTING_ACTIVATION_DEFAULT, 0) + header = DoIPHeader(DOIP_ROUTING_ACTIVATION_REQUEST, len(payload)) + message = header.pack() + payload + + self.tcp_socket.sendall(message) + + # Wait for response + response = self.tcp_socket.recv(4096) + header = DoIPHeader.unpack(response) + + if header.payload_type != DOIP_ROUTING_ACTIVATION_RESPONSE: + print(f"Unexpected response type: 0x{header.payload_type:04X}") + return False + + payload = response[DoIPHeader.SIZE:] + tester_addr, entity_addr, response_code = struct.unpack("!HHB", payload[:5]) + + print(f" Response Code: 0x{response_code:02X}") + print(f" Tester Address: 0x{tester_addr:04X}") + print(f" Entity Address: 0x{entity_addr:04X}") + + if response_code == 0x10: # Successfully activated + print(" Status: Successfully activated") + return True + else: + print(f" Status: Activation failed") + return False + + except Exception as e: + print(f"Error during routing activation: {e}") + return False + + def send_uds_rdbi(self, did: int = 0xF190) -> bool: + """Send UDS Read Data By Identifier request""" + if not self.tcp_socket or not self.target_address: + print("No TCP connection or target address") + return False + + print(f"\nSending UDS RDBI request for DID 0x{did:04X}...") + + try: + # UDS message: Service ID + DID + uds_message = struct.pack("!BH", UDS_READ_DATA_BY_ID, did) + + # DoIP Diagnostic Message: Source Addr + Target Addr + UDS Data + payload = struct.pack("!HH", self.source_address, self.target_address) + uds_message + header = DoIPHeader(DOIP_DIAGNOSTIC_MESSAGE, len(payload)) + message = header.pack() + payload + + self.tcp_socket.sendall(message) + + # Wait for diagnostic message positive/negative acknowledgement + response = self.tcp_socket.recv(4096) + header = DoIPHeader.unpack(response) + + if header.payload_type == DOIP_DIAGNOSTIC_MESSAGE_NACK: + payload = response[DoIPHeader.SIZE:] + nack_code = payload[4] if len(payload) > 4 else 0 + print(f" Diagnostic message NACK: 0x{nack_code:02X}") + return False + + # Wait for actual UDS response + if header.payload_type != DOIP_DIAGNOSTIC_MESSAGE: + response += self.tcp_socket.recv(4096) + # Find diagnostic message in response + if len(response) < DoIPHeader.SIZE: + print("Response too short") + return False + header = DoIPHeader.unpack(response) + + if header.payload_type == DOIP_DIAGNOSTIC_MESSAGE: + payload = response[DoIPHeader.SIZE:] + source_addr, target_addr = struct.unpack("!HH", payload[:4]) + uds_response = payload[4:] + + print(f" Source: 0x{source_addr:04X}, Target: 0x{target_addr:04X}") + print(f" UDS Response: {uds_response.hex()}") + + if len(uds_response) > 0: + service_id = uds_response[0] + if service_id == 0x62: # Positive response + print(f" Service: Positive Response (0x62)") + if len(uds_response) >= 3: + resp_did = struct.unpack("!H", uds_response[1:3])[0] + data = uds_response[3:] + print(f" DID: 0x{resp_did:04X}") + print(f" Data: {data.hex()}") + print(f" Data (ASCII): {data.decode('ascii', errors='ignore')}") + return True + elif service_id == 0x7F: # Negative response + print(f" Service: Negative Response (0x7F)") + if len(uds_response) >= 3: + print(f" NRC: 0x{uds_response[2]:02X}") + return False + else: + print(f"Unexpected response type: 0x{header.payload_type:04X}") + return False + + except Exception as e: + print(f"Error during UDS RDBI: {e}") + return False + + return False + + def close_tcp(self): + """Close TCP connection""" + if self.tcp_socket: + print("\nClosing TCP connection...") + self.tcp_socket.close() + self.tcp_socket = None + + +def main(): + print("DoIP Test Client") + print("=" * 50) + + client = DoIPClient() + + # Step 1: Listen for vehicle announcements + announcement_received = client.listen_for_announcement() + + # Step 2: If no announcement, send identification request + if not announcement_received: + print("\nNo announcement received, sending identification request...") + identification_received = client.send_vehicle_identification_request() + + if not identification_received: + print("\nERROR: Neither announcement nor identification response received") + sys.exit(1) + + print("\n" + "=" * 50) + print("Vehicle discovered successfully!") + print("=" * 50) + + # Step 3: Open TCP connection + if not client.connect_tcp(): + print("\nERROR: Failed to establish TCP connection") + sys.exit(1) + + # Step 4: Send routing activation request + if not client.send_routing_activation(): + print("\nERROR: Routing activation failed") + client.close_tcp() + sys.exit(1) + + # Step 5: Send UDS RDBI request + if not client.send_uds_rdbi(0xF190): + print("\nWARNING: UDS RDBI request failed or returned negative response") + + # Step 6: Close TCP connection + client.close_tcp() + + print("\n" + "=" * 50) + print("Test completed successfully!") + print("=" * 50) + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/minimal/MinimalDoIPClient.cpp b/examples/minimal/MinimalDoIPClient.cpp index d1009ed..349e840 100644 --- a/examples/minimal/MinimalDoIPClient.cpp +++ b/examples/minimal/MinimalDoIPClient.cpp @@ -12,9 +12,15 @@ const std::vector UDS_MESSAGES = { /** * @brief Example DoIP client model that handles callbacks. - * + * It illustrates the usage of the DoIPClientModel interface. */ struct MyDoIPClientModel : public DoIPClientModel { + /** + * @brief Callback when routing activation has finished. + * @param client the DoIP client + * @param activated true if routing was activated successfully, false otherwise + * @param logicalAddress the assigned logical address (if activated) + */ void routingActivationFinished(DoIPClient &client, bool activated, DoIPAddress logicalAddress) override { (void)client; if (activated) { @@ -25,20 +31,37 @@ struct MyDoIPClientModel : public DoIPClientModel { } } + /** + * @brief Callback when a DoIP message has been sent to the server. + * @param client the DoIP client + * @param msg the DoIP message that was sent + */ void messageSent(DoIPClient &client, const DoIPMessage &msg) override { (void)client; std::cout << "Message sent: " << msg << std::endl; } + /** + * @brief Callback when a diagnostic message has been acknowledged by the server. + * Payload types for acknowledgments are DiagnosticMessagePositiveAck (0x8002) and DiagnosticMessageNegativeAck (0x8003). + * @param client the DoIP client + * @param ack the diagnostic acknowledgment received + */ void diagMessageAcked(DoIPClient &client, DoIPDiagnosticAck ack) override { (void)client; std::cout << "Diagnostic message acknowledged with: " << static_cast(ack) << std::endl; } + /** + * @brief Callback when a diagnostic message has (0x8001) been received from the server. + * + * @param client the DoIP client + * @param msg the diagnostic message received + * @return CallbackResult + */ CallbackResult diagMessageReceived(DoIPClient &client, const DoIPMessage &msg) override { (void)client; std::cout << "Message received: " << msg << std::endl; - std::cout << "--> index: " << udsMessageIndex << std::endl; return sendNextUdsMessage(client); } @@ -52,30 +75,27 @@ struct MyDoIPClientModel : public DoIPClientModel { CallbackResult sendNextUdsMessage(DoIPClient &client) { if (udsMessageIndex < UDS_MESSAGES.size()) { - std::cout << "Sending UDS message " << (udsMessageIndex + 1) << "/" << UDS_MESSAGES.size() << std::endl; client.sendDiagnosticMessage(UDS_MESSAGES[udsMessageIndex]); } // wait until the last message has been received udsMessageIndex++; return (udsMessageIndex <= UDS_MESSAGES.size()) ? CallbackResult::Continue : CallbackResult::Stop; } - - // 0 -> 1 }; int main() { + // Assign the client model DoIPClient client(std::make_unique()); + // Setup TCP connection to DoIP server if (!client.startTcpConnection()) { std::cerr << "Failed to start TCP connection to DoIP server" << std::endl; return 1; } - // Wait a bit for communication to complete while (client.isTcpRunning()) { std::this_thread::sleep_for(std::chrono::seconds(1)); } - // Clean up: close the connection (this will join the thread) client.closeTcpConnection(); return 0; diff --git a/inc/DoIPClient.h b/inc/DoIPClient.h index c7123a1..7106d3f 100644 --- a/inc/DoIPClient.h +++ b/inc/DoIPClient.h @@ -18,7 +18,6 @@ namespace doip { - class DoIPClient { enum class ReceiveState { WaitForAckOrAliveCheck, @@ -36,19 +35,17 @@ class DoIPClient { [[nodiscard]] bool reconnectServer(); void closeTcpConnection(); - void startUdpConnection(); [[nodiscard]] bool isUdpRunning() const noexcept { return m_udpRunning.load(); } void startAnnouncementListener(); void closeUdpConnection(); // TODO: Make private later - void receiveUdpMessage(); + void receiveUdpMessage(); ssize_t sendVehicleIdentificationRequest(const char *inet_address); [[nodiscard]] bool receiveVehicleAnnouncement(); - /** * @brief Enqueues a DoIP message to the server * @param msg DoIP message to send @@ -82,7 +79,8 @@ class DoIPClient { private: UniqueDoIPClientModelPtr m_model; ByteArray m_receiveBuf; - Socket m_tcpSocket, m_udpSocket, m_udpAnnouncementSocket, m_connected; + Socket m_tcpSocket, m_tcpClientSocket; + Socket m_udpSocket, m_udpVehicleAnnSocket; ThreadSafeQueue m_messageQueue; std::atomic m_tcpRunning{false}; std::atomic m_udpRunning{false}; @@ -101,12 +99,10 @@ class DoIPClient { ReceiveState m_receiveState = ReceiveState::WaitForAckOrAliveCheck; - ssize_t sendRoutingActivationRequest(); + ssize_t sendRoutingActivationRequest(); std::optional receiveRoutingActivationResponse(); - - - /** + /** * Sends a alive check response containing the clients source address to the server */ ssize_t sendAliveCheckResponse(); @@ -118,12 +114,12 @@ class DoIPClient { void reactToMessage(const DoIPMessage &msg); + void udpThreadFunction(); void parseVehicleIdentificationResponse(const DoIPMessage &msg); int emptyMessageCounter = 0; }; - } // namespace doip #endif /* DOIPCLIENT_H */ diff --git a/inc/DoIPPayloadType.h b/inc/DoIPPayloadType.h index 2ef12e5..0fe643f 100644 --- a/inc/DoIPPayloadType.h +++ b/inc/DoIPPayloadType.h @@ -15,6 +15,7 @@ namespace doip { * * This enumeration contains all payload types used in the DoIP protocol. * The values correspond to the message types specified in ISO 13400-2. + * See table 17. */ enum class DoIPPayloadType : uint16_t { /** diff --git a/src/DoIPClient.cpp b/src/DoIPClient.cpp index 46b7a71..5a8746a 100644 --- a/src/DoIPClient.cpp +++ b/src/DoIPClient.cpp @@ -1,5 +1,6 @@ #include "DoIPClient.h" #include "DoIPMessage.h" +#include "DoIPTimes.h" #include "DoIPPayloadType.h" #include "util/Logger.h" #include // for errno @@ -8,50 +9,9 @@ using namespace doip; -/* - *Set up the connection between client and server - */ -bool DoIPClient::startTcpConnection(const char *inet_address, uint16_t port) { - int tmpSocket = socket(AF_INET, SOCK_STREAM, 0); - - if (tmpSocket >= 0) { - m_tcpSocket.reset(tmpSocket); - m_log->info("Client TCP-Socket created successfully"); - - bool connectedFlag = false; - const char *ipAddr = inet_address; - m_serverAddress.sin_family = AF_INET; - m_serverAddress.sin_port = htons(port); - inet_aton(ipAddr, &(m_serverAddress.sin_addr)); - - int retries = 3; - while (!connectedFlag && retries > 0) { - int tmpConnSocket = connect(m_tcpSocket.get(), reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); - if (tmpConnSocket != -1) { - m_connected.reset(tmpConnSocket); - connectedFlag = true; - m_log->info("Connection to server established"); - - if (!activateRouting()) { - m_log->error("Routing activation failed - connection closed"); - m_connected.close(); - m_model->routingActivationFinished(*this, false, m_logicalAddress); - return false; - } - - m_model->routingActivationFinished(*this, true, m_logicalAddress); - - m_tcpRunning.store(true); - m_tcpThread = std::thread(&DoIPClient::tcpThreadFunction, this); - return true; - } - std::this_thread::sleep_for(std::chrono::seconds(1)); - retries--; - } - } - - return false; -} +// ------------------------------------------------------------------- +// UDP Connection and Thread Handling +// ------------------------------------------------------------------- void DoIPClient::startUdpConnection() { @@ -82,16 +42,16 @@ void DoIPClient::startAnnouncementListener() { int tmpUdpAnnouncementSocket = socket(AF_INET, SOCK_DGRAM, 0); if (tmpUdpAnnouncementSocket >= 0) { - m_udpAnnouncementSocket.reset(tmpUdpAnnouncementSocket); + m_udpVehicleAnnSocket.reset(tmpUdpAnnouncementSocket); m_log->info("Client-Announcement-Socket created successfully"); // Allow socket reuse for broadcast int reuse = 1; - setsockopt(m_udpAnnouncementSocket.get(), SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + setsockopt(m_udpVehicleAnnSocket.get(), SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); // Enable broadcast reception int broadcast = 1; - if (setsockopt(m_udpAnnouncementSocket.get(), SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { + if (setsockopt(m_udpVehicleAnnSocket.get(), SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { m_log->error("Failed to enable broadcast reception: {}", strerror(errno)); } else { m_log->info("Broadcast reception enabled for announcements"); @@ -102,7 +62,7 @@ void DoIPClient::startAnnouncementListener() { m_announcementAddress.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to port 13401 for Vehicle Announcements - if (bind(m_udpAnnouncementSocket.get(), reinterpret_cast(&m_announcementAddress), sizeof(m_announcementAddress)) < 0) { + if (bind(m_udpVehicleAnnSocket.get(), reinterpret_cast(&m_announcementAddress), sizeof(m_announcementAddress)) < 0) { m_log->error("Failed to bind announcement socket to port {}: {}", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT, strerror(errno)); } else { m_log->info("Announcement socket bound to port {} successfully", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); @@ -112,6 +72,168 @@ void DoIPClient::startAnnouncementListener() { } } +void DoIPClient::udpThreadFunction() { + while (m_udpRunning.load()) { + receiveUdpMessage(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} + +void DoIPClient::closeUdpConnection() { + m_udpSocket.close(); + if (m_udpVehicleAnnSocket.get() >= 0) { + m_udpVehicleAnnSocket.close(); + } +} + +void DoIPClient::receiveUdpMessage() { + + unsigned int length = sizeof(m_clientAddress); + + // Set socket to timeout after 3 seconds + struct timeval timeout; + timeout.tv_sec = static_cast(doip::times::client::UdpMessageTimeout.count() / 1000); + timeout.tv_usec = 0; + setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + int bytesRead; + bytesRead = recvfrom(m_udpSocket.get(), m_receiveBuf.data(), DOIP_MAXIMUM_MTU, 0, reinterpret_cast(&m_clientAddress), &length); + + if (bytesRead < 0) { + if (errno == EAGAIN) { + m_log->warn("Timeout waiting for UDP response"); + } else { + m_log->error("Error receiving UDP message: {}", strerror(errno)); + } + return; + } + + m_log->info("Received {} bytes from UDP", bytesRead); + + auto optMmsg = DoIPMessage::tryParse(m_receiveBuf.data(), static_cast(bytesRead)); + if (!optMmsg.has_value()) { + m_log->error("Failed to parse DoIP message from UDP data"); + return; + } + + DoIPMessage msg = optMmsg.value(); + + m_log->info("RX: {}", fmt::streamed(msg)); +} + +bool DoIPClient::receiveVehicleAnnouncement() { + unsigned int length = sizeof(m_announcementAddress); + int bytesRead; + + m_log->debug("Listening for Vehicle Announcements on port {}", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); + + // Set socket timeout for announcement reception + struct timeval timeout; + timeout.tv_sec = 5; // increase to 5 seconds for robustness in CI + timeout.tv_usec = 0; + setsockopt(m_udpVehicleAnnSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + bytesRead = recvfrom(m_udpVehicleAnnSocket.get(), m_receiveBuf.data(), DOIP_MAXIMUM_MTU, 0, + reinterpret_cast(&m_announcementAddress), &length); + if (bytesRead < 0) { + if (errno == EAGAIN) { + m_log->warn("Timeout waiting for Vehicle Announcement"); + } else { + m_log->error("Error receiving Vehicle Announcement: {}", strerror(errno)); + } + return false; + } + + auto optMsg = DoIPMessage::tryParse(m_receiveBuf.data(), static_cast(bytesRead)); + if (!optMsg.has_value()) { + m_log->error("Failed to parse Vehicle Announcement message"); + return false; + } + + DoIPMessage msg = optMsg.value(); + // Parse and display the announcement information + if (msg.getPayloadType() == DoIPPayloadType::VehicleIdentificationResponse) { + m_log->info("Vehicle Announcement received: {}", fmt::streamed(msg)); + parseVehicleIdentificationResponse(msg); + return true; + } + return false; +} + +ssize_t DoIPClient::sendVehicleIdentificationRequest(const char *inet_address) { + + int setAddressError = inet_aton(inet_address, &(m_serverAddress.sin_addr)); + + if (setAddressError != 0) { + m_log->info("Address set successfully"); + } else { + m_log->error("Could not set address. Try again"); + } + + int socketError = setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_BROADCAST, &m_broadcast, sizeof(m_broadcast)); + + if (socketError == 0) { + m_log->info("Broadcast Option set successfully"); + } + + DoIPMessage vehicleIdReq = message::makeVehicleIdentificationRequest(); + + ssize_t bytesSent = sendto(m_udpSocket.get(), vehicleIdReq.data(), vehicleIdReq.size(), 0, reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); + m_log->info("Sent Vehicle Identification Request to {}:{}", inet_address, ntohs(m_serverAddress.sin_port)); + + if (bytesSent > 0) { + m_log->info("Sending Vehicle Identification Request"); + } + + return bytesSent; +} + +// ------------------------------------------------------------------- +// TCP Connection and Thread Handling +// ------------------------------------------------------------------- + +bool DoIPClient::startTcpConnection(const char *inet_address, uint16_t port) { + int tmpSocket = socket(AF_INET, SOCK_STREAM, 0); + + if (tmpSocket >= 0) { + m_tcpSocket.reset(tmpSocket); + m_log->info("Client TCP-Socket created successfully"); + + bool connectedFlag = false; + const char *ipAddr = inet_address; + m_serverAddress.sin_family = AF_INET; + m_serverAddress.sin_port = htons(port); + inet_aton(ipAddr, &(m_serverAddress.sin_addr)); + + int retries = 3; + while (!connectedFlag && retries > 0) { + int tmpConnSocket = connect(m_tcpSocket.get(), reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); + if (tmpConnSocket != -1) { + m_tcpClientSocket.reset(tmpConnSocket); + connectedFlag = true; + m_log->info("Connection to server established"); + + if (!activateRouting()) { + m_log->error("Routing activation failed - connection closed"); + m_tcpClientSocket.close(); + m_model->routingActivationFinished(*this, false, m_logicalAddress); + return false; + } + + m_model->routingActivationFinished(*this, true, m_logicalAddress); + + m_tcpRunning.store(true); + m_tcpThread = std::thread(&DoIPClient::tcpThreadFunction, this); + return true; + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + retries--; + } + } + + return false; +} + void DoIPClient::tcpThreadFunction() { int sendRetries = 5; int receiveRetries = 5; @@ -262,16 +384,10 @@ void DoIPClient::closeTcpConnection() { m_tcpThread.join(); } - m_connected.close(); + m_tcpClientSocket.close(); m_tcpSocket.close(); } -void DoIPClient::closeUdpConnection() { - m_udpSocket.close(); - if (m_udpAnnouncementSocket.get() >= 0) { - m_udpAnnouncementSocket.close(); - } -} bool DoIPClient::reconnectServer() { closeTcpConnection(); @@ -336,107 +452,6 @@ std::optional DoIPClient::receiveMessage() { return msg; } -void DoIPClient::receiveUdpMessage() { - - unsigned int length = sizeof(m_clientAddress); - - // Set socket to timeout after 3 seconds - struct timeval timeout; - timeout.tv_sec = 3; - timeout.tv_usec = 0; - setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - - int bytesRead; - bytesRead = recvfrom(m_udpSocket.get(), m_receiveBuf.data(), DOIP_MAXIMUM_MTU, 0, reinterpret_cast(&m_clientAddress), &length); - - if (bytesRead < 0) { - if (errno == EAGAIN) { - m_log->warn("Timeout waiting for UDP response"); - } else { - m_log->error("Error receiving UDP message: {}", strerror(errno)); - } - return; - } - - m_log->info("Received {} bytes from UDP", bytesRead); - - auto optMmsg = DoIPMessage::tryParse(m_receiveBuf.data(), static_cast(bytesRead)); - if (!optMmsg.has_value()) { - m_log->error("Failed to parse DoIP message from UDP data"); - return; - } - - DoIPMessage msg = optMmsg.value(); - - m_log->info("RX: {}", fmt::streamed(msg)); -} - -bool DoIPClient::receiveVehicleAnnouncement() { - unsigned int length = sizeof(m_announcementAddress); - int bytesRead; - - m_log->debug("Listening for Vehicle Announcements on port {}", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); - - // Set socket timeout for announcement reception - struct timeval timeout; - timeout.tv_sec = 5; // increase to 5 seconds for robustness in CI - timeout.tv_usec = 0; - setsockopt(m_udpAnnouncementSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - - bytesRead = recvfrom(m_udpAnnouncementSocket.get(), m_receiveBuf.data(), DOIP_MAXIMUM_MTU, 0, - reinterpret_cast(&m_announcementAddress), &length); - if (bytesRead < 0) { - if (errno == EAGAIN) { - m_log->warn("Timeout waiting for Vehicle Announcement"); - } else { - m_log->error("Error receiving Vehicle Announcement: {}", strerror(errno)); - } - return false; - } - - auto optMsg = DoIPMessage::tryParse(m_receiveBuf.data(), static_cast(bytesRead)); - if (!optMsg.has_value()) { - m_log->error("Failed to parse Vehicle Announcement message"); - return false; - } - - DoIPMessage msg = optMsg.value(); - // Parse and display the announcement information - if (msg.getPayloadType() == DoIPPayloadType::VehicleIdentificationResponse) { - m_log->info("Vehicle Announcement received: {}", fmt::streamed(msg)); - parseVehicleIdentificationResponse(msg); - return true; - } - return false; -} - -ssize_t DoIPClient::sendVehicleIdentificationRequest(const char *inet_address) { - - int setAddressError = inet_aton(inet_address, &(m_serverAddress.sin_addr)); - - if (setAddressError != 0) { - m_log->info("Address set successfully"); - } else { - m_log->error("Could not set address. Try again"); - } - - int socketError = setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_BROADCAST, &m_broadcast, sizeof(m_broadcast)); - - if (socketError == 0) { - m_log->info("Broadcast Option set successfully"); - } - - DoIPMessage vehicleIdReq = message::makeVehicleIdentificationRequest(); - - ssize_t bytesSent = sendto(m_udpSocket.get(), vehicleIdReq.data(), vehicleIdReq.size(), 0, reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); - m_log->info("Sent Vehicle Identification Request to {}:{}", inet_address, ntohs(m_serverAddress.sin_port)); - - if (bytesSent > 0) { - m_log->info("Sending Vehicle Identification Request"); - } - - return bytesSent; -} /** * Sets the source address for this client diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index df73172..024d6a8 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -89,6 +89,7 @@ bool DoIPServer::setupTcpSocket(std::function modelFacto } if (!m_transport->setup(DOIP_TCP_DEFAULT_PORT)) { + FIXME: setup only TCP port m_doipLog->error("Failed to setup transport"); return false; } @@ -115,6 +116,7 @@ void DoIPServer::closeTcpSocket() { bool DoIPServer::setupUdpSocket() { // UDP is already setup in TcpServerTransport + FIXME: setup UDP port here, not in setupTcpSocket m_udpRunning.store(true); m_workerThreads.emplace_back([this]() { udpAnnouncementThread(); @@ -212,9 +214,13 @@ void DoIPServer::udpAnnouncementThread() { m_udpLog->error("Failed to send announcement"); } + // TODO: receive and log any UDP responses from clients + usleep(m_config.announceInterval * 1000); } + + m_doipLog->info("Announcement thread stopped"); } diff --git a/src/tp/TcpServerTransport.cpp b/src/tp/TcpServerTransport.cpp index bfb386b..4774726 100644 --- a/src/tp/TcpServerTransport.cpp +++ b/src/tp/TcpServerTransport.cpp @@ -105,6 +105,8 @@ bool TcpServerTransport::setupUdpSocket() { return false; } + auto port = DOIP_UDP_DISCOVERY_PORT; + // Set socket timeout struct timeval timeout; timeout.tv_sec = 1; @@ -120,10 +122,10 @@ bool TcpServerTransport::setupUdpSocket() { memset(&udp_addr, 0, sizeof(udp_addr)); udp_addr.sin_family = AF_INET; udp_addr.sin_addr.s_addr = htonl(INADDR_ANY); - udp_addr.sin_port = htons(DOIP_UDP_DISCOVERY_PORT); + udp_addr.sin_port = htons(port); if (bind(tmpUdpSocket, reinterpret_cast(&udp_addr), sizeof(udp_addr)) < 0) { - m_log->error("Failed to bind UDP socket to port {}: {}", DOIP_UDP_DISCOVERY_PORT, strerror(errno)); + m_log->error("Failed to bind UDP socket to port {}: {}", port, strerror(errno)); ::close(tmpUdpSocket); tmpUdpSocket = -1; return false; @@ -131,7 +133,7 @@ bool TcpServerTransport::setupUdpSocket() { m_udpSocket.reset(tmpUdpSocket); - m_log->info("UDP socket bound to port {}", DOIP_UDP_DISCOVERY_PORT); + m_log->info("UDP socket bound to port {}", port); return true; } From 2f5b1c74aa38b2ae79883d4acad8e96a3fe9dc20 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Mon, 5 Jan 2026 21:25:22 +0100 Subject: [PATCH 15/44] WIP: Udp migration --- inc/DoIPServer.h | 11 ++++ inc/tp/IServerTransport.h | 2 +- inc/tp/TcpServerTransport.h | 18 ++---- src/DoIPServer.cpp | 95 ++++++++++++++++++++++++++++++- src/tp/TcpServerTransport.cpp | 104 +++------------------------------- 5 files changed, 115 insertions(+), 115 deletions(-) diff --git a/inc/DoIPServer.h b/inc/DoIPServer.h index cb35039..d177367 100644 --- a/inc/DoIPServer.h +++ b/inc/DoIPServer.h @@ -240,6 +240,11 @@ class DoIPServer { // Automatic mode state std::atomic m_udpRunning{false}; std::atomic m_tcpRunning{false}; + // migrated + Socket m_udpSocket{-1}; + bool m_loopback; + struct sockaddr_in m_broadcastAddress{}; + std::vector m_workerThreads; std::mutex m_mutex; @@ -253,6 +258,12 @@ class DoIPServer { void connectionHandlerThread(std::unique_ptr connection); + ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port); + + /** + * @brief Configure broadcast/multicast settings + */ + void configureBroadcast(); void udpAnnouncementThread(); }; diff --git a/inc/tp/IServerTransport.h b/inc/tp/IServerTransport.h index 886e800..5bb924a 100644 --- a/inc/tp/IServerTransport.h +++ b/inc/tp/IServerTransport.h @@ -58,7 +58,7 @@ class IServerTransport { * @param port The destination port for broadcast * @return Number of bytes sent, or -1 on error */ - virtual ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port) = 0; + //virtual ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port) = 0; /** * @brief Close the server transport and cleanup resources diff --git a/inc/tp/TcpServerTransport.h b/inc/tp/TcpServerTransport.h index 6926636..ae2dba8 100644 --- a/inc/tp/TcpServerTransport.h +++ b/inc/tp/TcpServerTransport.h @@ -37,37 +37,27 @@ class TcpServerTransport : public IServerTransport { // IServerTransport interface bool setup(uint16_t port) override; std::unique_ptr acceptConnection() override; - ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port) override; + //--ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port) override; void close() override; bool isActive() const override; std::string getIdentifier() const override; private: Socket m_tcpServerSocket{-1}; - Socket m_udpSocket{-1}; + //-- uint16_t m_port{0}; - bool m_loopback; + //--bool m_loopback; std::atomic m_isActive{false}; std::shared_ptr m_log; struct sockaddr_in m_serverAddress{}; - struct sockaddr_in m_broadcastAddress{}; + //--struct sockaddr_in m_broadcastAddress{}; /** * @brief Set up TCP server socket (bind + listen) */ bool setupTcpSocket(); - /** - * @brief Set up UDP socket for announcements - */ - bool setupUdpSocket(); - - /** - * @brief Configure broadcast/multicast settings - */ - void configureBroadcast(); - void closeSocket(); }; diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index 024d6a8..ccb71eb 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -115,8 +115,43 @@ void DoIPServer::closeTcpSocket() { } bool DoIPServer::setupUdpSocket() { - // UDP is already setup in TcpServerTransport - FIXME: setup UDP port here, not in setupTcpSocket + m_udpLog->debug("Setting up UDP socket for broadcasts"); + + int tmpUdpSocket = socket(AF_INET, SOCK_DGRAM, 0); + if (tmpUdpSocket < 0) { + m_udpLog->error("Failed to create UDP socket: {}", strerror(errno)); + return false; + } + + auto port = DOIP_UDP_DISCOVERY_PORT; + + // Set socket timeout + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + setsockopt(tmpUdpSocket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + // Enable SO_REUSEADDR + int reuse = 1; + setsockopt(tmpUdpSocket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + + // Bind to discovery port + struct sockaddr_in udp_addr; + memset(&udp_addr, 0, sizeof(udp_addr)); + udp_addr.sin_family = AF_INET; + udp_addr.sin_addr.s_addr = htonl(INADDR_ANY); + udp_addr.sin_port = htons(port); + + if (bind(tmpUdpSocket, reinterpret_cast(&udp_addr), sizeof(udp_addr)) < 0) { + m_udpLog->error("Failed to bind UDP socket to port {}: {}", port, strerror(errno)); + ::close(tmpUdpSocket); + tmpUdpSocket = -1; + return false; + } + + m_udpSocket.reset(tmpUdpSocket); + + m_udpLog->info("UDP socket bound to port {}", port); m_udpRunning.store(true); m_workerThreads.emplace_back([this]() { udpAnnouncementThread(); @@ -205,7 +240,7 @@ void DoIPServer::udpAnnouncementThread() { DoIPMessage msg = message::makeVehicleIdentificationResponse( m_config.vin, m_config.logicalAddress, m_config.eid, m_config.gid); - ssize_t sentBytes = m_transport->sendBroadcast(msg, DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); + ssize_t sentBytes = sendBroadcast(msg, DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); m_doipLog->info("TX {}", fmt::streamed(msg)); if (sentBytes > 0) { @@ -271,3 +306,57 @@ void DoIPServer::tcpListenerThread(std::function modelFa m_doipLog->info("TCP listener thread stopped"); } + +// remigrated from TcpServerTransport.cpp + + + +void DoIPServer::configureBroadcast() { + if (m_loopback) { + m_udpLog->debug("Configuring for loopback mode"); + m_broadcastAddress.sin_family = AF_INET; + m_broadcastAddress.sin_addr.s_addr = inet_addr("127.0.0.1"); + m_broadcastAddress.sin_port = htons(DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); + } else { + m_udpLog->debug("Configuring for broadcast mode"); + + // Enable broadcast + int broadcast = 1; + if (setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { + m_udpLog->warn("Failed to enable broadcast: {}", strerror(errno)); + } + + m_broadcastAddress.sin_family = AF_INET; + m_broadcastAddress.sin_addr.s_addr = inet_addr("255.255.255.255"); + m_broadcastAddress.sin_port = htons(DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); + } +} + +ssize_t DoIPServer::sendBroadcast(const DoIPMessage &msg, uint16_t port) { + if (m_udpSocket.get() < 0) { + m_udpLog->error("UDP socket not initialized, cannot send broadcast"); + return -1; + } + + // Override port if specified + struct sockaddr_in dest_addr = m_broadcastAddress; + if (port != 0) { + dest_addr.sin_port = htons(port); + } + + ssize_t sent = sendto( + m_udpSocket.get(), + msg.data(), + msg.size(), + 0, + reinterpret_cast(&dest_addr), + sizeof(dest_addr)); + + if (sent < 0) { + m_udpLog->error("Failed to send broadcast: {}", strerror(errno)); + return -1; + } + + m_udpLog->debug("Sent {} bytes via UDP broadcast", sent); + return sent; +} diff --git a/src/tp/TcpServerTransport.cpp b/src/tp/TcpServerTransport.cpp index 4774726..ce427b5 100644 --- a/src/tp/TcpServerTransport.cpp +++ b/src/tp/TcpServerTransport.cpp @@ -26,7 +26,7 @@ void TcpServerTransport::closeSocket() { if (m_log) m_log->info("Closing TCP server transport (destructor)"); m_tcpServerSocket.close(); - m_udpSocket.close(); + //m_udpSocket.close(); } } @@ -39,13 +39,13 @@ bool TcpServerTransport::setup(uint16_t port) { return false; } - if (!setupUdpSocket()) { - m_log->error("Failed to setup UDP socket"); - m_udpSocket.close(); - return false; - } + // if (!setupUdpSocket()) { + // m_log->error("Failed to setup UDP socket"); + // m_udpSocket.close(); + // return false; + // } - configureBroadcast(); + // configureBroadcast(); m_isActive = true; m_log->info("TCP server transport ready on port {}", port); return true; @@ -96,68 +96,6 @@ bool TcpServerTransport::setupTcpSocket() { return true; } -bool TcpServerTransport::setupUdpSocket() { - m_log->debug("Setting up UDP socket for broadcasts"); - - int tmpUdpSocket = socket(AF_INET, SOCK_DGRAM, 0); - if (tmpUdpSocket < 0) { - m_log->error("Failed to create UDP socket: {}", strerror(errno)); - return false; - } - - auto port = DOIP_UDP_DISCOVERY_PORT; - - // Set socket timeout - struct timeval timeout; - timeout.tv_sec = 1; - timeout.tv_usec = 0; - setsockopt(tmpUdpSocket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - - // Enable SO_REUSEADDR - int reuse = 1; - setsockopt(tmpUdpSocket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); - - // Bind to discovery port - struct sockaddr_in udp_addr; - memset(&udp_addr, 0, sizeof(udp_addr)); - udp_addr.sin_family = AF_INET; - udp_addr.sin_addr.s_addr = htonl(INADDR_ANY); - udp_addr.sin_port = htons(port); - - if (bind(tmpUdpSocket, reinterpret_cast(&udp_addr), sizeof(udp_addr)) < 0) { - m_log->error("Failed to bind UDP socket to port {}: {}", port, strerror(errno)); - ::close(tmpUdpSocket); - tmpUdpSocket = -1; - return false; - } - - m_udpSocket.reset(tmpUdpSocket); - - m_log->info("UDP socket bound to port {}", port); - return true; -} - -void TcpServerTransport::configureBroadcast() { - if (m_loopback) { - m_log->debug("Configuring for loopback mode"); - m_broadcastAddress.sin_family = AF_INET; - m_broadcastAddress.sin_addr.s_addr = inet_addr("127.0.0.1"); - m_broadcastAddress.sin_port = htons(DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); - } else { - m_log->debug("Configuring for broadcast mode"); - - // Enable broadcast - int broadcast = 1; - if (setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { - m_log->warn("Failed to enable broadcast: {}", strerror(errno)); - } - - m_broadcastAddress.sin_family = AF_INET; - m_broadcastAddress.sin_addr.s_addr = inet_addr("255.255.255.255"); - m_broadcastAddress.sin_port = htons(DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); - } -} - std::unique_ptr TcpServerTransport::acceptConnection() { if (!m_isActive || m_tcpServerSocket.get() < 0) { return nullptr; @@ -185,34 +123,6 @@ std::unique_ptr TcpServerTransport::acceptConnection() { return std::make_unique(client_socket); } -ssize_t TcpServerTransport::sendBroadcast(const DoIPMessage &msg, uint16_t port) { - if (m_udpSocket.get() < 0) { - m_log->error("UDP socket not initialized, cannot send broadcast"); - return -1; - } - - // Override port if specified - struct sockaddr_in dest_addr = m_broadcastAddress; - if (port != 0) { - dest_addr.sin_port = htons(port); - } - - ssize_t sent = sendto( - m_udpSocket.get(), - msg.data(), - msg.size(), - 0, - reinterpret_cast(&dest_addr), - sizeof(dest_addr)); - - if (sent < 0) { - m_log->error("Failed to send broadcast: {}", strerror(errno)); - return -1; - } - - m_log->debug("Sent {} bytes via UDP broadcast", sent); - return sent; -} void TcpServerTransport::close() { closeSocket(); From 4d5a9a76e68b0799099d0cd57b0e2b754be9cab4 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Wed, 7 Jan 2026 22:11:40 +0100 Subject: [PATCH 16/44] fix: Simplified code --- libs/uds/src/UdsDiagnosticTroubleCode.cpp | 114 ++++++++++------------ 1 file changed, 50 insertions(+), 64 deletions(-) diff --git a/libs/uds/src/UdsDiagnosticTroubleCode.cpp b/libs/uds/src/UdsDiagnosticTroubleCode.cpp index 3aec8bf..7704b98 100644 --- a/libs/uds/src/UdsDiagnosticTroubleCode.cpp +++ b/libs/uds/src/UdsDiagnosticTroubleCode.cpp @@ -1,8 +1,8 @@ #include "uds/UdsDiagnosticTroubleCode.h" -#include -#include #include +#include +#include namespace doip::uds { @@ -30,14 +30,14 @@ DiagnosticTroubleCode::Severity DiagnosticTroubleCode::getSeverity() const noexc uint8_t severityBits = (high >> 6) & 0x03; switch (severityBits) { - case 0x01: - return Severity::Warning; - case 0x02: - return Severity::Error; - case 0x03: - return Severity::Critical; - default: - return Severity::Informational; + case 0x01: + return Severity::Warning; + case 0x02: + return Severity::Error; + case 0x03: + return Severity::Critical; + default: + return Severity::Informational; } } @@ -45,73 +45,71 @@ std::array DiagnosticTroubleCode::serialize() const noexcept { return {getHighByte(), getMiddleByte(), getLowByte(), m_statusBits}; } -bool DiagnosticTroubleCode::deserialize(const std::array& data) noexcept { +bool DiagnosticTroubleCode::deserialize(const std::array &data) noexcept { m_code = (static_cast(data[0]) << 16) | (static_cast(data[1]) << 8) | static_cast(data[2]); m_statusBits = data[3]; return true; } -bool DiagnosticTroubleCode::deserialize(const std::vector& data, +bool DiagnosticTroubleCode::deserialize(const std::vector &data, size_t offset) noexcept { if (data.size() < offset + 4) { return false; } std::array arr = {data[offset], data[offset + 1], data[offset + 2], - data[offset + 3]}; + data[offset + 3]}; return deserialize(arr); } std::string DiagnosticTroubleCode::getStatusDescription() const noexcept { std::ostringstream oss; - bool first = true; + std::vector tags; if (hasStatusBit(STATUS_TEST_FAILED)) { - if (!first) oss << ", "; - oss << "testFailed"; - first = false; + tags.emplace_back("testFailed"); } + if (hasStatusBit(STATUS_TEST_FAILED_THIS_CYCLE)) { - if (!first) oss << ", "; - oss << "testFailedThisOperationCycle"; - first = false; + tags.emplace_back("testFailedThisOperationCycle"); } + if (hasStatusBit(STATUS_PENDING_DTC)) { - if (!first) oss << ", "; - oss << "pendingDTC"; - first = false; + tags.emplace_back("pendingDTC"); } + if (hasStatusBit(STATUS_CONFIRMED_DTC)) { - if (!first) oss << ", "; - oss << "confirmedDTC"; - first = false; + tags.emplace_back("confirmedDTC"); } + if (hasStatusBit(STATUS_TEST_NOT_COMPLETED_SINCE_CLEAR)) { - if (!first) oss << ", "; - oss << "testNotCompletedSinceClear"; - first = false; + tags.emplace_back("testNotCompletedSinceClear"); } + if (hasStatusBit(STATUS_TEST_FAILED_SINCE_CLEAR)) { - if (!first) oss << ", "; - oss << "testFailedSinceClear"; - first = false; + tags.emplace_back("testFailedSinceClear"); } + if (hasStatusBit(STATUS_TEST_NOT_COMPLETED_THIS_CYCLE)) { - if (!first) oss << ", "; - oss << "testNotCompletedThisOperationCycle"; - first = false; + tags.emplace_back("testNotCompletedThisOperationCycle"); } + if (hasStatusBit(STATUS_WARNING_INDICATOR)) { + tags.emplace_back("warningIndicatorRequested"); + } + + bool first = true; + for (const auto &tag : tags) { if (!first) oss << ", "; - oss << "warningIndicatorRequested"; + oss << tag; first = false; } return oss.str(); } -std::ostream& operator<<(std::ostream& os, const DiagnosticTroubleCode& dtc) { +std::ostream &operator<<(std::ostream &os, const DiagnosticTroubleCode &dtc) { os << "DTC(0x" << std::hex << std::setfill('0') << std::setw(6) << dtc.getCode() << ", status=0x" << std::setw(2) << static_cast(dtc.getStatusBits()) << std::dec << ")"; @@ -120,7 +118,7 @@ std::ostream& operator<<(std::ostream& os, const DiagnosticTroubleCode& dtc) { // DiagnosticTroubleCodeStore implementation -bool DiagnosticTroubleCodeStore::addDTC(const DiagnosticTroubleCode& dtc) noexcept { +bool DiagnosticTroubleCodeStore::addDTC(const DiagnosticTroubleCode &dtc) noexcept { if (hasDTC(dtc.getCode())) { return false; } @@ -131,9 +129,9 @@ bool DiagnosticTroubleCodeStore::addDTC(const DiagnosticTroubleCode& dtc) noexce bool DiagnosticTroubleCodeStore::removeDTC(uint32_t code) noexcept { auto it = std::find_if(m_dtcs.begin(), m_dtcs.end(), - [code](const DiagnosticTroubleCode& dtc) { - return dtc.getCode() == code; - }); + [code](const DiagnosticTroubleCode &dtc) { + return dtc.getCode() == code; + }); if (it != m_dtcs.end()) { m_dtcs.erase(it); @@ -143,23 +141,23 @@ bool DiagnosticTroubleCodeStore::removeDTC(uint32_t code) noexcept { } bool DiagnosticTroubleCodeStore::hasDTC(uint32_t code) const noexcept { - return std::any_of(m_dtcs.begin(), m_dtcs.end(), [code](const DiagnosticTroubleCode& dtc) { + return std::any_of(m_dtcs.begin(), m_dtcs.end(), [code](const DiagnosticTroubleCode &dtc) { return dtc.getCode() == code; }); } -DiagnosticTroubleCode* DiagnosticTroubleCodeStore::findDTC(uint32_t code) noexcept { +DiagnosticTroubleCode *DiagnosticTroubleCodeStore::findDTC(uint32_t code) noexcept { auto it = - std::find_if(m_dtcs.begin(), m_dtcs.end(), [code](const DiagnosticTroubleCode& dtc) { + std::find_if(m_dtcs.begin(), m_dtcs.end(), [code](const DiagnosticTroubleCode &dtc) { return dtc.getCode() == code; }); return it != m_dtcs.end() ? &(*it) : nullptr; } -const DiagnosticTroubleCode* DiagnosticTroubleCodeStore::findDTC(uint32_t code) const noexcept { +const DiagnosticTroubleCode *DiagnosticTroubleCodeStore::findDTC(uint32_t code) const noexcept { auto it = - std::find_if(m_dtcs.begin(), m_dtcs.end(), [code](const DiagnosticTroubleCode& dtc) { + std::find_if(m_dtcs.begin(), m_dtcs.end(), [code](const DiagnosticTroubleCode &dtc) { return dtc.getCode() == code; }); @@ -169,32 +167,20 @@ const DiagnosticTroubleCode* DiagnosticTroubleCodeStore::findDTC(uint32_t code) std::vector DiagnosticTroubleCodeStore::getConfirmedDTCs() const noexcept { std::vector confirmed; - for (const auto& dtc : m_dtcs) { - if (dtc.isConfirmed()) { - confirmed.push_back(dtc); - } - } + std::copy_if(m_dtcs.begin(), m_dtcs.end(), std::back_inserter(confirmed), [](const DiagnosticTroubleCode& dtc) { return dtc.isConfirmed(); }); return confirmed; } std::vector DiagnosticTroubleCodeStore::getPendingDTCs() const noexcept { std::vector pending; - for (const auto& dtc : m_dtcs) { - if (dtc.isPending()) { - pending.push_back(dtc); - } - } + std::copy_if(m_dtcs.begin(), m_dtcs.end(), std::back_inserter(pending), [](const DiagnosticTroubleCode& dtc) { return dtc.isPending(); }); return pending; } std::vector DiagnosticTroubleCodeStore::getActiveDTCs() const noexcept { std::vector active; - for (const auto& dtc : m_dtcs) { - if (dtc.hasActiveFailure()) { - active.push_back(dtc); - } - } + std::copy_if(m_dtcs.begin(), m_dtcs.end(), std::back_inserter(active), [](const DiagnosticTroubleCode& dtc) { return dtc.hasActiveFailure(); }); return active; } @@ -202,7 +188,7 @@ std::vector DiagnosticTroubleCodeStore::serialize() const noexcept { std::vector result; result.reserve(m_dtcs.size() * 4); - for (const auto& dtc : m_dtcs) { + for (const auto &dtc : m_dtcs) { auto serialized = dtc.serialize(); result.insert(result.end(), serialized.begin(), serialized.end()); } @@ -210,14 +196,14 @@ std::vector DiagnosticTroubleCodeStore::serialize() const noexcept { return result; } -DiagnosticTroubleCode* DiagnosticTroubleCodeStore::getDTCAt(size_t index) noexcept { +DiagnosticTroubleCode *DiagnosticTroubleCodeStore::getDTCAt(size_t index) noexcept { if (index >= m_dtcs.size()) { return nullptr; } return &m_dtcs[index]; } -const DiagnosticTroubleCode* DiagnosticTroubleCodeStore::getDTCAt(size_t index) const noexcept { +const DiagnosticTroubleCode *DiagnosticTroubleCodeStore::getDTCAt(size_t index) const noexcept { if (index >= m_dtcs.size()) { return nullptr; } From 9f2c559ade2d8ac820eabaef320b0c486b122e42 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Wed, 7 Jan 2026 22:47:31 +0100 Subject: [PATCH 17/44] fix: Continue UDP migration, fix cppcheck findings/compiler errors --- inc/DoIPClient.h | 2 +- inc/DoIPServer.h | 1 + inc/tp/MockServerTransport.h | 2 +- inc/tp/TcpServerTransport.h | 2 +- src/DoIPClient.cpp | 3 ++- src/DoIPServer.cpp | 4 ++-- src/tp/MockServerTransport.cpp | 10 ---------- src/tp/TcpServerTransport.cpp | 5 +---- 8 files changed, 9 insertions(+), 20 deletions(-) diff --git a/inc/DoIPClient.h b/inc/DoIPClient.h index 7106d3f..5f78e06 100644 --- a/inc/DoIPClient.h +++ b/inc/DoIPClient.h @@ -26,7 +26,7 @@ class DoIPClient { }; public: - DoIPClient(UniqueDoIPClientModelPtr model = std::make_unique()) : m_model(std::move(model)) { m_receiveBuf.reserve(DOIP_MAXIMUM_MTU); } + explicit DoIPClient(UniqueDoIPClientModelPtr model = std::make_unique()) : m_model(std::move(model)) { m_receiveBuf.reserve(DOIP_MAXIMUM_MTU); } [[nodiscard]] bool startTcpConnection(const char *inet_address = "127.0.0.1", uint16_t port = DOIP_TCP_DEFAULT_PORT); diff --git a/inc/DoIPServer.h b/inc/DoIPServer.h index d177367..4e2036e 100644 --- a/inc/DoIPServer.h +++ b/inc/DoIPServer.h @@ -21,6 +21,7 @@ #include "DoIPServerModel.h" #include "tp/IServerTransport.h" #include "DoIPDefaultConnection.h" +#include "util/Socket.h" namespace doip { diff --git a/inc/tp/MockServerTransport.h b/inc/tp/MockServerTransport.h index afdbcf3..f3e9886 100644 --- a/inc/tp/MockServerTransport.h +++ b/inc/tp/MockServerTransport.h @@ -29,7 +29,7 @@ class MockServerTransport : public IServerTransport { // IServerTransport interface bool setup(uint16_t port) override; std::unique_ptr acceptConnection() override; - ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port) override; + //--ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port) override; void close() override; bool isActive() const override; std::string getIdentifier() const override; diff --git a/inc/tp/TcpServerTransport.h b/inc/tp/TcpServerTransport.h index ae2dba8..94f1ded 100644 --- a/inc/tp/TcpServerTransport.h +++ b/inc/tp/TcpServerTransport.h @@ -43,7 +43,7 @@ class TcpServerTransport : public IServerTransport { std::string getIdentifier() const override; private: - Socket m_tcpServerSocket{-1}; + Socket m_tcpServerSocket{}; //-- uint16_t m_port{0}; //--bool m_loopback; diff --git a/src/DoIPClient.cpp b/src/DoIPClient.cpp index 5a8746a..593aee8 100644 --- a/src/DoIPClient.cpp +++ b/src/DoIPClient.cpp @@ -92,7 +92,8 @@ void DoIPClient::receiveUdpMessage() { // Set socket to timeout after 3 seconds struct timeval timeout; - timeout.tv_sec = static_cast(doip::times::client::UdpMessageTimeout.count() / 1000); + //timeout.tv_sec = static_cast(doip::times::client::UdpMessageTimeout.count() / 1000); + timeout.tv_sec = doip::times::client::UdpMessageTimeout.count() / 1000; timeout.tv_usec = 0; setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index ccb71eb..1358347 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -27,7 +27,8 @@ DoIPServer::DoIPServer(const ServerConfig &config) m_doipLog(Logger::get("server")), m_udpLog(Logger::getUdp()), m_tcpLog(Logger::getTcp()), - m_transport(std::make_unique(config.loopback)) { + m_transport(std::make_unique(config.loopback)), + m_loopback(config.loopback) { setLoopbackMode(m_config.loopback); } @@ -89,7 +90,6 @@ bool DoIPServer::setupTcpSocket(std::function modelFacto } if (!m_transport->setup(DOIP_TCP_DEFAULT_PORT)) { - FIXME: setup only TCP port m_doipLog->error("Failed to setup transport"); return false; } diff --git a/src/tp/MockServerTransport.cpp b/src/tp/MockServerTransport.cpp index 34652a4..468e94a 100644 --- a/src/tp/MockServerTransport.cpp +++ b/src/tp/MockServerTransport.cpp @@ -25,16 +25,6 @@ std::unique_ptr MockServerTransport::acceptConnection() { return nullptr; } -ssize_t MockServerTransport::sendBroadcast(const DoIPMessage &msg, uint16_t port) { - (void)port; - if (!m_isActive) { - return -1; - } - - m_broadcastQueue.push(msg); - return static_cast(msg.size()); -} - void MockServerTransport::close() { m_isActive = false; clearQueues(); diff --git a/src/tp/TcpServerTransport.cpp b/src/tp/TcpServerTransport.cpp index ce427b5..7a53ed3 100644 --- a/src/tp/TcpServerTransport.cpp +++ b/src/tp/TcpServerTransport.cpp @@ -11,8 +11,7 @@ namespace doip { TcpServerTransport::TcpServerTransport(bool loopback) - : m_loopback(loopback), - m_log(Logger::get("TcpServerTransport")) { + : m_log(Logger::get("TcpServerTransport")) { m_log->debug("TcpServerTransport created (loopback={})", loopback); } @@ -74,7 +73,6 @@ bool TcpServerTransport::setupTcpSocket() { if (bind(tmpSocket, reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)) < 0) { m_log->error("Failed to bind TCP socket to port {}: {}", m_port, strerror(errno)); ::close(tmpSocket); - tmpSocket = -1; return false; } @@ -86,7 +84,6 @@ bool TcpServerTransport::setupTcpSocket() { if (listen(tmpSocket, 5) < 0) { m_log->error("Failed to listen on TCP socket: {}", strerror(errno)); ::close(tmpSocket); - tmpSocket = -1; return false; } From e0e878f17c7b80fa6bf47326eeb0c51dd81da20f Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Wed, 7 Jan 2026 22:48:11 +0100 Subject: [PATCH 18/44] chore: Use std output dir --- test/integration/uds-download/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index fc3033a..5a10226 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -19,6 +19,7 @@ foreach(demo_source ${DISCOVER_SOURCES}) CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) # Disable switch-default warning for examples using spdlog From e4564534f8719340967d21345dd84e4e452f988d Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Wed, 7 Jan 2026 23:17:20 +0100 Subject: [PATCH 19/44] test: Complete discover test --- examples/discover/CMakeLists.txt | 4 ++++ examples/discover/DoIPUdpServer.cpp | 7 +++++-- examples/discover/discover-client.py | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/examples/discover/CMakeLists.txt b/examples/discover/CMakeLists.txt index 6f4c6e5..a51b86f 100644 --- a/examples/discover/CMakeLists.txt +++ b/examples/discover/CMakeLists.txt @@ -4,5 +4,9 @@ target_link_libraries(DoIPUdpServer ${DOIP_SERVER_LIB_NAME} ) +set_target_properties(DoIPUdpServer PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) + configure_file(discover-client.py ${CMAKE_CURRENT_BINARY_DIR}/discover-client.py COPYONLY) configure_file(discover-client.py ${CMAKE_BINARY_DIR}/discover-client.py COPYONLY) \ No newline at end of file diff --git a/examples/discover/DoIPUdpServer.cpp b/examples/discover/DoIPUdpServer.cpp index 51a8120..611880f 100644 --- a/examples/discover/DoIPUdpServer.cpp +++ b/examples/discover/DoIPUdpServer.cpp @@ -17,10 +17,13 @@ int main(int argc, char *argv[]) { // for discovery check we use relaxed announcement settings server->setAnnounceInterval(500); // Send announcements every 500ms for faster discovery server->setAnnounceNum(100); // Send 100 announcements = 50 seconds of announcements (enough for parallel test execution) + server->setVin("WVWZZZ1JZ3W386752"); + server->setGid(123456); + server->setEid(654321); // Start announcement thread after sockets are bound // Set up TCP first to ensure transport creates/binds both TCP and UDP sockets - if (!server->setupTcpSocket()) { + if (!server->setupTcpSocket([]() { return std::make_unique(); })) { console->critical("Failed to set up TCP socket"); return 1; } @@ -34,7 +37,7 @@ int main(int argc, char *argv[]) { console->info("DoIP UDP Server is running"); - while (server->isUdpRunning()) { + while (server->isRunning()) { // Main loop can perform other tasks or just sleep std::this_thread::sleep_for(std::chrono::seconds(1)); } diff --git a/examples/discover/discover-client.py b/examples/discover/discover-client.py index 1b67cf6..6eca907 100644 --- a/examples/discover/discover-client.py +++ b/examples/discover/discover-client.py @@ -227,6 +227,10 @@ def _parse_vehicle_announcement(self, data: bytes, server_ip: str) -> bool: print(f" EID: {eid}") print(f" GID: {gid}") + # assert(vin == "WVWZZZ1JZ3W386752") + # assert(eid == 123456) + # assert(eid == 654321) + self.server_ip = server_ip self.target_address = logical_address @@ -330,6 +334,9 @@ def send_uds_rdbi(self, did: int = 0xF190) -> bool: print(f" Diagnostic message NACK: 0x{nack_code:02X}") return False + response = self.tcp_socket.recv(4096) + header = DoIPHeader.unpack(response) + # Wait for actual UDS response if header.payload_type != DOIP_DIAGNOSTIC_MESSAGE: response += self.tcp_socket.recv(4096) From a655e26bcff6d892762056d3e6c40f39f50887dd Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Wed, 7 Jan 2026 23:19:11 +0100 Subject: [PATCH 20/44] WIP --- src/DoIPServer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index 1358347..2c58483 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -254,6 +254,7 @@ void DoIPServer::udpAnnouncementThread() { usleep(m_config.announceInterval * 1000); } + TODO: Wait for vehicle requests m_doipLog->info("Announcement thread stopped"); From c96b70796d5e9f96dcf952e60d5ce3c841b952c8 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Wed, 7 Jan 2026 23:47:33 +0100 Subject: [PATCH 21/44] WIP: Udp listening --- examples/discover/DoIPUdpServer.cpp | 2 +- src/DoIPServer.cpp | 33 ++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/examples/discover/DoIPUdpServer.cpp b/examples/discover/DoIPUdpServer.cpp index 611880f..1a3d539 100644 --- a/examples/discover/DoIPUdpServer.cpp +++ b/examples/discover/DoIPUdpServer.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) { // for discovery check we use relaxed announcement settings server->setAnnounceInterval(500); // Send announcements every 500ms for faster discovery - server->setAnnounceNum(100); // Send 100 announcements = 50 seconds of announcements (enough for parallel test execution) + server->setAnnounceNum(10); // Send 100 announcements = 50 seconds of announcements (enough for parallel test execution) server->setVin("WVWZZZ1JZ3W386752"); server->setGid(123456); server->setEid(654321); diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index 2c58483..33bd642 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -254,7 +254,38 @@ void DoIPServer::udpAnnouncementThread() { usleep(m_config.announceInterval * 1000); } - TODO: Wait for vehicle requests + m_udpLog->info("Listening for vehicle requests..."); + while(m_udpRunning.load()) { + std::array receive; + ssize_t result = recv(m_udpSocket.get(), receive.data(), DOIP_MAXIMUM_MTU, 0); + + if (result <= 0) { + m_udpLog->warn("Receive {}", result); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + const auto& optMsg = DoIPMessage::tryParse(receive.data(), static_cast(result)); + if (!optMsg.has_value()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + m_udpLog->error("Received invalid message"); + continue; + } + + const auto& msg = optMsg.value(); + if (msg.getPayloadType() == DoIPPayloadType::VehicleIdentificationRequest) { + const auto& rsp = doip::message::makeVehicleIdentificationResponse( + m_config.vin, + m_config.logicalAddress, + m_config.eid, + m_config.gid + ); + m_udpLog->info("Send {}", fmt::streamed(rsp)); + write(m_udpSocket.get(), rsp.data(), rsp.size()); + } else { + m_udpLog->warn("Unexpected payload type: {}", fmt::streamed(msg)); + } + } m_doipLog->info("Announcement thread stopped"); From 4fbdf70d85c41fd66bd84aae30fb40cca55c2d98 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 11:17:42 +0100 Subject: [PATCH 22/44] fix: Eval return value of write --- src/DoIPServer.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index 33bd642..076b8b7 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -281,7 +281,12 @@ void DoIPServer::udpAnnouncementThread() { m_config.gid ); m_udpLog->info("Send {}", fmt::streamed(rsp)); - write(m_udpSocket.get(), rsp.data(), rsp.size()); + ssize_t rc = write(m_udpSocket.get(), rsp.data(), rsp.size()); + if (rc < 0) { + m_udpLog->error("Failed to send Vehicle Identification Response: {}", strerror(errno)); + } else { + m_udpLog->info("Sent Vehicle Identification Response: {} bytes", rc); + } } else { m_udpLog->warn("Unexpected payload type: {}", fmt::streamed(msg)); } From e8d39f8f5087ae49273b113467dd58c38267598c Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 13:50:14 +0100 Subject: [PATCH 23/44] feat: Add examples and enhance UDP server functionality --- README.md | 6 +++ examples/discover/DoIPUdpServer.cpp | 11 ++-- examples/discover/ReadMe.md | 81 ++++++++++++++++++++++++++++ examples/discover/discover-client.py | 13 ++++- examples/minimal/CMakeLists.txt | 1 + examples/minimal/ReadMe.md | 41 ++++++++++++++ src/DoIPServer.cpp | 31 +++++++---- 7 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 examples/discover/ReadMe.md create mode 100644 examples/minimal/ReadMe.md diff --git a/README.md b/README.md index 765545f..4134cf6 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,12 @@ doxygen Doxyfile xdg-open docs/html/index.html ``` +#### Examples + +1. [Minimal client/server](./examples/minimal/ReadMe.md) +2. [UDP discover](./examples/discover/ReadMe.md) +3. [UDS download (test)](./test/integration/uds-download/) + ### Installation 1. To install the library on the system, first get the source files with: diff --git a/examples/discover/DoIPUdpServer.cpp b/examples/discover/DoIPUdpServer.cpp index 1a3d539..04fcc7a 100644 --- a/examples/discover/DoIPUdpServer.cpp +++ b/examples/discover/DoIPUdpServer.cpp @@ -14,14 +14,11 @@ int main(int argc, char *argv[]) { auto server = std::make_unique(cfg); auto console = spdlog::stdout_color_mt("doip-udp-server"); - // for discovery check we use relaxed announcement settings - server->setAnnounceInterval(500); // Send announcements every 500ms for faster discovery - server->setAnnounceNum(10); // Send 100 announcements = 50 seconds of announcements (enough for parallel test execution) - server->setVin("WVWZZZ1JZ3W386752"); - server->setGid(123456); - server->setEid(654321); + // Set server properties + server->setVin("WVWZZZ1JZ3W386752"); // Some valid VIN + server->setGid(0x123456); + server->setEid(0x654321); - // Start announcement thread after sockets are bound // Set up TCP first to ensure transport creates/binds both TCP and UDP sockets if (!server->setupTcpSocket([]() { return std::make_unique(); })) { console->critical("Failed to set up TCP socket"); diff --git a/examples/discover/ReadMe.md b/examples/discover/ReadMe.md new file mode 100644 index 0000000..1b1ec58 --- /dev/null +++ b/examples/discover/ReadMe.md @@ -0,0 +1,81 @@ +# Example illustrating the "discover" mechanism. + +- Server sends vehicle announcements/responses (payload type `0x0004`); then listens for vehicle requests +- Client listens for vehicle announcements; then send a vehicle request (payload type `0x0001`) +- When the client received a vehicle identification response, it connects to the TCP endpoint +- Client sends a routing activation request and expects a response +- Client sends a diagnostic UDS message (RDBI 0x22 with DID 0xF190): `22.F1.90` +- Server responds with Diag Ack, then sends the response which should contain the VIN: `62.F1.90.57.56.57.5A.5A.5A.31.4A.5A.34.57.30.31.32.33.34.35` + + +## Build the example + +```bash +$ cmake . -Bbuild +$ cd build +$ make -j +$ cd examples/discover +``` + +## Start the server + +```bash +$ ./DoIPServer +# Send 3 vehicle announcements with 500ms interval +... +[13:18:03.469] [server] [info] TX V03|VehicleIdentificationResponse (0x0004)|L33| Payload: 57.56.57.5A.5A.5A.31.4A.5A.33.57.33.38.36.37.35.32.00.28.00.00.00.65.43.21.00.00.00.12.34.56.00.00 +[13:18:03.969] [server] [info] TX V03|VehicleIdentificationResponse (0x0004)|L33| Payload: 57.56.57.5A.5A.5A.31.4A.5A.33.57.33.38.36.37.35.32.00.28.00.00.00.65.43.21.00.00.00.12.34.56.00.00 +[13:18:04.469] [server] [info] TX V03|VehicleIdentificationResponse (0x0004)|L33| Payload: 57.56.57.5A.5A.5A.31.4A.5A.33.57.33.38.36.37.35.32.00.28.00.00.00.65.43.21.00.00.00.12.34.56.00.00 +``` + +## Run the client + +The client (please note the `--loopback` argument) then tries to discover the DOIP server - either by receiving an announcement or sending actively a vehicle + +```bash +$ python3 discover-client.py --loopback +... +No announcement received, sending identification request... +Sending vehicle identification request... +Request sent to 127.0.0.1:13400 +Received response from 127.0.0.1:13400 + VIN: WVWZZZ1JZ3W386752 + Logical Address: 0x0028 + EID: 000000654321 + GID: 000000123456 + Further Action: 0x00 +================================================== +Vehicle discovered successfully! +================================================== +... +``` + +Then the client connects to the TCP socket using the IP address of the server response: + +```bash +Connecting to 127.0.0.1:13400... +TCP connection established +``` + +Now the client sends a routing activation request... + +```bash +Sending routing activation request... + Response Code: 0x10 + Tester Address: 0x0E80 + Entity Address: 0x0028 + Status: Successfully activated +``` + +... and finally reads the VIN via UDS RDBI + +```bash +Sending UDS RDBI request for DID 0xF190... + Source: 0x0028, Target: 0x0E80 + UDS Response: 62f1905756575a5a5a314a5a3457303132333435 + Service: Positive Response (0x62) + DID: 0xF190 + Data: 5756575a5a5a314a5a3457303132333435 + Data (ASCII): WVWZZZ1JZ4W012345 + +``` diff --git a/examples/discover/discover-client.py b/examples/discover/discover-client.py index 6eca907..d7f75b6 100644 --- a/examples/discover/discover-client.py +++ b/examples/discover/discover-client.py @@ -39,7 +39,7 @@ # Timeouts # Announcements are often sent at multi-second intervals; use generous defaults -UDP_ANNOUNCEMENT_TIMEOUT = 10.0 # seconds +UDP_ANNOUNCEMENT_TIMEOUT = 5.0 # seconds UDP_RESPONSE_TIMEOUT = 5.0 # seconds TCP_RESPONSE_TIMEOUT = 5.0 # seconds @@ -392,6 +392,14 @@ def main(): print("DoIP Test Client") print("=" * 50) + # Check for loopback mode argument + use_loopback = False + if len(sys.argv) > 1 and sys.argv[1] in ['--loopback', '-l', 'loopback']: + use_loopback = True + print("Running in LOOPBACK mode (127.0.0.1)") + else: + print("Running in BROADCAST mode (255.255.255.255)") + client = DoIPClient() # Step 1: Listen for vehicle announcements @@ -400,7 +408,8 @@ def main(): # Step 2: If no announcement, send identification request if not announcement_received: print("\nNo announcement received, sending identification request...") - identification_received = client.send_vehicle_identification_request() + broadcast_addr = '127.0.0.1' if use_loopback else '255.255.255.255' + identification_received = client.send_vehicle_identification_request(broadcast_addr) if not identification_received: print("\nERROR: Neither announcement nor identification response received") diff --git a/examples/minimal/CMakeLists.txt b/examples/minimal/CMakeLists.txt index 1be848c..70801d4 100644 --- a/examples/minimal/CMakeLists.txt +++ b/examples/minimal/CMakeLists.txt @@ -18,6 +18,7 @@ foreach(demo_source ${DEMO_SOURCES}) CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) # Disable switch-default warning for examples using spdlog diff --git a/examples/minimal/ReadMe.md b/examples/minimal/ReadMe.md new file mode 100644 index 0000000..c09ab0e --- /dev/null +++ b/examples/minimal/ReadMe.md @@ -0,0 +1,41 @@ +# Minimal DoIP Client/Server Example + +Server listens on TCP port 13400, client connects to server and sends the following UDS message sequence + +1. Open a diag session `10.02` +2. Read VIN via RDBI 0xF190 `22.F1.90` +3. Close diag session / switch back to default session `10.01` + +## Build the example + +```bash +$ cmake . -Bbuild +$ cd build +$ make -j +$ cd examples/minimal +``` + +## Start the server + +```bash +$ ./MinimalDoIPServer +... +[13:42:49.197] [tcp ] [info] TCP transport ready and listening on port 13400 +... + +```bash +$ ./MinimalDoIPClient +... +# Open diag session +[2026-01-08 13:41:52.039] [doip-client] [info] TX: V03|Diag E000 -> 28: 10.02 +[2026-01-08 13:41:52.039] [doip-client] [info] RX: V03|DiagnosticMessageAck (0x8002)|L5| Payload: E0.28.00.28.00 +[2026-01-08 13:41:52.099] [doip-client] [info] RX: V03|Diag 28 -> E000: 50.02.03.E8.13.88 +# Read VIN +[2026-01-08 13:41:52.099] [doip-client] [info] TX: V03|Diag E000 -> 28: 22.F1.90 +[2026-01-08 13:41:52.099] [doip-client] [info] RX: V03|DiagnosticMessageAck (0x8002)|L5| Payload: E0.28.00.28.00 +[2026-01-08 13:41:52.159] [doip-client] [info] RX: V03|Diag 28 -> E000: 62.F1.90.57.56.57.5A.5A.5A.31.4A.5A.34.57.30.31.32.33.34.35 +# Close diag session +[2026-01-08 13:41:52.159] [doip-client] [info] TX: V03|Diag E000 -> 28: 10.01 +[2026-01-08 13:41:52.159] [doip-client] [info] RX: V03|DiagnosticMessageAck (0x8002)|L5| Payload: E0.00.00.28.00 +[2026-01-08 13:41:52.219] [doip-client] [info] RX: V03|Diag 28 -> E000: 50.01.03.E8.13.88 +``` \ No newline at end of file diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index 076b8b7..af4d5e0 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -135,6 +135,12 @@ bool DoIPServer::setupUdpSocket() { int reuse = 1; setsockopt(tmpUdpSocket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + // Enable SO_BROADCAST to receive broadcast packets + int broadcast = 1; + if (setsockopt(tmpUdpSocket, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) < 0) { + m_udpLog->warn("Failed to enable SO_BROADCAST for receive socket: {}", strerror(errno)); + } + // Bind to discovery port struct sockaddr_in udp_addr; memset(&udp_addr, 0, sizeof(udp_addr)); @@ -223,10 +229,6 @@ void DoIPServer::setLoopbackMode(bool useLoopback) { } } - - -// new version starts here - void DoIPServer::udpAnnouncementThread() { m_doipLog->info("Announcement thread started"); @@ -257,10 +259,16 @@ void DoIPServer::udpAnnouncementThread() { m_udpLog->info("Listening for vehicle requests..."); while(m_udpRunning.load()) { std::array receive; - ssize_t result = recv(m_udpSocket.get(), receive.data(), DOIP_MAXIMUM_MTU, 0); + struct sockaddr_in clientAddr; + socklen_t clientAddrLen = sizeof(clientAddr); + + ssize_t result = recvfrom(m_udpSocket.get(), receive.data(), DOIP_MAXIMUM_MTU, 0, + reinterpret_cast(&clientAddr), &clientAddrLen); if (result <= 0) { - m_udpLog->warn("Receive {}", result); + if (result < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { + m_udpLog->warn("Receive error: {} ({})", strerror(errno), errno); + } std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } @@ -273,6 +281,9 @@ void DoIPServer::udpAnnouncementThread() { } const auto& msg = optMsg.value(); + m_udpLog->info("RX {} from {}:{}", fmt::streamed(msg), + inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); + if (msg.getPayloadType() == DoIPPayloadType::VehicleIdentificationRequest) { const auto& rsp = doip::message::makeVehicleIdentificationResponse( m_config.vin, @@ -280,12 +291,14 @@ void DoIPServer::udpAnnouncementThread() { m_config.eid, m_config.gid ); - m_udpLog->info("Send {}", fmt::streamed(rsp)); - ssize_t rc = write(m_udpSocket.get(), rsp.data(), rsp.size()); + m_udpLog->info("TX {}", fmt::streamed(rsp)); + ssize_t rc = sendto(m_udpSocket.get(), rsp.data(), rsp.size(), 0, + reinterpret_cast(&clientAddr), clientAddrLen); if (rc < 0) { m_udpLog->error("Failed to send Vehicle Identification Response: {}", strerror(errno)); } else { - m_udpLog->info("Sent Vehicle Identification Response: {} bytes", rc); + m_udpLog->info("Sent Vehicle Identification Response: {} bytes to {}:{}", + rc, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); } } else { m_udpLog->warn("Unexpected payload type: {}", fmt::streamed(msg)); From ad2c3d1975a5bd4b4d2074b095f754c5732b1a7b Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 13:50:24 +0100 Subject: [PATCH 24/44] doc: Remove code review --- Code Review.md | 401 ------------------------------------------------- 1 file changed, 401 deletions(-) delete mode 100644 Code Review.md diff --git a/Code Review.md b/Code Review.md deleted file mode 100644 index 331ec52..0000000 --- a/Code Review.md +++ /dev/null @@ -1,401 +0,0 @@ -# Code Review: libdoip DoIP Server Implementation - -**Date:** January 3, 2026 -**Reviewer:** GitHub Copilot (Product Manager Persona) -**Scope:** C++17 DoIP server library - Architecture, Design, Quality, and Usability Assessment -**Perspective:** Product Manager focusing on developer experience and student adoption. - ---- - -## Executive Summary - -This is a **well-architected C++17 implementation** of a DoIP (Diagnostics over IP) protocol server. The codebase demonstrates strong adherence to modern C++ practices, clean separation of concerns, and comprehensive testing. The recent addition of a Python integration test for software downloads is a valuable step towards better real-world examples. - -However, the core developer experience for extending the server with new UDS services remains a significant barrier. The current abstraction requires deep UDS protocol knowledge, which hinders rapid prototyping and educational use. - -**Overall Rating:** ⭐⭐⭐⭐ (4/5) - *Adjusted due to developer experience concerns.* - -**Target Audience Suitability:** -- **Professional Development:** ⭐⭐⭐⭐⭐ Excellent -- **Educational/Simulation Use:** ⭐⭐⭐ Good (improving, but still complex) -- **Quick Prototyping:** ⭐⭐½ Fair (UDS implementation is slow) - -**Strengths:** -- Excellent RAII usage and modern C++ architecture. -- Clean dependency injection via transport abstraction. -- Comprehensive test coverage and CI/CD pipeline. -- **New:** Added a real-world Python example for software downloads. - -**Areas for Improvement:** -- ⚠️ **UDS Service Extension:** The `UdsServiceHandler` requires manual byte parsing, making it complex and error-prone for non-experts. -- ⚠️ **Learning Curve:** Still steep for students; lacks a "Hello World" in 5 minutes. -- ⚠️ **Client Library:** The C++ client remains legacy code, forcing reliance on external Python tools. -- ⚠️ **Configuration:** No simple configuration file support (e.g., YAML/JSON). - ---- - -**Typical Use Cases Identified:** -1. **Automotive ECU Simulation** - Simulate multiple ECUs for HIL testing. -2. **DoIP Protocol Learning** - Understand automotive diagnostics practically. -3. **UDS Service Development** - Test UDS services without physical hardware. -4. **Test Automation** - Build automated diagnostic test suites. -5. **University Projects** - Capstone projects on vehicle diagnostics. - -### Current Pain Points for Students - -#### 1. **Onboarding Difficulty** ⚠️ -The simplest C++ example (`DoIPCanIsoTpServer.cpp`) is still too complex for a first-time user, mixing DoIP, CAN, and advanced C++ concepts. - -#### 2. **Missing Minimal Example** 🚫 -A simple, 20-line "hello world" server that just brings the DoIP stack up and responds to basic requests is still the most-needed feature for new user adoption. - -#### 3. **Client Code is Legacy** ⚠️ -The `DoIPClient.h` is explicitly marked as legacy. This forces users to use the Python `doipclient` for testing, which, while functional, prevents the creation of pure C++ test environments and examples. - -#### 4. **UDS Service Registration Complexity** -This remains the most significant barrier. The current `UdsServiceHandler` is a good first step but is too low-level. - -**Before (Current State):** -```cpp -// User has to manually parse bytes and check lengths. -// This requires intimate knowledge of the UDS (ISO-14229) standard. -class MyReadDIDHandler : public UdsServiceHandler { - ByteArray handle(const ByteArray& request, const UniqueUdsModelPtr& model) override { - // Manual length check, manual DID extraction, manual response creation. - if (request.size() < 3) { - return makeNegativeResponse(UdsResponseCode::IncorrectMessageLength, request); - } - uint16_t did = (request[1] << 8) | request[2]; - if (did == 0xF190) { - ByteArray data; - data.insert(data.end(), {0x62, 0xF1, 0x90}); // Positive response SID - data.insert(data.end(), vin.begin(), vin.end()); - return data; - } - return makeNegativeResponse(UdsResponseCode::RequestOutOfRange, request); - } -}; -``` - -**After (Proposed High-Level Abstraction):** -```cpp -// A proposed higher-level handler that abstracts away byte manipulation. -class ReadDataByIdentifierHandler : public UdsServiceHandlerT { -public: - // The framework calls this with already-parsed data. - UdsResponse handle(uint16_t did, const UniqueUdsModelPtr& model) override { - if (model->hasDid(did)) { - return model->readDid(did); - } - return UdsResponseCode::RequestOutOfRange; - } -}; -``` -This highlights the need for a more sophisticated handler that simplifies the developer's task to pure logic rather than protocol mechanics. - ---- - -## A) Modern C++ Practices (RAII, DRY, KISS) - -✅ **RAII, DRY, KISS:** The project continues to excel in its use of modern C++ patterns. No regressions noted. The core architecture remains robust and clean. - ---- - -## B) Class Responsibilities & Dependencies - -✅ **Separation of Concerns:** The transport layer abstraction remains a key strength, enabling excellent testability and flexibility. - ---- - -## C) Maintainability & Extensibility - -### ✅ Adding New Downstream Providers - Easy -This remains a strong point. The `IDownstreamProvider` interface is well-designed. - ---- - -### ⚠️ UDS Service Extension - High Priority for Improvement -The existing `UdsServiceHandler` forces developers to work at the byte level. This is not ideal for the target audience of students and those prototyping. - -**Recommended Improvement:** -Introduce a templated `UdsServiceHandlerT` base class that provides parsed request parameters instead of a raw byte array. - -**Action Item:** 🔧 **(High Priority)** Create a higher-level `UdsServiceHandler` abstraction. The goal should be to allow a developer to implement a service like `ReadDataByIdentifier` without writing any byte-parsing code. - ---- - -### ✅ Transport Extension - Perfect -No changes needed. The `IConnectionTransport` interface is excellent. - ---- - -## D) Robustness & Concurrency - -✅ **Thread-Safety & Deadlock Prevention:** The core concurrency primitives (`TimerManager`, `ThreadSafeQueue`) are solid. No issues found. - ---- - -## E) Testability - -⭐⭐⭐⭐⭐ **Excellent** - The transport abstraction continues to be the foundation of the project's testability. The addition of the Python-based integration test is a great real-world check. - ---- - -## F) Understandability - -### ✅ Good Documentation -Doxygen coverage is good, but it documents *what* the code does, not *how* a new user should use it. Tutorial-style documentation is the missing piece. - -### ⚠️ Complexity: State Machine & UDS Services -The core complexity remains. The best way to address this is through better examples and higher-level abstractions. - -**Recommendation:** Add sequence diagrams and an architecture overview document. - ---- - -## 6) Proposed Features & Improvements (Updated) - -### Critical Priority 🔥 (Product Roadmap) - -#### 1. **Minimal "Hello World" Example** -This remains the **#1 priority** for user adoption. A simple 20-line server example is essential. -- **Goal:** Reduce onboarding time from hours to under 10 minutes. -- **Action:** Create `examples/minimal/minimal_server.cpp`. - -#### 2. **High-Level UDS Service Abstraction** -Create a new, easy-to-use `UdsServiceHandler` that abstracts away byte parsing. -- **Goal:** Allow developers to add UDS services by implementing a simple, logical interface, not by parsing bytes. -- **Action:** Design and implement a `UdsServiceHandlerT` that provides strongly-typed `handle` methods (e.g., `handle(uint16_t did)` for RDBI). - -#### 3. **Modernized DoIP Client Library** -The legacy C++ client is a major gap. A modern, simple C++ client is needed for examples and testing. -- **Goal:** Enable pure C++ round-trip examples and automated tests. -- **Action:** Create a new `DoIPClient` with a clean, high-level API (`connect`, `sendDiagnostic`, etc.). - ---- - -### High Priority - -#### 4. **UDS Tutorial Documentation** -With the new high-level abstraction in place, create a tutorial demonstrating how to add a custom UDS service. -- **Goal:** Enable a student to add a custom DID handler in under 30 minutes. -- **Action:** Write `docs/tutorials/Creating_UDS_Services.md`. - -#### 5. **DoIPSimulator Helper Class** -A helper class to manage multiple simulated ECUs. -- **Goal:** Simplify the creation of multi-ECU test environments for HIL simulation or lab work. -- **Action:** Implement the `DoIPSimulator` class as proposed in the previous review. - ---- - -### Medium Priority 🔨 - -#### 6. **Configuration File Support (YAML)** -Allow server configuration via a `config.yaml` file instead of just command-line arguments. -- **Value:** Simplifies test setup and configuration for non-programmers. - -#### 7. **Python Bindings (pybind11)** -Expose the server library (and the new `DoIPSimulator`) to Python. -- **Value:** Unlocks the library for the vast Python-based automotive testing ecosystem. - ---- - -## 9) Critical Action Items (Updated Roadmap) - -### Immediate (Next Sprint) 🎓 -1. **Create `examples/minimal/` directory** with a "5-minute tutorial" and a dead-simple `minimal_server.cpp`. **(Highest Priority)** -2. **Design & Implement High-Level UDS Handler**: Create the new `UdsServiceHandlerT` abstraction to hide byte parsing from the user. -3. **Refactor one existing service** (e.g., `ReadDataByIdentifier`) to use the new high-level handler as a proof-of-concept. - -### Short-term (This Quarter) -4. **Write UDS Service Tutorial**: Create `docs/tutorials/Creating_UDS_Services.md` based on the new, simpler abstraction. -5. **Modernize DoIPClient**: Begin implementation of the new C++ client library. -6. **Create docs/Architecture.md**: Add component and threading diagrams. - -### Medium-term (3-6 months) -7. **Implement `DoIPSimulator` helper class** for multi-ECU simulation. -8. **Add YAML configuration support**. -9. **Develop Python bindings** for the server and simulator. - -### Long-term (Future) -10. **Add Statistics & Monitoring API**. -11. **Implement Graceful Shutdown** signal handling. -12. Explore **TLS and WebSocket transports**. - ---- - -## 10) Conclusion - -The project is technically excellent but needs a strong focus on **developer experience** to achieve wider adoption in educational and prototyping settings. The immediate priority should be to drastically simplify the two most common user journeys: **(1) starting a server for the first time**, and **(2) adding a custom UDS service**. - -By implementing the high-priority action items, we can significantly lower the barrier to entry and make this the go-to library for DoIP simulation and education. - ---- - -## 11) Product Requirements Summary - -### Required Features by Priority - -#### MUST HAVE (Release Blocking) - -**Feature 1: Minimal Server Example** -- **User Story:** "As a new developer, I want to run a DoIP server in less than 5 minutes without understanding CAN, UDS, or advanced C++." -- **Acceptance Criteria:** - - ✅ Example file under 30 lines of code - - ✅ No external dependencies beyond the library itself - - ✅ Executable runs immediately with `./examples/minimal/minimal_server --vin WVWZZZ1KZ8W000001` - - ✅ Responds to at least one diagnostic request - - ✅ Clear error messages if dependencies are missing -- **Estimated Effort:** 2-3 days -- **Success Metric:** New developers can get a working server in under 5 minutes on first try - -**Feature 2: High-Level UDS Service Abstraction** -- **User Story:** "As a developer, I want to add a custom UDS service (e.g., Read DID) by writing business logic, not by parsing bytes." -- **Acceptance Criteria:** - - ✅ Template-based handler (`UdsServiceHandlerT`) - - ✅ No manual byte array manipulation required - - ✅ Strongly-typed parameters (e.g., `handle(uint16_t did)` for RDBI) - - ✅ Automatic response SID generation - - ✅ Built-in validation helpers - - ✅ At least one reference implementation (RDBI handler) -- **Estimated Effort:** 1 week -- **Success Metric:** A new developer can implement a custom UDS service in under 30 minutes - ---- - -#### SHOULD HAVE (High Priority) - -**Feature 3: Modernized C++ DoIP Client** -- **User Story:** "As a test engineer, I want to write C++ integration tests without relying on external Python tools." -- **Requirements:** - - High-level API: `connect()`, `activateRouting()`, `sendDiagnostic()` - - Automatic message framing and timeout handling - - Connection state management - - Statistics tracking (messages sent/received, latency) - - Example code for basic round-trip communication -- **Acceptance Criteria:** - - ✅ Clean, intuitive API (max 5 methods for basic use) - - ✅ Proper error handling with exceptions - - ✅ Thread-safe implementation - - ✅ Working example with server integration -- **Estimated Effort:** 1-2 weeks -- **Success Metric:** Can write a complete C++ test without external tools - -**Feature 4: UDS Service Creation Tutorial** -- **User Story:** "As a student, I want a step-by-step guide to understand and implement my first custom UDS service." -- **Deliverables:** - - Tutorial document: `docs/tutorials/Creating_UDS_Services.md` - - Real-world example (Temperature sensor DID simulation) - - Common pitfalls section - - UDS response code reference table - - Video walkthrough (15 minutes) -- **Estimated Effort:** 3-4 days -- **Success Metric:** 80% of first-time users can follow tutorial successfully - -**Feature 5: Multi-ECU Simulator Helper Class** -- **User Story:** "As a test automation engineer, I want to easily simulate multiple ECUs with different DIDs and behaviors." -- **API Design:** - ```cpp - DoIPSimulator sim; - sim.addECU("Engine", 0x0010, {{0xF190, vin_data}}) - .addECU("TCU", 0x0018, {{0xF190, vin_data}}); - sim.start(); - // Simulate for testing... - sim.stop(); - ``` -- **Acceptance Criteria:** - - ✅ Support 5+ simultaneous ECUs - - ✅ Per-ECU DID configuration - - ✅ Custom callback handlers for complex behavior - - ✅ Message statistics and logging - - ✅ Clean RAII semantics (auto-cleanup) -- **Estimated Effort:** 1 week -- **Success Metric:** Multi-ECU HIL tests require <50 lines of setup code - ---- - -#### NICE TO HAVE (Medium Priority) - -**Feature 6: YAML Configuration Support** -- Allow non-programmers to configure servers via YAML files -- Example: `config.yaml` for VIN, EID, GID, ports, logging -- Estimated Effort: 3-4 days -- Success Metric: Configuration via file is faster than code changes - -**Feature 7: Statistics & Monitoring API** -- Real-time statistics: connections, messages sent/received, bytes, errors -- Per-connection metrics (latency, throughput) -- Performance profiling hooks -- Estimated Effort: 4-5 days - -**Feature 8: Python Bindings (pybind11)** -- Expose core classes to Python (Server, Simulator, Client) -- Enable ecosystem integration with pytest, automation frameworks -- Estimated Effort: 2 weeks -- Value: Bridges Python test engineers to C++ library - ---- - -### Feature Success Metrics - -| Feature | Target Users | Success Criteria | Timeline | -|---------|--------------|------------------|----------| -| Minimal Example | New developers | <5 min setup time | Week 1 | -| UDS Abstraction | UDS developers | <30 min to implement service | Week 2 | -| C++ Client | Test engineers | Full round-trip test possible | Week 3-4 | -| UDS Tutorial | Students | 80% completion rate | Week 2 | -| Multi-ECU Helper | Test automation | <50 LOC for 5-ECU setup | Week 4 | - ---- - -### Product Roadmap Timeline - -**Phase 1: Foundation (4 weeks)** 🎯 -- Minimal server example ✓ -- High-level UDS service abstraction ✓ -- UDS creation tutorial ✓ -- Modernized C++ client ✓ - -**Phase 2: Ecosystem (6-8 weeks)** -- Multi-ECU simulator helper -- YAML configuration support -- Statistics & monitoring API - -**Phase 3: Integration (8+ weeks)** -- Python bindings -- WebSocket transport -- TLS support -- Community building (Discord, forums) - ---- - -### Developer Experience Metrics (To Track) - -1. **Time to First Success (TTFS):** Minutes to get a working server -2. **Cognitive Load:** UDS service creation complexity (0-10 scale) -3. **Documentation Quality:** % of questions answered by existing docs -4. **Adoption Rate:** GitHub stars, forks, issues per month -5. **Community Engagement:** GitHub discussions, issues, PRs -6. **Educational Use:** University projects using library - ---- - -### Success Definition - -**After Implementing Phase 1 Features:** -- ✅ New developer can run server in <5 minutes -- ✅ New developer can add custom UDS service in <30 minutes -- ✅ Test engineers can write pure C++ integration tests -- ✅ Students can understand DoIP by reading code + tutorial -- ✅ Library reaches 500+ GitHub stars (up from current) -- ✅ First educational institutions adopting in curriculum - -**After Implementing Phase 2 Features:** -- ✅ Multi-ECU simulation becomes standard practice -- ✅ 50+ custom UDS services created by community -- ✅ Used in university capstone projects - -**After Implementing Phase 3 Features:** -- ✅ Python test automation framework built on library -- ✅ Secure diagnostics (TLS) for production use cases -- ✅ Active community with 50+ contributors \ No newline at end of file From 27a948406ecc4ad3174c04d5c1d99fc66d169256 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 14:26:37 +0100 Subject: [PATCH 25/44] fix: Code cleanup --- CMakeLists.txt | 2 +- examples/minimal/CMakeLists.txt | 20 +++ examples/minimal/run-test.sh | 10 ++ inc/tp/IServerTransport.h | 17 +- inc/tp/MockServerTransport.h | 24 --- inc/tp/TcpServerTransport.h | 2 - src/tp/MockServerTransport.cpp | 21 --- src/tp/TcpServerTransport.cpp | 8 - test/integration/discover/CMakeLists.txt | 172 ++++++++++--------- test/integration/uds-download/CMakeLists.txt | 17 +- 10 files changed, 131 insertions(+), 162 deletions(-) create mode 100644 examples/minimal/run-test.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index de6f8ec..2d35555 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Build options option(WITH_UNIT_TEST "Build unit tests" ON) -option(WITH_INTEGRATION_TEST "Enable integration tests" ON) +option(WITH_INTEGRATION_TEST "Enable integration tests" ON) # Enable test coverage reporting option(WITH_TEST_COV "Enable test coverage reporting " OFF) option(WITH_EXAMPLES "Build examples" ON) diff --git a/examples/minimal/CMakeLists.txt b/examples/minimal/CMakeLists.txt index 70801d4..3315e73 100644 --- a/examples/minimal/CMakeLists.txt +++ b/examples/minimal/CMakeLists.txt @@ -24,3 +24,23 @@ foreach(demo_source ${DEMO_SOURCES}) # Disable switch-default warning for examples using spdlog target_compile_options(${demo_name} PRIVATE -Wno-switch-default) endforeach() + +if (WITH_INTEGRATION_TEST) + configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) + + add_custom_target( + copy_script_minimal ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh + ) + + add_test( + NAME uds-minimal-test + COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(uds-minimal-test PROPERTIES + TIMEOUT 20 + DEPENDS copy_script_minimal + LABELS "integration" + ) +endif() \ No newline at end of file diff --git a/examples/minimal/run-test.sh b/examples/minimal/run-test.sh new file mode 100644 index 0000000..7d74b94 --- /dev/null +++ b/examples/minimal/run-test.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e +./MinimalDoIPServer & +SERVER_PID=$! +sleep 1 + +# Run tests +./MinimalDoIPClient + +kill $SERVER_PID \ No newline at end of file diff --git a/inc/tp/IServerTransport.h b/inc/tp/IServerTransport.h index 5bb924a..f74379e 100644 --- a/inc/tp/IServerTransport.h +++ b/inc/tp/IServerTransport.h @@ -12,12 +12,11 @@ namespace doip { struct ServerConfig; /** - * @brief Server-level transport abstraction for DoIP + * @brief Server-level TCP transport abstraction for DoIP * * This interface handles server-level operations: - * - Setting up listener sockets (TCP, UDP) + * - Setting up TCP listener socket * - Accepting incoming connections - * - Broadcasting UDP announcements * - Server lifecycle management * * Implementations: TcpServerTransport, MockServerTransport @@ -48,18 +47,6 @@ class IServerTransport { */ virtual std::unique_ptr acceptConnection() = 0; - /** - * @brief Send a broadcast/announcement message (UDP) - * - * For TCP: Sends UDP broadcast on port 13400 - * For Mock: Stores message for inspection - * - * @param msg The DoIP message to broadcast (typically vehicle announcement) - * @param port The destination port for broadcast - * @return Number of bytes sent, or -1 on error - */ - //virtual ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port) = 0; - /** * @brief Close the server transport and cleanup resources * diff --git a/inc/tp/MockServerTransport.h b/inc/tp/MockServerTransport.h index f3e9886..725b9d7 100644 --- a/inc/tp/MockServerTransport.h +++ b/inc/tp/MockServerTransport.h @@ -43,27 +43,6 @@ class MockServerTransport : public IServerTransport { */ void injectConnection(std::unique_ptr connection); - /** - * @brief Get the next broadcast message that was sent - * - * @return The broadcast message, or std::nullopt if queue is empty - */ - std::optional popBroadcast(); - - /** - * @brief Check if any broadcast messages have been sent - * - * @return true if broadcast queue is not empty - */ - bool hasBroadcasts() const; - - /** - * @brief Get the number of broadcast messages in the queue - * - * @return Number of broadcasts waiting to be read - */ - size_t broadcastCount() const; - /** * @brief Clear all queues (connections and broadcasts) */ @@ -76,9 +55,6 @@ class MockServerTransport : public IServerTransport { // Queue for injected connections ThreadSafeQueue> m_connectionQueue; - - // Queue for broadcast messages - ThreadSafeQueue m_broadcastQueue; }; } // namespace doip diff --git a/inc/tp/TcpServerTransport.h b/inc/tp/TcpServerTransport.h index 94f1ded..b465829 100644 --- a/inc/tp/TcpServerTransport.h +++ b/inc/tp/TcpServerTransport.h @@ -37,7 +37,6 @@ class TcpServerTransport : public IServerTransport { // IServerTransport interface bool setup(uint16_t port) override; std::unique_ptr acceptConnection() override; - //--ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port) override; void close() override; bool isActive() const override; std::string getIdentifier() const override; @@ -51,7 +50,6 @@ class TcpServerTransport : public IServerTransport { std::shared_ptr m_log; struct sockaddr_in m_serverAddress{}; - //--struct sockaddr_in m_broadcastAddress{}; /** * @brief Set up TCP server socket (bind + listen) diff --git a/src/tp/MockServerTransport.cpp b/src/tp/MockServerTransport.cpp index 468e94a..610affa 100644 --- a/src/tp/MockServerTransport.cpp +++ b/src/tp/MockServerTransport.cpp @@ -44,25 +44,4 @@ void MockServerTransport::injectConnection(std::unique_ptr MockServerTransport::popBroadcast() { - DoIPMessage msg; - if (m_broadcastQueue.tryPop(msg)) { - return msg; - } - return std::nullopt; -} - -bool MockServerTransport::hasBroadcasts() const { - return !m_broadcastQueue.empty(); -} - -size_t MockServerTransport::broadcastCount() const { - return m_broadcastQueue.size(); -} - -void MockServerTransport::clearQueues() { - m_connectionQueue.clear(); - m_broadcastQueue.clear(); -} - } // namespace doip diff --git a/src/tp/TcpServerTransport.cpp b/src/tp/TcpServerTransport.cpp index 7a53ed3..a448955 100644 --- a/src/tp/TcpServerTransport.cpp +++ b/src/tp/TcpServerTransport.cpp @@ -25,7 +25,6 @@ void TcpServerTransport::closeSocket() { if (m_log) m_log->info("Closing TCP server transport (destructor)"); m_tcpServerSocket.close(); - //m_udpSocket.close(); } } @@ -38,13 +37,6 @@ bool TcpServerTransport::setup(uint16_t port) { return false; } - // if (!setupUdpSocket()) { - // m_log->error("Failed to setup UDP socket"); - // m_udpSocket.close(); - // return false; - // } - - // configureBroadcast(); m_isActive = true; m_log->info("TCP server transport ready on port {}", port); return true; diff --git a/test/integration/discover/CMakeLists.txt b/test/integration/discover/CMakeLists.txt index 5502880..14401d7 100644 --- a/test/integration/discover/CMakeLists.txt +++ b/test/integration/discover/CMakeLists.txt @@ -38,92 +38,94 @@ foreach(demo_source ${DISCOVER_SOURCES}) endforeach() -message(STATUS "Adding integration tests for discover examples") +if (WITH_INTEGRATION_TEST) + message(STATUS "Adding integration tests for discover examples") -# Integration fixture: start DoIPServer as a daemon -add_test(NAME Integration_StartDiscoverServer - COMMAND bash -c "$ --daemon && sleep 1.0" -) -set_tests_properties(Integration_StartDiscoverServer PROPERTIES - FIXTURES_SETUP demo_server - TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE -) + # Integration fixture: start DoIPServer as a daemon + add_test(NAME Integration_StartDiscoverServer + COMMAND bash -c "$ --daemon && sleep 1.0" + ) + set_tests_properties(Integration_StartDiscoverServer PROPERTIES + FIXTURES_SETUP demo_server + TIMEOUT 20 + LABELS integration + RUN_SERIAL TRUE + ) -# Wait until both UDP 13400 and TCP 13400 are bound before running client tests -# This prevents race conditions where tests start before the server is ready -# Simplified script that works reliably in CI environments -add_test(NAME Integration_WaitForServerReady - COMMAND bash -c " -echo 'Waiting for server to bind ports...' -sleep 5 - -# First check if server process exists -if ! pgrep -f Discover_Server >/dev/null 2>&1; then - echo 'ERROR: Discover_Server process not running!' >&2 - exit 1 -fi - -for i in {1..150}; do - if command -v ss >/dev/null 2>&1; then - # Linux: use ss (without -p to avoid privilege restrictions) - udp_check=`ss -uln 2>/dev/null | grep -c ':13400'` - tcp_check=`ss -tln 2>/dev/null | grep -c ':13400'` - else - # macOS/BSD: use netstat - udp_check=`netstat -an 2>/dev/null | grep -c 'udp.*\\.13400'` - tcp_check=`netstat -an 2>/dev/null | grep -c '\\.13400.*LISTEN'` - fi - - if [ \$udp_check -gt 0 ] && [ \$tcp_check -gt 0 ]; then - echo 'Server ready: both UDP and TCP port 13400 are bound' - # Give server additional time to start sending announcements (important for sanitizers) - sleep 2 - exit 0 - fi - - if [ \$i -eq 50 ]; then - echo \"Still waiting... (UDP check: \$udp_check, TCP check: \$tcp_check)\" - pgrep -f Discover_Server >/dev/null 2>&1 || echo 'WARNING: Server process died!' - fi - - sleep 0.1 -done - -echo 'ERROR: Server not ready after 15 seconds (UDP: '\$udp_check', TCP: '\$tcp_check')' >&2 -pgrep -f Discover_Server >/dev/null 2>&1 || echo 'Server process is NOT running' >&2 -exit 1 -" -) -set_tests_properties(Integration_WaitForServerReady PROPERTIES - FIXTURES_REQUIRED demo_server - FIXTURES_SETUP demo_ready - TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE -) + # Wait until both UDP 13400 and TCP 13400 are bound before running client tests + # This prevents race conditions where tests start before the server is ready + # Simplified script that works reliably in CI environments + add_test(NAME Integration_WaitForServerReady + COMMAND bash -c " + echo 'Waiting for server to bind ports...' + sleep 5 + + # First check if server process exists + if ! pgrep -f Discover_Server >/dev/null 2>&1; then + echo 'ERROR: Discover_Server process not running!' >&2 + exit 1 + fi + + for i in {1..150}; do + if command -v ss >/dev/null 2>&1; then + # Linux: use ss (without -p to avoid privilege restrictions) + udp_check=`ss -uln 2>/dev/null | grep -c ':13400'` + tcp_check=`ss -tln 2>/dev/null | grep -c ':13400'` + else + # macOS/BSD: use netstat + udp_check=`netstat -an 2>/dev/null | grep -c 'udp.*\\.13400'` + tcp_check=`netstat -an 2>/dev/null | grep -c '\\.13400.*LISTEN'` + fi + + if [ \$udp_check -gt 0 ] && [ \$tcp_check -gt 0 ]; then + echo 'Server ready: both UDP and TCP port 13400 are bound' + # Give server additional time to start sending announcements (important for sanitizers) + sleep 2 + exit 0 + fi + + if [ \$i -eq 50 ]; then + echo \"Still waiting... (UDP check: \$udp_check, TCP check: \$tcp_check)\" + pgrep -f Discover_Server >/dev/null 2>&1 || echo 'WARNING: Server process died!' + fi + + sleep 0.1 + done + + echo 'ERROR: Server not ready after 15 seconds (UDP: '\$udp_check', TCP: '\$tcp_check')' >&2 + pgrep -f Discover_Server >/dev/null 2>&1 || echo 'Server process is NOT running' >&2 + exit 1 + " + ) + set_tests_properties(Integration_WaitForServerReady PROPERTIES + FIXTURES_REQUIRED demo_server + FIXTURES_SETUP demo_ready + TIMEOUT 20 + LABELS integration + RUN_SERIAL TRUE + ) -# Integration test: run discover (udp client) against the running server -# CRITICAL: Must run serially and immediately after server is ready to catch announcements -add_test(NAME Integration_DiscoverClient_Runs - COMMAND $ -) -set_tests_properties(Integration_DiscoverClient_Runs PROPERTIES - FIXTURES_REQUIRED demo_ready - TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE - DEPENDS Integration_WaitForServerReady -) + # Integration test: run discover (udp client) against the running server + # CRITICAL: Must run serially and immediately after server is ready to catch announcements + add_test(NAME Integration_DiscoverClient_Runs + COMMAND $ + ) + set_tests_properties(Integration_DiscoverClient_Runs PROPERTIES + FIXTURES_REQUIRED demo_ready + TIMEOUT 20 + LABELS integration + RUN_SERIAL TRUE + DEPENDS Integration_WaitForServerReady + ) -# Teardown fixture: stop server and wait for ports to be released -add_test(NAME Integration_StopDiscoverServer - COMMAND bash -c "if [ -f /tmp/doip-discover.pid ]; then kill $(cat /tmp/doip-discover.pid) 2>/dev/null; rm -f /tmp/doip-discover.pid; else pkill -f Discover_Server 2>/dev/null || true; fi; for i in $(seq 1 50); do if command -v ss >/dev/null 2>&1; then ss -tlpn 2>/dev/null | grep -q ':13400' || exit 0; else netstat -an 2>/dev/null | grep -q '\\.13400.*LISTEN' || exit 0; fi; sleep 0.1; done; echo 'Warning: Port 13400 still in use' >&2; exit 0" -) -set_tests_properties(Integration_StopDiscoverServer PROPERTIES - FIXTURES_CLEANUP demo_server - TIMEOUT 10 - LABELS integration - RUN_SERIAL TRUE -) + # Teardown fixture: stop server and wait for ports to be released + add_test(NAME Integration_StopDiscoverServer + COMMAND bash -c "if [ -f /tmp/doip-discover.pid ]; then kill $(cat /tmp/doip-discover.pid) 2>/dev/null; rm -f /tmp/doip-discover.pid; else pkill -f Discover_Server 2>/dev/null || true; fi; for i in $(seq 1 50); do if command -v ss >/dev/null 2>&1; then ss -tlpn 2>/dev/null | grep -q ':13400' || exit 0; else netstat -an 2>/dev/null | grep -q '\\.13400.*LISTEN' || exit 0; fi; sleep 0.1; done; echo 'Warning: Port 13400 still in use' >&2; exit 0" + ) + set_tests_properties(Integration_StopDiscoverServer PROPERTIES + FIXTURES_CLEANUP demo_server + TIMEOUT 10 + LABELS integration + RUN_SERIAL TRUE + ) +endif() # WITH_INTEGRATION_TEST \ No newline at end of file diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index 5a10226..f8715f3 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -48,7 +48,7 @@ add_custom_command( # Install packages add_custom_command( OUTPUT ${VENV_DIR}/installed - COMMAND ${VENV_DIR}/bin/pip install udsoncan doipclient + COMMAND ${VENV_DIR}/bin/pip3 install udsoncan doipclient DEPENDS ${VENV_DIR} COMMENT "Installing Python packages in venv" BYPRODUCTS ${VENV_DIR}/installed @@ -82,8 +82,13 @@ add_custom_target(copy_script ALL DEPENDS ${DESTINATION_SCRIPT}) message(STATUS "Adding integration tests for examples") -add_test( - NAME uds-download-test - COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} -) +if (WITH_INTEGRATION_TEST) + add_test( + NAME uds-download-test + COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(uds-download-test PROPERTIES + DEPENDS "setup_venv;copy_script" + ) +endif() \ No newline at end of file From 6db51e12f22957b1740dbdc10f472c44f5a47ba3 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 15:09:25 +0100 Subject: [PATCH 26/44] feat: Add integration test setup and scripts for DoIP client functionality --- CMakeLists.txt | 7 +++- cmake/python-venv.cmake | 25 +++++++++++++ examples/discover/CMakeLists.txt | 33 ++++++++++++++++- examples/discover/run-test.sh | 26 ++++++++++++++ examples/minimal/CMakeLists.txt | 8 ++--- examples/minimal/run-test.sh | 6 +++- test/integration/uds-download/CMakeLists.txt | 35 +++---------------- ...t-local-client.py => test-uds-download.py} | 0 8 files changed, 102 insertions(+), 38 deletions(-) create mode 100644 cmake/python-venv.cmake create mode 100644 examples/discover/run-test.sh rename test/integration/uds-download/{test-local-client.py => test-uds-download.py} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d35555..11ddac9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Build options -option(WITH_UNIT_TEST "Build unit tests" ON) +option(WITH_UNIT_TEST "Enable unit tests" ON) option(WITH_INTEGRATION_TEST "Enable integration tests" ON) # Enable test coverage reporting option(WITH_TEST_COV "Enable test coverage reporting " OFF) @@ -43,6 +43,11 @@ endif() include(cmake/projectSettings.cmake) +# Include python virtual environment setup for integration tests +if (WITH_INTEGRATION_TEST) + include(cmake/python-venv.cmake) +endif() + # Find required packages find_package(Threads REQUIRED) diff --git a/cmake/python-venv.cmake b/cmake/python-venv.cmake new file mode 100644 index 0000000..66bf3d6 --- /dev/null +++ b/cmake/python-venv.cmake @@ -0,0 +1,25 @@ +# Paths +set(VENV_DIR ${CMAKE_CURRENT_BINARY_DIR}/venv) +set(PYTHON_EXECUTABLE python3) # or specify a full path + +# Create venv +add_custom_command( + OUTPUT ${VENV_DIR} + COMMAND ${PYTHON_EXECUTABLE} -m venv ${VENV_DIR} + COMMENT "Creating Python virtual environment" +) + +# Install packages +add_custom_command( + OUTPUT ${VENV_DIR}/installed + COMMAND ${VENV_DIR}/bin/pip3 install udsoncan doipclient + DEPENDS ${VENV_DIR} + COMMENT "Installing Python packages in venv" + BYPRODUCTS ${VENV_DIR}/installed +) + +# Custom target to set up venv and packages +add_custom_target( + setup_venv ALL + DEPENDS ${VENV_DIR}/installed +) diff --git a/examples/discover/CMakeLists.txt b/examples/discover/CMakeLists.txt index a51b86f..294c29f 100644 --- a/examples/discover/CMakeLists.txt +++ b/examples/discover/CMakeLists.txt @@ -9,4 +9,35 @@ set_target_properties(DoIPUdpServer PROPERTIES ) configure_file(discover-client.py ${CMAKE_CURRENT_BINARY_DIR}/discover-client.py COPYONLY) -configure_file(discover-client.py ${CMAKE_BINARY_DIR}/discover-client.py COPYONLY) \ No newline at end of file +configure_file(discover-client.py ${CMAKE_BINARY_DIR}/discover-client.py COPYONLY) + +set(SOURCE_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/discover-client.py) +set(DESTINATION_SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/discover-client.py) +configure_file(${SOURCE_SCRIPT} ${DESTINATION_SCRIPT} COPYONLY) + +configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) + +# Add a custom command to copy the file only if it changes +add_custom_command( + OUTPUT ${DESTINATION_SCRIPT} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${SOURCE_SCRIPT} + ${DESTINATION_SCRIPT} + DEPENDS ${SOURCE_SCRIPT} + COMMENT "Copying ${SOURCE_SCRIPT} to binary directory (if changed)" +) + +# Optionally, add the output to a target to ensure it's built +add_custom_target(copy_discover_script ALL DEPENDS ${DESTINATION_SCRIPT}) + + +if (WITH_INTEGRATION_TEST) + add_test( + NAME Integration_UdpDiscover + COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(Integration_UdpDiscover PROPERTIES + DEPENDS "setup_venv;copy_discover_script" + ) +endif() \ No newline at end of file diff --git a/examples/discover/run-test.sh b/examples/discover/run-test.sh new file mode 100644 index 0000000..d9560df --- /dev/null +++ b/examples/discover/run-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Activate virtual environment +source venv/bin/activate + +# Start server in background, log to server.log +./DoIPUdpServer > server.log 2>&1 & +SERVER_PID=$! + +# Give the server a moment to start +sleep 1 + +# Run client, log to client.log +python3 ./discover-client.py --loopback > client.log 2>&1 +CLIENT_EXIT=$? + +# Kill server +kill $SERVER_PID + +# Print logs for debugging +echo "=== Server Log ===" +tail server.log +echo "=== Client Log ===" +cat client.log + +# Return client's exit code +exit $CLIENT_EXIT \ No newline at end of file diff --git a/examples/minimal/CMakeLists.txt b/examples/minimal/CMakeLists.txt index 3315e73..f1bf358 100644 --- a/examples/minimal/CMakeLists.txt +++ b/examples/minimal/CMakeLists.txt @@ -29,18 +29,18 @@ if (WITH_INTEGRATION_TEST) configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) add_custom_target( - copy_script_minimal ALL + copy_test_script_minimal_cs ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh ) add_test( - NAME uds-minimal-test + NAME Integration_UdsDownload COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) - set_tests_properties(uds-minimal-test PROPERTIES + set_tests_properties(Integration_UdsDownload PROPERTIES TIMEOUT 20 - DEPENDS copy_script_minimal + DEPENDS copy_test_script_minimal_cs LABELS "integration" ) endif() \ No newline at end of file diff --git a/examples/minimal/run-test.sh b/examples/minimal/run-test.sh index 7d74b94..e5559fd 100644 --- a/examples/minimal/run-test.sh +++ b/examples/minimal/run-test.sh @@ -6,5 +6,9 @@ sleep 1 # Run tests ./MinimalDoIPClient +CLIENT_EXIT=$? -kill $SERVER_PID \ No newline at end of file +kill $SERVER_PID + +# Return client's exit code +exit $CLIENT_EXIT \ No newline at end of file diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index f8715f3..5ef86a0 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -34,36 +34,9 @@ foreach(demo_source ${DISCOVER_SOURCES}) endforeach() -# Paths -set(VENV_DIR ${CMAKE_CURRENT_BINARY_DIR}/venv) -set(PYTHON_EXECUTABLE python3) # or specify a full path - -# Create venv -add_custom_command( - OUTPUT ${VENV_DIR} - COMMAND ${PYTHON_EXECUTABLE} -m venv ${VENV_DIR} - COMMENT "Creating Python virtual environment" -) - -# Install packages -add_custom_command( - OUTPUT ${VENV_DIR}/installed - COMMAND ${VENV_DIR}/bin/pip3 install udsoncan doipclient - DEPENDS ${VENV_DIR} - COMMENT "Installing Python packages in venv" - BYPRODUCTS ${VENV_DIR}/installed -) - -# Custom target to set up venv and packages -add_custom_target( - setup_venv ALL - DEPENDS ${VENV_DIR}/installed -) - - # Define the source and destination paths -set(SOURCE_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/test-local-client.py) -set(DESTINATION_SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/test-local-client.py) +set(SOURCE_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/test-uds-download.py) +set(DESTINATION_SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/test-uds-download.py) configure_file(${SOURCE_SCRIPT} ${DESTINATION_SCRIPT} COPYONLY) configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) @@ -78,7 +51,7 @@ add_custom_command( ) # Optionally, add the output to a target to ensure it's built -add_custom_target(copy_script ALL DEPENDS ${DESTINATION_SCRIPT}) +add_custom_target(copy_uds_download_script ALL DEPENDS ${DESTINATION_SCRIPT}) message(STATUS "Adding integration tests for examples") @@ -89,6 +62,6 @@ if (WITH_INTEGRATION_TEST) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) set_tests_properties(uds-download-test PROPERTIES - DEPENDS "setup_venv;copy_script" + DEPENDS "setup_venv;copy_uds_download_script" ) endif() \ No newline at end of file diff --git a/test/integration/uds-download/test-local-client.py b/test/integration/uds-download/test-uds-download.py similarity index 100% rename from test/integration/uds-download/test-local-client.py rename to test/integration/uds-download/test-uds-download.py From deaa9b91e02e32be410dff46b7a24e53172333b3 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 15:14:48 +0100 Subject: [PATCH 27/44] test: Add unit test for mock server transport --- src/tp/MockServerTransport.cpp | 5 + test/unit/CMakeLists.txt | 1 + test/unit/tp/MockServerTransport_Test.cpp | 331 ++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 test/unit/tp/MockServerTransport_Test.cpp diff --git a/src/tp/MockServerTransport.cpp b/src/tp/MockServerTransport.cpp index 610affa..8f53cf6 100644 --- a/src/tp/MockServerTransport.cpp +++ b/src/tp/MockServerTransport.cpp @@ -44,4 +44,9 @@ void MockServerTransport::injectConnection(std::unique_ptr + +using namespace doip; + +TEST_SUITE("MockServerTransport") { + TEST_CASE("MockServerTransport construction and basic properties") { + SUBCASE("Default identifier") { + MockServerTransport transport; + + CHECK_FALSE(transport.isActive()); + CHECK(transport.getIdentifier() == "mock-server:0"); + } + + SUBCASE("Custom identifier") { + MockServerTransport transport("custom-test-server"); + + CHECK_FALSE(transport.isActive()); + CHECK(transport.getIdentifier() == "custom-test-server:0"); + } + } + + TEST_CASE("MockServerTransport setup") { + MockServerTransport transport("test-server"); + + SUBCASE("Setup activates transport and sets port") { + bool result = transport.setup(13400); + + CHECK(result); + CHECK(transport.isActive()); + CHECK(transport.getIdentifier() == "test-server:13400"); + } + + SUBCASE("Setup with different ports") { + transport.setup(8080); + CHECK(transport.getIdentifier() == "test-server:8080"); + + transport.setup(3000); + CHECK(transport.getIdentifier() == "test-server:3000"); + } + + SUBCASE("Setup always succeeds") { + // Mock transport should always succeed setup + CHECK(transport.setup(0)); + CHECK(transport.setup(65535)); + } + } + + TEST_CASE("MockServerTransport acceptConnection without injected connections") { + MockServerTransport transport("test-server"); + transport.setup(13400); + + SUBCASE("Returns nullptr when no connections queued") { + auto conn = transport.acceptConnection(); + CHECK(conn == nullptr); + } + + SUBCASE("Multiple calls return nullptr when queue is empty") { + CHECK(transport.acceptConnection() == nullptr); + CHECK(transport.acceptConnection() == nullptr); + CHECK(transport.acceptConnection() == nullptr); + } + } + + TEST_CASE("MockServerTransport acceptConnection with injected connections") { + MockServerTransport transport("test-server"); + transport.setup(13400); + + SUBCASE("Single injected connection") { + auto mockConn = std::make_unique("client-1"); + transport.injectConnection(std::move(mockConn)); + + auto conn = transport.acceptConnection(); + REQUIRE(conn != nullptr); + CHECK(conn->getIdentifier() == "client-1"); + + // Queue should be empty now + CHECK(transport.acceptConnection() == nullptr); + } + + SUBCASE("Multiple injected connections are returned in order") { + transport.injectConnection(std::make_unique("client-1")); + transport.injectConnection(std::make_unique("client-2")); + transport.injectConnection(std::make_unique("client-3")); + + auto conn1 = transport.acceptConnection(); + REQUIRE(conn1 != nullptr); + CHECK(conn1->getIdentifier() == "client-1"); + + auto conn2 = transport.acceptConnection(); + REQUIRE(conn2 != nullptr); + CHECK(conn2->getIdentifier() == "client-2"); + + auto conn3 = transport.acceptConnection(); + REQUIRE(conn3 != nullptr); + CHECK(conn3->getIdentifier() == "client-3"); + + // Queue should be empty now + CHECK(transport.acceptConnection() == nullptr); + } + + SUBCASE("Inject connections after accepting some") { + transport.injectConnection(std::make_unique("client-1")); + + auto conn1 = transport.acceptConnection(); + REQUIRE(conn1 != nullptr); + CHECK(conn1->getIdentifier() == "client-1"); + + // Inject another connection + transport.injectConnection(std::make_unique("client-2")); + + auto conn2 = transport.acceptConnection(); + REQUIRE(conn2 != nullptr); + CHECK(conn2->getIdentifier() == "client-2"); + } + } + + TEST_CASE("MockServerTransport close functionality") { + MockServerTransport transport("test-server"); + transport.setup(13400); + + SUBCASE("Close deactivates transport") { + CHECK(transport.isActive()); + + transport.close(); + + CHECK_FALSE(transport.isActive()); + } + + SUBCASE("Close clears connection queue") { + transport.injectConnection(std::make_unique("client-1")); + transport.injectConnection(std::make_unique("client-2")); + + transport.close(); + + // After close, queue should be cleared + CHECK(transport.acceptConnection() == nullptr); + } + + SUBCASE("acceptConnection returns nullptr after close") { + transport.injectConnection(std::make_unique("client-1")); + + transport.close(); + + auto conn = transport.acceptConnection(); + CHECK(conn == nullptr); + } + + SUBCASE("Can close multiple times") { + transport.close(); + CHECK_FALSE(transport.isActive()); + + transport.close(); // Second close should be safe + CHECK_FALSE(transport.isActive()); + } + } + + TEST_CASE("MockServerTransport isActive state transitions") { + MockServerTransport transport("test-server"); + + SUBCASE("Initial state is inactive") { + CHECK_FALSE(transport.isActive()); + } + + SUBCASE("Active after setup") { + transport.setup(13400); + CHECK(transport.isActive()); + } + + SUBCASE("Inactive after close") { + transport.setup(13400); + transport.close(); + CHECK_FALSE(transport.isActive()); + } + + SUBCASE("Can reactivate after close by calling setup again") { + transport.setup(13400); + CHECK(transport.isActive()); + + transport.close(); + CHECK_FALSE(transport.isActive()); + + transport.setup(8080); + CHECK(transport.isActive()); + } + } + + TEST_CASE("MockServerTransport getIdentifier formatting") { + SUBCASE("Identifier includes port") { + MockServerTransport transport("my-server"); + transport.setup(12345); + + CHECK(transport.getIdentifier() == "my-server:12345"); + } + + SUBCASE("Identifier updates when port changes") { + MockServerTransport transport("server"); + + CHECK(transport.getIdentifier() == "server:0"); + + transport.setup(8080); + CHECK(transport.getIdentifier() == "server:8080"); + + transport.setup(9090); + CHECK(transport.getIdentifier() == "server:9090"); + } + + SUBCASE("Identifier with special characters") { + MockServerTransport transport("test-server_v2.0"); + transport.setup(3000); + + CHECK(transport.getIdentifier() == "test-server_v2.0:3000"); + } + } + + TEST_CASE("MockServerTransport clearQueues via close") { + MockServerTransport transport("test-server"); + transport.setup(13400); + + SUBCASE("clearQueues is called on close") { + // Inject multiple connections + transport.injectConnection(std::make_unique("client-1")); + transport.injectConnection(std::make_unique("client-2")); + transport.injectConnection(std::make_unique("client-3")); + + // Close should clear all queues + transport.close(); + + // Reactivate and verify queues are empty + transport.setup(13400); + CHECK(transport.acceptConnection() == nullptr); + } + } + + TEST_CASE("MockServerTransport thread safety simulation") { + MockServerTransport transport("test-server"); + transport.setup(13400); + + SUBCASE("Inject and accept in sequence") { + // Simulate multiple inject/accept cycles + for (int i = 0; i < 10; ++i) { + std::string clientName = "client-" + std::to_string(i); + transport.injectConnection(std::make_unique(clientName)); + + auto conn = transport.acceptConnection(); + REQUIRE(conn != nullptr); + CHECK(conn->getIdentifier() == clientName); + } + } + } + + TEST_CASE("MockServerTransport integration with MockConnectionTransport") { + MockServerTransport transport("test-server"); + transport.setup(13400); + + SUBCASE("Accepted connection is usable") { + auto mockConn = std::make_unique("client-1"); + + // Inject a message into the connection before injecting it + DoIPMessage testMsg(DoIPPayloadType::VehicleIdentificationRequest, nullptr, 0); + mockConn->injectMessage(testMsg); + + transport.injectConnection(std::move(mockConn)); + + // Accept the connection + auto conn = transport.acceptConnection(); + REQUIRE(conn != nullptr); + + // Verify we can receive the injected message through the connection + auto receivedMsg = conn->receiveMessage(); + REQUIRE(receivedMsg.has_value()); + CHECK(receivedMsg->getPayloadType() == DoIPPayloadType::VehicleIdentificationRequest); + } + + SUBCASE("Send message through accepted connection") { + transport.injectConnection(std::make_unique("client-1")); + + auto conn = transport.acceptConnection(); + REQUIRE(conn != nullptr); + + // Send a message through the connection + DoIPMessage msg(DoIPPayloadType::RoutingActivationResponse, nullptr, 0); + ssize_t sent = conn->sendMessage(msg); + + CHECK(sent == static_cast(msg.size())); + CHECK(conn->isActive()); + } + } + + TEST_CASE("MockServerTransport edge cases") { + SUBCASE("Accept before setup") { + MockServerTransport transport("test-server"); + + // Transport is not active, should return nullptr + auto conn = transport.acceptConnection(); + CHECK(conn == nullptr); + } + + SUBCASE("Setup with port 0") { + MockServerTransport transport("test-server"); + transport.setup(0); + + CHECK(transport.isActive()); + CHECK(transport.getIdentifier() == "test-server:0"); + } + + SUBCASE("Empty identifier") { + MockServerTransport transport(""); + transport.setup(13400); + + CHECK(transport.getIdentifier() == ":13400"); + } + + SUBCASE("Inject null behavior prevention") { + MockServerTransport transport("test-server"); + transport.setup(13400); + + // This tests the queue behavior when popping from empty queue + // after injecting and accepting a connection + transport.injectConnection(std::make_unique("client-1")); + auto conn = transport.acceptConnection(); + REQUIRE(conn != nullptr); + + // Try to accept again - should return nullptr + CHECK(transport.acceptConnection() == nullptr); + } + } +} From f7973111f162887abdfecf30c4428e8e0bac3cbc Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 17:57:22 +0100 Subject: [PATCH 28/44] fix: Fix buggy TCP socket init, remove buggy m_tcpClientSocket fix: Potential resource leak when routing act. failed refactor: Introduce ServerProperties to host VIN, EID style: Rename some tests --- examples/discover/DoIPUdpServer.cpp | 10 +- inc/DoIPAddress.h | 6 + inc/DoIPClient.h | 267 ++++++++++++++++-- inc/DoIPMessage.h | 11 +- inc/DoIPServer.h | 25 +- inc/cli/ServerConfig.h | 58 +++- src/DoIPClient.cpp | 65 ++--- src/DoIPServer.cpp | 41 ++- src/cli/ServerConfigCLI.cpp | 8 +- test/integration/discover/CMakeLists.txt | 8 +- ...Discover_Client.cpp => DiscoverClient.cpp} | 21 +- ...Discover_Server.cpp => DiscoverServer.cpp} | 10 + test/integration/uds-download/CMakeLists.txt | 2 +- .../{DoIP_Uds_Server.cpp => UdsServer.cpp} | 2 +- test/unit/DoIPServer_Test.cpp | 2 +- test/unit/cli/ServerConfigCLI_Test.cpp | 2 +- 16 files changed, 401 insertions(+), 137 deletions(-) rename test/integration/discover/{Discover_Client.cpp => DiscoverClient.cpp} (64%) rename test/integration/discover/{Discover_Server.cpp => DiscoverServer.cpp} (80%) rename test/integration/uds-download/{DoIP_Uds_Server.cpp => UdsServer.cpp} (95%) diff --git a/examples/discover/DoIPUdpServer.cpp b/examples/discover/DoIPUdpServer.cpp index 04fcc7a..5638b5d 100644 --- a/examples/discover/DoIPUdpServer.cpp +++ b/examples/discover/DoIPUdpServer.cpp @@ -10,15 +10,15 @@ int main(int argc, char *argv[]) { ServerConfig cfg; cfg.loopback = true; // For testing, use loopback announcements + // Set server properties + cfg.properties.logicalAddress = DoIPAddress(0x29); // Example logical address + cfg.properties.vin = Vin("WVWZZZ1JZ3W386752"); // Some valid VIN + cfg.properties.eid = EntityId(0x654321); + cfg.properties.gid = GroupId(0x123456); auto server = std::make_unique(cfg); auto console = spdlog::stdout_color_mt("doip-udp-server"); - // Set server properties - server->setVin("WVWZZZ1JZ3W386752"); // Some valid VIN - server->setGid(0x123456); - server->setEid(0x654321); - // Set up TCP first to ensure transport creates/binds both TCP and UDP sockets if (!server->setupTcpSocket([]() { return std::make_unique(); })) { console->critical("Failed to set up TCP socket"); diff --git a/inc/DoIPAddress.h b/inc/DoIPAddress.h index 71a3da0..7efafbe 100644 --- a/inc/DoIPAddress.h +++ b/inc/DoIPAddress.h @@ -73,6 +73,12 @@ inline DoIPAddress readAddressFrom(const uint8_t *data, size_t offset = 0) { return static_cast(data[offset] << 8 | data[offset + 1]); } +inline std::string toHex4 (const DoIPAddress &address) { + std::ostringstream os; + os << "0x" << std::hex << std::setw(4) << std::setfill('0') << static_cast(address) << std::dec; + return os.str(); +} + } // namespace doip #endif /* DOIPADDRESS_H */ diff --git a/inc/DoIPClient.h b/inc/DoIPClient.h index 5f78e06..00f608a 100644 --- a/inc/DoIPClient.h +++ b/inc/DoIPClient.h @@ -5,6 +5,7 @@ #include "DoIPClientModel.h" #include "DoIPConfig.h" #include "DoIPMessage.h" +#include "cli/ServerConfig.h" #include "util/Logger.h" #include "util/Socket.h" #include "util/ThreadSafeQueue.h" @@ -18,57 +19,202 @@ namespace doip { +/** + * @brief DoIP client implementation for diagnostic communication + * + * This class implements a DoIP (Diagnostics over IP) client according to ISO 13400. + * It handles both UDP vehicle discovery and TCP diagnostic communication. + * + * @note This is legacy code kept for testing purposes only. Not actively maintained. + * + * The client supports: + * - UDP vehicle discovery (announcements and identification requests) + * - TCP routing activation and diagnostic messaging + * - Alive check handling + * - Automatic reconnection + * + * @warning Thread-safe for enqueueing messages, but TCP/UDP threads should not be + * accessed concurrently with start/stop operations. + */ class DoIPClient { + /** + * @brief Internal state for managing message reception flow + */ enum class ReceiveState { - WaitForAckOrAliveCheck, - WaitForDiagnosticMessage, - Quit, + WaitForAckOrAliveCheck, ///< Waiting for acknowledgement or alive check from server + WaitForDiagnosticMessage, ///< Waiting for diagnostic response message + Quit, ///< Client is shutting down }; public: + /** + * @brief Constructs a DoIP client with optional custom model + * + * @param model Client model defining behavior (defaults to standard DoIPClientModel) + */ explicit DoIPClient(UniqueDoIPClientModelPtr model = std::make_unique()) : m_model(std::move(model)) { m_receiveBuf.reserve(DOIP_MAXIMUM_MTU); } + /** + * @brief Establishes TCP connection to DoIP server and activates routing + * + * Creates a TCP socket, connects to the specified server, and performs + * routing activation handshake. Starts the TCP receiver thread. + * + * @param inet_address Server IP address (default: "127.0.0.1") + * @param port Server TCP port (default: 13400) + * @return true if connection and routing activation succeeded, false otherwise + * + * @note Blocks until routing activation completes or fails + */ [[nodiscard]] bool startTcpConnection(const char *inet_address = "127.0.0.1", uint16_t port = DOIP_TCP_DEFAULT_PORT); + /** + * @brief Checks if TCP connection is active + * + * @return true if TCP thread is running, false otherwise + */ [[nodiscard]] bool isTcpRunning() const noexcept { return m_tcpRunning.load(); } + /** + * @brief Attempts to reconnect to the DoIP server + * + * Closes existing connection and re-establishes TCP connection using + * previously stored server address and port. + * TODO: Currently does not retry on failure - not implemented. + * + * @return true if reconnection and routing activation succeeded, false otherwise + */ [[nodiscard]] bool reconnectServer(); + + /** + * @brief Closes TCP connection and stops TCP receiver thread + * + * Waits for TCP thread to terminate gracefully. + */ void closeTcpConnection(); - void startUdpConnection(); + /** + * @brief Initializes UDP socket for vehicle discovery + * + * Creates and configures UDP socket for sending vehicle identification + * requests. + * @return true if UDP socket setup succeeded, false otherwise + */ + [[nodiscard]] bool startUdpConnection(); + + /** + * @brief Checks if UDP connection is active + * + * @return true if UDP thread is running, false otherwise + */ [[nodiscard]] bool isUdpRunning() const noexcept { return m_udpRunning.load(); } + + /** + * @brief Starts UDP listener thread for vehicle announcements + * + * Listens on port 13401 for vehicle announcement messages from DoIP entities. + */ void startAnnouncementListener(); + + /** + * @brief Closes UDP socket and stops UDP listener thread + */ void closeUdpConnection(); - // TODO: Make private later + /** + * @brief Receives and processes a single UDP message + * + * @todo Make private later + */ void receiveUdpMessage(); + + /** + * @brief Sends vehicle identification request via UDP + * + * Broadcasts or unicasts a vehicle identification request to discover + * available DoIP entities. + * + * @param inet_address Target IP address (broadcast or unicast) + * @return Number of bytes sent, or -1 on error + * + * @todo Make private later + */ ssize_t sendVehicleIdentificationRequest(const char *inet_address); + + /** + * @brief Waits for and processes vehicle announcement message + * + * Blocks until a vehicle announcement is received or timeout occurs. + * Extracts VIN, EID, GID, and logical address from the announcement. + * + * @return true if valid announcement received, false on timeout or error + * + * @todo Make private later + */ [[nodiscard]] bool receiveVehicleAnnouncement(); /** * @brief Enqueues a DoIP message to the server + * + * Thread-safe method to queue messages for transmission. The TCP thread + * will dequeue and send them asynchronously. + * * @param msg DoIP message to send */ void sendMessage(const DoIPMessage &msg); /** * @brief Enqueues a diagnostic message to the server - * @param payload data that will be given to the ecu + * + * Wraps the payload in a DoIP diagnostic message format with appropriate + * source and target addresses, then enqueues for transmission. + * + * @param payload Diagnostic data that will be forwarded to the ECU */ void sendDiagnosticMessage(const ByteArray &payload); + /** + * @brief Sets the client's source address for diagnostic communication + * + * This address identifies the client in routing activation and diagnostic + * messages. + * + * @param address Client's DoIP logical address (typically in tester range) + */ void setSourceAddress(const DoIPAddress &address); - void printVehicleInformationResponse(); - // Request the client to quit gracefully + /** + * @brief Requests the client to quit gracefully + * + * Signals both TCP and UDP threads to terminate and updates receive state. + * Call closeTcpConnection() and closeUdpConnection() after this to ensure + * threads have stopped. + */ void requestQuit() { updateReceiveState(ReceiveState::Quit); m_tcpRunning.store(false); m_udpRunning.store(false); } + /** + * @brief Gets the properties of the connected DoIP server + * + * @return ServerProperties structure containing server details + */ + const ServerProperties &getServerProperties() const { + return m_serverProperties; + } + protected: + /** + * @brief Updates internal receive state with logging + * + * Transitions the client's receive state machine and logs the change. + * Used to coordinate message handling flow. + * + * @param newState Target receive state + */ void updateReceiveState(ReceiveState newState) { if (m_receiveState != newState) { m_log->info("Receive state changed from {} to {}", static_cast(m_receiveState), static_cast(newState)); @@ -77,47 +223,112 @@ class DoIPClient { } private: - UniqueDoIPClientModelPtr m_model; - ByteArray m_receiveBuf; - Socket m_tcpSocket, m_tcpClientSocket; - Socket m_udpSocket, m_udpVehicleAnnSocket; - ThreadSafeQueue m_messageQueue; - std::atomic m_tcpRunning{false}; - std::atomic m_udpRunning{false}; - std::thread m_tcpThread{}; - int m_broadcast = 1; - struct sockaddr_in m_serverAddress, m_clientAddress, m_announcementAddress; + UniqueDoIPClientModelPtr m_model; ///< Client behavior model + ByteArray m_receiveBuf; ///< Buffer for receiving messages + Socket m_tcpSocket; ///< TCP connection sockets + Socket m_udpSocket, m_udpVehicleAnnSocket; ///< UDP discovery sockets + ThreadSafeQueue m_messageQueue; ///< Outgoing message queue + std::atomic m_tcpRunning{false}; ///< TCP thread running flag + std::atomic m_udpRunning{false}; ///< UDP thread running flag + std::thread m_tcpThread{}; ///< TCP receiver thread + int m_broadcast = 1; ///< Broadcast socket option flag + struct sockaddr_in m_serverAddress, m_clientAddress, m_announcementAddress; ///< Socket addresses std::shared_ptr m_log = spdlog::stdout_color_mt("doip-client"); - DoIPAddress m_sourceAddress = DoIPAddress(0xE000); - Vin m_vin{0}; - DoIPAddress m_logicalAddress = ZERO_ADDRESS; - EntityId m_eid{0}; - GroupId m_gid{0}; - DoIPFurtherAction m_furtherActionReqResult = DoIPFurtherAction::NoFurtherAction; + DoIPAddress m_sourceAddress = DoIPAddress(0xE000); ///< Client's logical address + ServerProperties m_serverProperties{}; ///< Properties of the connected server + DoIPFurtherAction m_furtherActionReqResult = DoIPFurtherAction::NoFurtherAction; ///< Further action required flag - ReceiveState m_receiveState = ReceiveState::WaitForAckOrAliveCheck; + ReceiveState m_receiveState = ReceiveState::WaitForAckOrAliveCheck; ///< Current receive state + /** + * @brief Sends routing activation request to server + * + * @return Number of bytes sent, or -1 on error + */ ssize_t sendRoutingActivationRequest(); + + /** + * @brief Receives and validates routing activation response + * + * @return Routing activation response message if received, std::nullopt otherwise + */ std::optional receiveRoutingActivationResponse(); /** - * Sends a alive check response containing the clients source address to the server + * @brief Sends alive check response containing the client's source address to the server + * + * Responds to server's alive check request to maintain the connection. + * + * @return Number of bytes sent, or -1 on error */ ssize_t sendAliveCheckResponse(); + /** + * @brief Main TCP receiver thread function + * + * Continuously receives messages from server and dispatches them for processing. + * Also dequeues and sends messages from the outgoing queue. + */ void tcpThreadFunction(); + + /** + * @brief Performs routing activation handshake with server + * + * Sends routing activation request and waits for positive response. + * + * @return true if routing successfully activated, false otherwise + */ [[nodiscard]] bool activateRouting(); + + /** + * @brief Sends a DoIP message over TCP connection + * + * @param msg Message to send + * @return Number of bytes sent, or -1 on error + */ [[nodiscard]] ssize_t sendDoIPMessage(const DoIPMessage &msg); + + /** + * @brief Receives a DoIP message from TCP connection + * + * Reads and parses a complete DoIP message from the socket. + * + * @return Received message if successful, std::nullopt on error or connection closed + */ [[nodiscard]] std::optional receiveMessage(); - void reactToMessage(const DoIPMessage &msg); + /** + * @brief Processes received DoIP message based on type + * + * Dispatches message to appropriate handler based on payload type. + * Handles acknowledgements, alive checks, and diagnostic responses. + * + * @param msg Received message to process + */ + void handleMessage(const DoIPMessage &msg); + /** + * @brief Main UDP listener thread function + * + * Continuously listens for vehicle announcements on UDP port 13401. + */ void udpThreadFunction(); + + /** + * @brief Parses vehicle identification response message + * + * Extracts VIN, logical address, EID, GID, and further action requirements + * from the vehicle announcement message. + * + * @param msg Vehicle identification response message + */ void parseVehicleIdentificationResponse(const DoIPMessage &msg); - int emptyMessageCounter = 0; + int emptyMessageCounter = 0; ///< Counter for consecutive empty message receives + + std::mutex m_mutex; ///< Mutex for protecting connection operations }; } // namespace doip diff --git a/inc/DoIPMessage.h b/inc/DoIPMessage.h index deae7e0..8d2b4f2 100644 --- a/inc/DoIPMessage.h +++ b/inc/DoIPMessage.h @@ -742,22 +742,21 @@ inline std::ostream &operator<<(std::ostream &os, const DoIPMessage &msg) { os << ansi::yellow << "|Alive Check?"; } else if (msg.getPayloadType() == DoIPPayloadType::AliveCheckResponse) { auto sa = msg.getSourceAddress(); - os << ansi::green << "|Alive Check " << sa.value() << " ✓"; + os << ansi::green << "|Alive Check " << toHex4(sa.value_or(0)) << " ✓"; } else if (msg.getPayloadType() == DoIPPayloadType::RoutingActivationRequest) { auto sa = msg.getSourceAddress(); - os << ansi::yellow << "|Routing activation? " << std::hex << sa.value() << std::dec; + os << ansi::yellow << "|Routing activation? " << toHex4(sa.value_or(0)); } else if (msg.getPayloadType() == DoIPPayloadType::RoutingActivationResponse) { auto sa = msg.getSourceAddress(); - - os << ansi::green << "|Routing activation " << std::hex << sa.value() << std::dec << " ✓"; + os << ansi::green << "|Routing activation " << toHex4(sa.value_or(0)) << " ✓"; } else if (msg.getPayloadType() == DoIPPayloadType::DiagnosticMessage) { auto payload = msg.getDiagnosticMessagePayload(); auto sa = msg.getSourceAddress(); auto ta = msg.getTargetAddress(); os << "|Diag "; - os << ansi::bold_magenta << std::hex << sa.value() << std::dec; + os << ansi::bold_magenta << toHex4(sa.value_or(0)); os << ansi::reset << " -> "; - os << ansi::bold_magenta << std::hex << ta.value() << std::dec; + os << ansi::bold_magenta << toHex4(ta.value_or(0)); os << ansi::reset << ": "; os << ansi::bold_blue; diff --git a/inc/DoIPServer.h b/inc/DoIPServer.h index 4e2036e..0f7e111 100644 --- a/inc/DoIPServer.h +++ b/inc/DoIPServer.h @@ -137,7 +137,7 @@ class DoIPServer { * @return DoIPAddress Logical gateway address */ DoIPAddress getLogicalGatewayAddress() const noexcept { - return m_config.logicalAddress; + return m_config.properties.logicalAddress; } /** * @brief Set the logical DoIP gateway address. @@ -167,29 +167,30 @@ class DoIPServer { * @brief Get current VIN. * @return Reference to configured VIN. */ - const Vin &getVin() const { return m_config.vin; } + const Vin &getVin() const { return m_config.properties.vin; } /** * @brief Set EID value. - * @param nputEID EID as 64-bit value (lower 48 bits used). + * @param eid EID as 64-bit value (lower 48 bits used). */ - void setEid(uint64_t nputEID); + void setEid(uint64_t eid); + /** * @brief Get current EID. * @return Reference to configured EID. */ - const EntityId &getEid() const { return m_config.eid; } + const EntityId &getEid() const { return m_config.properties.eid; } /** * @brief Set GID value. - * @param inputGID GID as 64-bit value (lower 48 bits used). + * @param gid GID as 64-bit value (lower 48 bits used). */ - void setGid(uint64_t inputGID); + void setGid(uint64_t gid); /** * @brief Get current GID. * @return Reference to configured GID. */ - const GroupId &getGid() const { return m_config.gid; } + const GroupId &getGid() const { return m_config.properties.gid; } /** * @brief Get current further action requirement status. @@ -261,6 +262,14 @@ class DoIPServer { ssize_t sendBroadcast(const DoIPMessage &msg, uint16_t port); + /** + * @brief Create a vehicle announcement message from the given server configuration. + * + * @param config Server configuration containing vehicle properties. + * @return DoIPMessage Vehicle announcement message. + */ + DoIPMessage makeVehicleAnnouncementMessage(const ServerConfig& config); + /** * @brief Configure broadcast/multicast settings */ diff --git a/inc/cli/ServerConfig.h b/inc/cli/ServerConfig.h index c965c68..19e7855 100644 --- a/inc/cli/ServerConfig.h +++ b/inc/cli/ServerConfig.h @@ -1,33 +1,61 @@ #ifndef SERVERCONFIG_H #define SERVERCONFIG_H -#include "Vin.h" -#include "DoIPIdentifiers.h" #include "DoIPAddress.h" +#include "DoIPIdentifiers.h" +#include "DoIPTimes.h" +#include "Vin.h" namespace doip { + +struct ServerProperties { + /** + * @brief Vehicle Identification Number (VIN) consisting of 17 characters. + * See ISO 3779 for details. + */ + Vin vin = Vin::Zero; + /** + * @brief Entity Identifier (EID) - 6-byte unique identifier for the vehicle. + */ + EntityId eid = EntityId::Zero; + /** + * @brief Group Identifier (GID) - 6-byte identifier for vehicle group. + */ + GroupId gid = GroupId::Zero; + /** + * @brief DoIP logical address of the server (gateway). + */ + DoIPAddress logicalAddress = ZERO_ADDRESS; +}; + /** * @brief Server configuration structure used to initialize a DoIP server. */ struct ServerConfig { - // EID and GID as fixed identifiers (6 bytes). Default: zeros. - EntityId eid = EntityId::Zero; - GroupId gid = GroupId::Zero; - - // VIN as fixed identifier (17 bytes). Default: zeros. - Vin vin = Vin::Zero; - - // Logical/server address (default 0x0028) - DoIPAddress logicalAddress = DoIPAddress(0x0028); - - // Use loopback announcements instead of broadcast + /** + * @brief Server properties (EID, GID, VIN, logical address). + */ + ServerProperties properties; + + /** + * @brief If true, use loopback interface for vehicle announcements. + */ bool loopback = false; - // Run the server as a daemon by default + /** + * @brief If true, run the server as a daemon. + */ bool daemonize = false; + /** + * @brief Number of vehicle announcements to send upon startup. + */ int announceCount = 3; // Default Value = 3 - unsigned int announceInterval = 500; // Default Value = 500ms + + /** + * @brief Interval between vehicle announcements in milliseconds. + */ + unsigned int announceInterval = times::server::VehicleAnnouncementInterval.count(); }; } // namespace doip diff --git a/src/DoIPClient.cpp b/src/DoIPClient.cpp index 593aee8..bc7eed8 100644 --- a/src/DoIPClient.cpp +++ b/src/DoIPClient.cpp @@ -1,7 +1,7 @@ #include "DoIPClient.h" #include "DoIPMessage.h" -#include "DoIPTimes.h" #include "DoIPPayloadType.h" +#include "DoIPTimes.h" #include "util/Logger.h" #include // for errno #include // for strerror @@ -13,7 +13,7 @@ using namespace doip; // UDP Connection and Thread Handling // ------------------------------------------------------------------- -void DoIPClient::startUdpConnection() { +bool DoIPClient::startUdpConnection() { int tmpUdpSocket = socket(AF_INET, SOCK_DGRAM, 0); @@ -34,8 +34,11 @@ void DoIPClient::startUdpConnection() { if (!rc) { m_log->error("Bind failed: {}", strerror(errno)); close(m_udpSocket.get()); + return false; } + return true; } + return false; } void DoIPClient::startAnnouncementListener() { @@ -62,7 +65,8 @@ void DoIPClient::startAnnouncementListener() { m_announcementAddress.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to port 13401 for Vehicle Announcements - if (bind(m_udpVehicleAnnSocket.get(), reinterpret_cast(&m_announcementAddress), sizeof(m_announcementAddress)) < 0) { + int rc = bind(m_udpVehicleAnnSocket.get(), reinterpret_cast(&m_announcementAddress), sizeof(m_announcementAddress)); + if (rc < 0) { m_log->error("Failed to bind announcement socket to port {}: {}", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT, strerror(errno)); } else { m_log->info("Announcement socket bound to port {} successfully", DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); @@ -92,7 +96,7 @@ void DoIPClient::receiveUdpMessage() { // Set socket to timeout after 3 seconds struct timeval timeout; - //timeout.tv_sec = static_cast(doip::times::client::UdpMessageTimeout.count() / 1000); + // timeout.tv_sec = static_cast(doip::times::client::UdpMessageTimeout.count() / 1000); timeout.tv_sec = doip::times::client::UdpMessageTimeout.count() / 1000; timeout.tv_usec = 0; setsockopt(m_udpSocket.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); @@ -201,6 +205,8 @@ bool DoIPClient::startTcpConnection(const char *inet_address, uint16_t port) { m_log->info("Client TCP-Socket created successfully"); bool connectedFlag = false; + m_tcpRunning.store(false); + const char *ipAddr = inet_address; m_serverAddress.sin_family = AF_INET; m_serverAddress.sin_port = htons(port); @@ -208,24 +214,27 @@ bool DoIPClient::startTcpConnection(const char *inet_address, uint16_t port) { int retries = 3; while (!connectedFlag && retries > 0) { - int tmpConnSocket = connect(m_tcpSocket.get(), reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); - if (tmpConnSocket != -1) { - m_tcpClientSocket.reset(tmpConnSocket); + int connResult = connect(m_tcpSocket.get(), reinterpret_cast(&m_serverAddress), sizeof(m_serverAddress)); + if (connResult == 0) { connectedFlag = true; m_log->info("Connection to server established"); if (!activateRouting()) { m_log->error("Routing activation failed - connection closed"); - m_tcpClientSocket.close(); - m_model->routingActivationFinished(*this, false, m_logicalAddress); + m_tcpSocket.close(); + m_tcpRunning.store(false); + m_model->routingActivationFinished(*this, false, m_serverProperties.logicalAddress); return false; } - m_model->routingActivationFinished(*this, true, m_logicalAddress); - + m_model->routingActivationFinished(*this, true, m_serverProperties.logicalAddress); m_tcpRunning.store(true); m_tcpThread = std::thread(&DoIPClient::tcpThreadFunction, this); return true; + } else { + m_log->error("Connection to server failed: {}", strerror(errno)); + m_tcpSocket.close(); + m_tcpRunning.store(false); } std::this_thread::sleep_for(std::chrono::seconds(1)); retries--; @@ -270,7 +279,7 @@ void DoIPClient::tcpThreadFunction() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } else { - reactToMessage(optAck.value()); + handleMessage(optAck.value()); } if (m_receiveState == ReceiveState::Quit) { @@ -295,12 +304,12 @@ void DoIPClient::tcpThreadFunction() { continue; } else { receiveRetries = 5; - reactToMessage(optMsg.value()); + handleMessage(optMsg.value()); } } } -void DoIPClient::reactToMessage(const DoIPMessage &msg) { +void DoIPClient::handleMessage(const DoIPMessage &msg) { if (m_receiveState == ReceiveState::WaitForAckOrAliveCheck) { switch (msg.getPayloadType()) { case DoIPPayloadType::AliveCheckRequest: @@ -343,7 +352,6 @@ void DoIPClient::reactToMessage(const DoIPMessage &msg) { } updateReceiveState(newState); } - } bool DoIPClient::activateRouting() { @@ -371,7 +379,8 @@ bool DoIPClient::activateRouting() { return false; } - m_logicalAddress = optLogicalAddress.value(); + std::lock_guard lock(m_mutex); + m_serverProperties.logicalAddress = optLogicalAddress.value(); return true; } @@ -385,11 +394,9 @@ void DoIPClient::closeTcpConnection() { m_tcpThread.join(); } - m_tcpClientSocket.close(); m_tcpSocket.close(); } - bool DoIPClient::reconnectServer() { closeTcpConnection(); return startTcpConnection(); @@ -409,7 +416,8 @@ ssize_t DoIPClient::sendRoutingActivationRequest() { } void DoIPClient::sendDiagnosticMessage(const ByteArray &payload) { - sendMessage(message::makeDiagnosticMessage(m_sourceAddress, m_logicalAddress, payload)); + std::lock_guard lock(m_mutex); + sendMessage(message::makeDiagnosticMessage(m_sourceAddress, m_serverProperties.logicalAddress, payload)); } ssize_t DoIPClient::sendAliveCheckResponse() { @@ -453,12 +461,12 @@ std::optional DoIPClient::receiveMessage() { return msg; } - /** * Sets the source address for this client * @param address source address for the client */ void DoIPClient::setSourceAddress(const DoIPAddress &address) { + std::lock_guard lock(m_mutex); m_sourceAddress = address; } @@ -473,18 +481,11 @@ void DoIPClient::parseVehicleIdentificationResponse(const DoIPMessage &msg) { m_log->warn("Incomplete Vehicle Identification Response received: Missing VIN, EID, GID, Logical Address or Further Action Request"); } - m_vin = optVin.value(); - m_eid = optEid.value(); - m_gid = optGid.value(); - m_logicalAddress = optLogicalAddress.value(); + std::lock_guard lock(m_mutex); + m_serverProperties.vin = optVin.value(); + m_serverProperties.eid = optEid.value(); + m_serverProperties.gid = optGid.value(); + m_serverProperties.logicalAddress = optLogicalAddress.value(); m_furtherActionReqResult = optFurtherAction.value(); } -void DoIPClient::printVehicleInformationResponse() { - m_log->info("Vehicle Identification Response:"); - m_log->info("VIN: {}", fmt::streamed(m_vin)); - m_log->info("EID: {}", fmt::streamed(m_eid)); - m_log->info("GID: {}", fmt::streamed(m_gid)); - m_log->info("Logical Address: 0x{:04X}", m_logicalAddress); - m_log->info("Further Action Request Result: {}", static_cast(m_furtherActionReqResult)); -} diff --git a/src/DoIPServer.cpp b/src/DoIPServer.cpp index af4d5e0..0b9d038 100644 --- a/src/DoIPServer.cpp +++ b/src/DoIPServer.cpp @@ -168,44 +168,42 @@ bool DoIPServer::setupUdpSocket() { void DoIPServer::closeUdpSocket() { m_udpRunning.store(false); - // Transport handles UDP socket cleanup } - bool DoIPServer::setDefaultEid() { MacAddress mac = {0}; if (!getFirstMacAddress(mac)) { m_doipLog->error("Failed to get MAC address, using default EID"); - m_config.eid = EntityId::Zero; + m_config.properties.eid = EntityId::Zero; return false; } // Set EID based on MAC address (last 6 bytes) - m_config.eid = EntityId(mac.data(), m_config.eid.ID_LENGTH); + m_config.properties.eid = EntityId(mac.data(), m_config.properties.eid.ID_LENGTH); return true; } void DoIPServer::setVin(const std::string &VINString) { - m_config.vin = Vin(VINString); + m_config.properties.vin = Vin(VINString); } void DoIPServer::setVin(const Vin &vin) { if (!isValidVin(vin)) { m_doipLog->warn("Invalid VIN provided {}", fmt::streamed(vin)); } - m_config.vin = vin; + m_config.properties.vin = vin; } void DoIPServer::setLogicalGatewayAddress(DoIPAddress logicalAddress) { - m_config.logicalAddress = logicalAddress; + m_config.properties.logicalAddress = logicalAddress; } -void DoIPServer::setEid(const uint64_t inputEID) { - m_config.eid = EntityId(inputEID); +void DoIPServer::setEid(const uint64_t eid) { + m_config.properties.eid = EntityId(eid); } -void DoIPServer::setGid(const uint64_t inputGID) { - m_config.gid = GroupId(inputGID); +void DoIPServer::setGid(const uint64_t gid) { + m_config.properties.gid = GroupId(gid); } void DoIPServer::setFurtherActionRequired(DoIPFurtherAction furtherActionRequired) { @@ -239,9 +237,7 @@ void DoIPServer::udpAnnouncementThread() { // Send announcements with configured interval and count for (int i = 0; i < m_config.announceCount && m_udpRunning; i++) { - DoIPMessage msg = message::makeVehicleIdentificationResponse( - m_config.vin, m_config.logicalAddress, m_config.eid, m_config.gid); - + DoIPMessage msg = makeVehicleAnnouncementMessage(m_config); ssize_t sentBytes = sendBroadcast(msg, DOIP_UDP_TEST_EQUIPMENT_REQUEST_PORT); m_doipLog->info("TX {}", fmt::streamed(msg)); @@ -285,12 +281,7 @@ void DoIPServer::udpAnnouncementThread() { inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); if (msg.getPayloadType() == DoIPPayloadType::VehicleIdentificationRequest) { - const auto& rsp = doip::message::makeVehicleIdentificationResponse( - m_config.vin, - m_config.logicalAddress, - m_config.eid, - m_config.gid - ); + const auto& rsp = makeVehicleAnnouncementMessage(m_config); m_udpLog->info("TX {}", fmt::streamed(rsp)); ssize_t rc = sendto(m_udpSocket.get(), rsp.data(), rsp.size(), 0, reinterpret_cast(&clientAddr), clientAddrLen); @@ -323,7 +314,7 @@ std::unique_ptr DoIPServer::waitForTcpConnection(std::function(); // Ensure server model uses configured logical address if (model) { - model->serverAddress = m_config.logicalAddress; + model->serverAddress = m_config.properties.logicalAddress; } m_tcpLog->info("Accepted new TCP connection, model {} (factory {})", model->getModelName(), @@ -410,3 +401,11 @@ ssize_t DoIPServer::sendBroadcast(const DoIPMessage &msg, uint16_t port) { m_udpLog->debug("Sent {} bytes via UDP broadcast", sent); return sent; } + +DoIPMessage DoIPServer::makeVehicleAnnouncementMessage(const ServerConfig& config) { + return message::makeVehicleIdentificationResponse( + config.properties.vin, + config.properties.logicalAddress, + config.properties.eid, + config.properties.gid); +} diff --git a/src/cli/ServerConfigCLI.cpp b/src/cli/ServerConfigCLI.cpp index ba8eb2d..6151a87 100644 --- a/src/cli/ServerConfigCLI.cpp +++ b/src/cli/ServerConfigCLI.cpp @@ -67,7 +67,7 @@ ServerConfig ServerConfigCLI::parse_and_build(int argc, char **argv) { cfg.announceInterval = m_announceInterval; if (!m_vinStr.empty()) { - cfg.vin = Vin(m_vinStr); + cfg.properties.vin = Vin(m_vinStr); } if (!m_eidHex.empty()) { @@ -75,7 +75,7 @@ ServerConfig ServerConfigCLI::parse_and_build(int argc, char **argv) { if (!parseHexBytes12(m_eidHex, b)) { throw std::runtime_error("Invalid EID: must be 12 hex chars"); } - cfg.eid = EntityId(b.data(), 6); + cfg.properties.eid = EntityId(b.data(), 6); } if (!m_gidHex.empty()) { @@ -83,7 +83,7 @@ ServerConfig ServerConfigCLI::parse_and_build(int argc, char **argv) { if (!parseHexBytes12(m_gidHex, b)) { throw std::runtime_error("Invalid GID: must be 12 hex chars"); } - cfg.gid = GroupId(b.data(), 6); + cfg.properties.gid = GroupId(b.data(), 6); } unsigned long la = 0; @@ -111,7 +111,7 @@ ServerConfig ServerConfigCLI::parse_and_build(int argc, char **argv) { throw std::runtime_error("Invalid logical-address: partial parse"); } } - cfg.logicalAddress = DoIPAddress(la & 0xFFFF); + cfg.properties.logicalAddress = DoIPAddress(la & 0xFFFF); return cfg; } diff --git a/test/integration/discover/CMakeLists.txt b/test/integration/discover/CMakeLists.txt index 14401d7..e2fa241 100644 --- a/test/integration/discover/CMakeLists.txt +++ b/test/integration/discover/CMakeLists.txt @@ -1,8 +1,8 @@ # Examples CMakeLists.txt set (DISCOVER_SOURCES - Discover_Server.cpp - Discover_Client.cpp + DiscoverServer.cpp + DiscoverClient.cpp ) foreach(demo_source ${DISCOVER_SOURCES}) @@ -43,7 +43,7 @@ if (WITH_INTEGRATION_TEST) # Integration fixture: start DoIPServer as a daemon add_test(NAME Integration_StartDiscoverServer - COMMAND bash -c "$ --daemon && sleep 1.0" + COMMAND bash -c "$ --daemon && sleep 1.0" ) set_tests_properties(Integration_StartDiscoverServer PROPERTIES FIXTURES_SETUP demo_server @@ -108,7 +108,7 @@ if (WITH_INTEGRATION_TEST) # Integration test: run discover (udp client) against the running server # CRITICAL: Must run serially and immediately after server is ready to catch announcements add_test(NAME Integration_DiscoverClient_Runs - COMMAND $ + COMMAND $ ) set_tests_properties(Integration_DiscoverClient_Runs PROPERTIES FIXTURES_REQUIRED demo_ready diff --git a/test/integration/discover/Discover_Client.cpp b/test/integration/discover/DiscoverClient.cpp similarity index 64% rename from test/integration/discover/Discover_Client.cpp rename to test/integration/discover/DiscoverClient.cpp index 0a6a585..40c15e2 100644 --- a/test/integration/discover/Discover_Client.cpp +++ b/test/integration/discover/DiscoverClient.cpp @@ -8,14 +8,16 @@ using std::string; DoIPClient client; int main() { - // string serverAddress = "224.0.0.2"; // Default multicast address - string serverAddress = "127.0.0.1"; // Default to loopback for testing + string serverAddress = "127.0.0.1"; // Default to loopback for testing to same hosts auto console = spdlog::stdout_color_mt("discover-client"); console->info("Starting DoIP Client"); // Start UDP connections (don't start TCP yet) - client.startUdpConnection(); + if (!client.startUdpConnection()) { + console->error("Failed to start UDP connection"); + return EXIT_FAILURE; + } client.startAnnouncementListener(); // Listen for Vehicle Announcements on port 13401 // Listen for Vehicle Announcements first @@ -25,13 +27,12 @@ int main() { return EXIT_FAILURE; } - client.printVehicleInformationResponse(); - - // Send Vehicle Identification Request to configured address - if (client.sendVehicleIdentificationRequest(serverAddress.c_str()) > 0) { - console->info("Vehicle Identification Request sent successfully"); - client.receiveUdpMessage(); - } + ServerProperties vehicleInfo = client.getServerProperties(); + console->info("Discovered Vehicle - VIN: {}, EID: {}, GID: {}, Logical Address: 0x{:04X}", + fmt::streamed(vehicleInfo.vin), + fmt::streamed(vehicleInfo.eid), + fmt::streamed(vehicleInfo.gid), + vehicleInfo.logicalAddress); // Now start TCP connection for diagnostic communication console->info("Discovery complete, closing UDP connections"); diff --git a/test/integration/discover/Discover_Server.cpp b/test/integration/discover/DiscoverServer.cpp similarity index 80% rename from test/integration/discover/Discover_Server.cpp rename to test/integration/discover/DiscoverServer.cpp index b12a480..37cf69c 100644 --- a/test/integration/discover/Discover_Server.cpp +++ b/test/integration/discover/DiscoverServer.cpp @@ -28,6 +28,11 @@ int main(int argc, char *argv[]) { ServerConfig cfg; cfg.loopback = true; // For testing, use loopback announcements cfg.daemonize = argc > 1 && std::string(argv[1]) == "--daemon"; // For testing, run as daemon + cfg.properties.logicalAddress = DoIPAddress(0x029); // Logical address for discovery server + cfg.properties.eid = EntityId(0x123456789ABC); // Fixed EID for testing + cfg.properties.vin = Vin("TESTV0N1234567890"); // Fixed VIN for testing (I is not allowed!) + cfg.properties.gid = GroupId(0x000000000001); // Fixed GID for testing + auto console = spdlog::stdout_color_mt("doip-server"); @@ -63,6 +68,11 @@ int main(int argc, char *argv[]) { server->setAnnounceInterval(500); // Send announcements every 500ms for faster discovery server->setAnnounceNum(100); // Send 100 announcements = 50 seconds of announcements (enough for parallel test execution) + server->setDefaultEid(); + console->info("VIN: {}", server->getVin().toString()); // just to log VIN validity + console->info("EID: {}", server->getEid().toString()); // just to log EID validity + console->info("GID: {}", server->getGid().toString()); + // Set up TCP first to ensure transport creates/binds both TCP and UDP sockets if (!server->setupTcpSocket()) { console->critical("Failed to set up TCP socket"); diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index 5ef86a0..4eaf07a 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -1,7 +1,7 @@ # Examples CMakeLists.txt set (DISCOVER_SOURCES - DoIP_Uds_Server.cpp + UdsServer.cpp ) foreach(demo_source ${DISCOVER_SOURCES}) diff --git a/test/integration/uds-download/DoIP_Uds_Server.cpp b/test/integration/uds-download/UdsServer.cpp similarity index 95% rename from test/integration/uds-download/DoIP_Uds_Server.cpp rename to test/integration/uds-download/UdsServer.cpp index 966e0a9..7199444 100644 --- a/test/integration/uds-download/DoIP_Uds_Server.cpp +++ b/test/integration/uds-download/UdsServer.cpp @@ -31,7 +31,7 @@ static void handle_signal(int) { int main(int argc, char *argv[]) { ServerConfig cfg; cfg.loopback = true; // For testing, use loopback announcements - cfg.logicalAddress = DoIPAddress(0x00E0); // Match client logical address used in tests + cfg.properties.logicalAddress = DoIPAddress(0x00E0); // Match client logical address used in tests cfg.daemonize = argc > 1 && std::string(argv[1]) == "--daemon"; // For testing, run as daemon auto console = spdlog::stdout_color_mt("doip-server"); diff --git a/test/unit/DoIPServer_Test.cpp b/test/unit/DoIPServer_Test.cpp index 8e19975..0e98467 100644 --- a/test/unit/DoIPServer_Test.cpp +++ b/test/unit/DoIPServer_Test.cpp @@ -20,7 +20,7 @@ TEST_SUITE("DoIPServer Tests") { CHECK(server.getVin() == Vin::Zero); CHECK(server.getEid() == EntityId::Zero); CHECK(server.getGid() == GroupId::Zero); - CHECK(server.getLogicalGatewayAddress() == DoIPAddress(0x0028)); + CHECK(server.getLogicalGatewayAddress() == DoIPAddress(0x0)); CHECK(server.getClientIp() == ""); CHECK(server.getClientPort() == 0); } diff --git a/test/unit/cli/ServerConfigCLI_Test.cpp b/test/unit/cli/ServerConfigCLI_Test.cpp index a5198dd..70b6e66 100644 --- a/test/unit/cli/ServerConfigCLI_Test.cpp +++ b/test/unit/cli/ServerConfigCLI_Test.cpp @@ -24,7 +24,7 @@ TEST_CASE("parses valid arguments into ServerConfig") { CHECK(cfg.loopback == true); CHECK(cfg.announceCount == 5); CHECK(cfg.announceInterval == 250); - CHECK(cfg.logicalAddress == 0x28); + CHECK(cfg.properties.logicalAddress == 0x28); } TEST_CASE("invalid EID raises error") { From bb72c480c1589290575bc87210ee26016313a964 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 17:58:16 +0100 Subject: [PATCH 29/44] doc: Fix project name --- Doxyfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doxyfile b/Doxyfile index b15faf4..d73edf8 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile for libdoip +# Doxyfile for doip-server #--------------------------------------------------------------------------- # Project related configuration options From de9ced2a1b96e690b7eaf90400a46612c3d46288 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:00:15 +0100 Subject: [PATCH 30/44] fix: Remove obsolete example --- examples/client/ReadMe.md | 56 ------------------------- examples/client/change-vin-and-reset.py | 52 ----------------------- 2 files changed, 108 deletions(-) delete mode 100644 examples/client/ReadMe.md delete mode 100644 examples/client/change-vin-and-reset.py diff --git a/examples/client/ReadMe.md b/examples/client/ReadMe.md deleted file mode 100644 index e8e97a4..0000000 --- a/examples/client/ReadMe.md +++ /dev/null @@ -1,56 +0,0 @@ -# Preconditions - -To run the Python scripts, you need to install `doipclient` and `udsoncan` first: - -```bash -pip install doipclient udsoncan -``` -# Notes - -Key points for client implementers: - -- The DoIP server sends an immediate Diagnostic Ack (0x8002) after receiving - a Diagnostic Message (0x8001). This confirms transport reception only. -- The functional UDS response may arrive later as another Diagnostic Message - (0x8001). The client must wait for that message to obtain the service - result. -- When using `udsoncan`, the library abstracts away DoIP transport details. - If you implement a custom connector, ensure you forward both the ack and - subsequent response frames appropriately. - -Minimal pseudo-code (conceptual): - -```python -# Using doipclient + udsoncan -from doipclient import DoIPClient -from doipclient.connectors import DoIPClientUDSConnector -from udsoncan.client import Client - -client = DoIPClient('127.0.0.1', 0x00E0) -conn = DoIPClientUDSConnector(client) - -with Client(conn, request_timeout=2) as u: - # change session -> server will ack on transport, then send response - resp = u.change_session(0x01) - # udsoncan waits for the real UDS response; transport ACK is handled internally - print('Session change response:', resp) -``` - -If you handle sockets yourself, inspect packet payload types: treat 0x8002 -as transport-level ack and do not consider it the functional response. - -```python -# Pseudocode for low-level handling -sock = setup_doip_socket() -while True: - pkt = sock.recv() - payload_type = parse_payload_type(pkt) - if payload_type == 0x8002: - # transport ack - ignore for UDS result - continue - if payload_type == 0x8001: - # functional UDS response - process - handle_uds_response(pkt) -``` - -See [python-doipclient docs](). diff --git a/examples/client/change-vin-and-reset.py b/examples/client/change-vin-and-reset.py deleted file mode 100644 index 5f7033f..0000000 --- a/examples/client/change-vin-and-reset.py +++ /dev/null @@ -1,52 +0,0 @@ -import udsoncan -from doipclient import DoIPClient -from doipclient.connectors import DoIPClientUDSConnector -from udsoncan.client import Client -from udsoncan.exceptions import * -from udsoncan.services import * -from udsoncan import DataIdentifier, AsciiCodec - -udsoncan.setup_logging() - -# Add this config -config = { - 'data_identifiers': { - DataIdentifier.VIN: AsciiCodec(17) - } -} - -ecu_ip = '127.0.0.1' -ecu_logical_address = 0x00E0 -doip_client = DoIPClient(ecu_ip, ecu_logical_address) -conn = DoIPClientUDSConnector(doip_client) -with Client(conn, request_timeout=2, config=config) as client: - try: - client.change_session(1) # integer with value of 3 - #client.unlock_security_access(MyCar.debug_level) # Fictive security level. Integer coming from fictive lib, let's say its value is 5 - # Read VIN once and handle response type (udsoncan may return bytes or str) - vin_response = client.read_data_by_identifier(udsoncan.DataIdentifier.VIN) - vin_value = vin_response.service_data.values[udsoncan.DataIdentifier.VIN] - if isinstance(vin_value, bytes): - vin_str = vin_value.decode('ascii', errors='ignore') - else: - # already a str - vin_str = str(vin_value) - print('Current Vehicle Identification Number is: %s' % vin_str) - - client.write_data_by_identifier(udsoncan.DataIdentifier.VIN, 'ABC123456789GHXJK') # Standard ID for VIN is 0xF190. Codec is set in the client configuration - print('Vehicle Identification Number successfully changed.') - client.ecu_reset(ECUReset.ResetType.hardReset) # HardReset = 0x01 - except NegativeResponseException as e: - print('Server refused our request for service %s with code "%s" (0x%02x)' % (e.response.service.get_name(), e.response.code_name, e.response.code)) - except (InvalidResponseException, UnexpectedResponseException) as e: - print('Server sent an invalid payload : %s' % e.response.original_payload) - - # Because we reset our UDS server, we must also reconnect/reactivate the DoIP socket - # Alternatively, we could have used the auto_reconnect_tcp flag on the DoIPClient - # Note: ECU's do not restart instantly, so you may need a sleep() before moving on - doip_client.reconnect() - client.tester_present() - -# Cleanup the DoIP Socket when we're done. Alternatively, we could have used the -# close_connection flag on conn so that the udsoncan client would clean it up -doip_client.close() \ No newline at end of file From 001e919956d7699302679bcdc48860121d35aa66 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:13:16 +0100 Subject: [PATCH 31/44] chore: Fix dup target (ninja failed) --- cmake/python-venv.cmake | 1 - 1 file changed, 1 deletion(-) diff --git a/cmake/python-venv.cmake b/cmake/python-venv.cmake index 66bf3d6..d8d1b3d 100644 --- a/cmake/python-venv.cmake +++ b/cmake/python-venv.cmake @@ -15,7 +15,6 @@ add_custom_command( COMMAND ${VENV_DIR}/bin/pip3 install udsoncan doipclient DEPENDS ${VENV_DIR} COMMENT "Installing Python packages in venv" - BYPRODUCTS ${VENV_DIR}/installed ) # Custom target to set up venv and packages From b937aa1c24da9c6f2b4356fdcdeba6675839c3c2 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:17:42 +0100 Subject: [PATCH 32/44] chore: Fix exe name --- test/integration/uds-download/run-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/uds-download/run-test.sh b/test/integration/uds-download/run-test.sh index bd10826..5d9ced3 100644 --- a/test/integration/uds-download/run-test.sh +++ b/test/integration/uds-download/run-test.sh @@ -3,7 +3,7 @@ source venv/bin/activate # Start server in background, log to server.log -./DoIP_Uds_Server > server.log 2>&1 & +./UdsServer > server.log 2>&1 & SERVER_PID=$! # Give the server a moment to start From 8f9a2bb8d885bf60d4046626064f7cea438dbd74 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:22:43 +0100 Subject: [PATCH 33/44] chore: Fix Discover_Server -> DiscoverServer --- test/integration/discover/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/integration/discover/CMakeLists.txt b/test/integration/discover/CMakeLists.txt index e2fa241..4446a45 100644 --- a/test/integration/discover/CMakeLists.txt +++ b/test/integration/discover/CMakeLists.txt @@ -61,8 +61,8 @@ if (WITH_INTEGRATION_TEST) sleep 5 # First check if server process exists - if ! pgrep -f Discover_Server >/dev/null 2>&1; then - echo 'ERROR: Discover_Server process not running!' >&2 + if ! pgrep -f DiscoverServer >/dev/null 2>&1; then + echo 'ERROR: DiscoverServer process not running!' >&2 exit 1 fi @@ -86,14 +86,14 @@ if (WITH_INTEGRATION_TEST) if [ \$i -eq 50 ]; then echo \"Still waiting... (UDP check: \$udp_check, TCP check: \$tcp_check)\" - pgrep -f Discover_Server >/dev/null 2>&1 || echo 'WARNING: Server process died!' + pgrep -f DiscoverServer >/dev/null 2>&1 || echo 'WARNING: Server process died!' fi sleep 0.1 done echo 'ERROR: Server not ready after 15 seconds (UDP: '\$udp_check', TCP: '\$tcp_check')' >&2 - pgrep -f Discover_Server >/dev/null 2>&1 || echo 'Server process is NOT running' >&2 + pgrep -f DiscoverServer >/dev/null 2>&1 || echo 'Server process is NOT running' >&2 exit 1 " ) @@ -120,7 +120,7 @@ if (WITH_INTEGRATION_TEST) # Teardown fixture: stop server and wait for ports to be released add_test(NAME Integration_StopDiscoverServer - COMMAND bash -c "if [ -f /tmp/doip-discover.pid ]; then kill $(cat /tmp/doip-discover.pid) 2>/dev/null; rm -f /tmp/doip-discover.pid; else pkill -f Discover_Server 2>/dev/null || true; fi; for i in $(seq 1 50); do if command -v ss >/dev/null 2>&1; then ss -tlpn 2>/dev/null | grep -q ':13400' || exit 0; else netstat -an 2>/dev/null | grep -q '\\.13400.*LISTEN' || exit 0; fi; sleep 0.1; done; echo 'Warning: Port 13400 still in use' >&2; exit 0" + COMMAND bash -c "if [ -f /tmp/doip-discover.pid ]; then kill $(cat /tmp/doip-discover.pid) 2>/dev/null; rm -f /tmp/doip-discover.pid; else pkill -f DiscoverServer 2>/dev/null || true; fi; for i in $(seq 1 50); do if command -v ss >/dev/null 2>&1; then ss -tlpn 2>/dev/null | grep -q ':13400' || exit 0; else netstat -an 2>/dev/null | grep -q '\\.13400.*LISTEN' || exit 0; fi; sleep 0.1; done; echo 'Warning: Port 13400 still in use' >&2; exit 0" ) set_tests_properties(Integration_StopDiscoverServer PROPERTIES FIXTURES_CLEANUP demo_server From 16ba9faf17cd7c651208a5417fece295c66e8fca Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:30:48 +0100 Subject: [PATCH 34/44] chore: Remove python code venv --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11ddac9..0c85693 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,9 +44,9 @@ endif() include(cmake/projectSettings.cmake) # Include python virtual environment setup for integration tests -if (WITH_INTEGRATION_TEST) - include(cmake/python-venv.cmake) -endif() +# if (WITH_INTEGRATION_TEST) +# include(cmake/python-venv.cmake) +# endif() # Find required packages find_package(Threads REQUIRED) From 42e05953504eabee32392169094194064a041dbe Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:31:04 +0100 Subject: [PATCH 35/44] fix: Make pointers const --- examples/discover/CMakeLists.txt | 2 +- examples/discover/run-test.sh | 3 - .../test/UdsDiagnosticTroubleCode_Test.cpp | 8 +- test/integration/discover/CMakeLists.txt | 99 +++---------------- test/integration/discover/run-test.sh | 14 +++ test/integration/uds-download/CMakeLists.txt | 2 +- test/integration/uds-download/run-test.sh | 3 +- 7 files changed, 35 insertions(+), 96 deletions(-) create mode 100644 test/integration/discover/run-test.sh diff --git a/examples/discover/CMakeLists.txt b/examples/discover/CMakeLists.txt index 294c29f..55a8a6d 100644 --- a/examples/discover/CMakeLists.txt +++ b/examples/discover/CMakeLists.txt @@ -38,6 +38,6 @@ if (WITH_INTEGRATION_TEST) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) set_tests_properties(Integration_UdpDiscover PROPERTIES - DEPENDS "setup_venv;copy_discover_script" + DEPENDS "copy_discover_script" ) endif() \ No newline at end of file diff --git a/examples/discover/run-test.sh b/examples/discover/run-test.sh index d9560df..088881d 100644 --- a/examples/discover/run-test.sh +++ b/examples/discover/run-test.sh @@ -1,7 +1,4 @@ #!/bin/bash -# Activate virtual environment -source venv/bin/activate - # Start server in background, log to server.log ./DoIPUdpServer > server.log 2>&1 & SERVER_PID=$! diff --git a/libs/uds/test/UdsDiagnosticTroubleCode_Test.cpp b/libs/uds/test/UdsDiagnosticTroubleCode_Test.cpp index efc8ddb..dbcd8bb 100644 --- a/libs/uds/test/UdsDiagnosticTroubleCode_Test.cpp +++ b/libs/uds/test/UdsDiagnosticTroubleCode_Test.cpp @@ -295,7 +295,7 @@ TEST_SUITE("DiagnosticTroubleCodeStore") { DiagnosticTroubleCode dtc(0x123456, 0xAB); store.addDTC(dtc); - DiagnosticTroubleCode *found = store.findDTC(0x123456); + const DiagnosticTroubleCode *found = store.findDTC(0x123456); CHECK(found != nullptr); CHECK(found->getCode() == 0x123456); CHECK(found->getStatusBits() == 0xAB); @@ -362,15 +362,15 @@ TEST_SUITE("DiagnosticTroubleCodeStore") { store.addDTC(DiagnosticTroubleCode(0x111111, 0xAA)); store.addDTC(DiagnosticTroubleCode(0x222222, 0xBB)); - DiagnosticTroubleCode *dtc0 = store.getDTCAt(0); + const DiagnosticTroubleCode *dtc0 = store.getDTCAt(0); CHECK(dtc0 != nullptr); CHECK(dtc0->getCode() == 0x111111); - DiagnosticTroubleCode *dtc1 = store.getDTCAt(1); + const DiagnosticTroubleCode *dtc1 = store.getDTCAt(1); CHECK(dtc1 != nullptr); CHECK(dtc1->getCode() == 0x222222); - DiagnosticTroubleCode *dtc_invalid = store.getDTCAt(999); + const DiagnosticTroubleCode *dtc_invalid = store.getDTCAt(999); CHECK(dtc_invalid == nullptr); } diff --git a/test/integration/discover/CMakeLists.txt b/test/integration/discover/CMakeLists.txt index 4446a45..6b46ec4 100644 --- a/test/integration/discover/CMakeLists.txt +++ b/test/integration/discover/CMakeLists.txt @@ -20,6 +20,7 @@ foreach(demo_source ${DISCOVER_SOURCES}) CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) # Disable switch-default warning for examples using spdlog @@ -39,93 +40,21 @@ foreach(demo_source ${DISCOVER_SOURCES}) endforeach() if (WITH_INTEGRATION_TEST) - message(STATUS "Adding integration tests for discover examples") + configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) - # Integration fixture: start DoIPServer as a daemon - add_test(NAME Integration_StartDiscoverServer - COMMAND bash -c "$ --daemon && sleep 1.0" + add_custom_target( + copy_test_script_discover ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh ) - set_tests_properties(Integration_StartDiscoverServer PROPERTIES - FIXTURES_SETUP demo_server - TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE - ) - - # Wait until both UDP 13400 and TCP 13400 are bound before running client tests - # This prevents race conditions where tests start before the server is ready - # Simplified script that works reliably in CI environments - add_test(NAME Integration_WaitForServerReady - COMMAND bash -c " - echo 'Waiting for server to bind ports...' - sleep 5 - - # First check if server process exists - if ! pgrep -f DiscoverServer >/dev/null 2>&1; then - echo 'ERROR: DiscoverServer process not running!' >&2 - exit 1 - fi - - for i in {1..150}; do - if command -v ss >/dev/null 2>&1; then - # Linux: use ss (without -p to avoid privilege restrictions) - udp_check=`ss -uln 2>/dev/null | grep -c ':13400'` - tcp_check=`ss -tln 2>/dev/null | grep -c ':13400'` - else - # macOS/BSD: use netstat - udp_check=`netstat -an 2>/dev/null | grep -c 'udp.*\\.13400'` - tcp_check=`netstat -an 2>/dev/null | grep -c '\\.13400.*LISTEN'` - fi - - if [ \$udp_check -gt 0 ] && [ \$tcp_check -gt 0 ]; then - echo 'Server ready: both UDP and TCP port 13400 are bound' - # Give server additional time to start sending announcements (important for sanitizers) - sleep 2 - exit 0 - fi - - if [ \$i -eq 50 ]; then - echo \"Still waiting... (UDP check: \$udp_check, TCP check: \$tcp_check)\" - pgrep -f DiscoverServer >/dev/null 2>&1 || echo 'WARNING: Server process died!' - fi - - sleep 0.1 - done - echo 'ERROR: Server not ready after 15 seconds (UDP: '\$udp_check', TCP: '\$tcp_check')' >&2 - pgrep -f DiscoverServer >/dev/null 2>&1 || echo 'Server process is NOT running' >&2 - exit 1 - " - ) - set_tests_properties(Integration_WaitForServerReady PROPERTIES - FIXTURES_REQUIRED demo_server - FIXTURES_SETUP demo_ready - TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE - ) - - # Integration test: run discover (udp client) against the running server - # CRITICAL: Must run serially and immediately after server is ready to catch announcements - add_test(NAME Integration_DiscoverClient_Runs - COMMAND $ - ) - set_tests_properties(Integration_DiscoverClient_Runs PROPERTIES - FIXTURES_REQUIRED demo_ready + add_test( + NAME Integration_UdpDiscoverCpp + COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(Integration_UdpDiscoverCpp PROPERTIES TIMEOUT 20 - LABELS integration - RUN_SERIAL TRUE - DEPENDS Integration_WaitForServerReady - ) - - # Teardown fixture: stop server and wait for ports to be released - add_test(NAME Integration_StopDiscoverServer - COMMAND bash -c "if [ -f /tmp/doip-discover.pid ]; then kill $(cat /tmp/doip-discover.pid) 2>/dev/null; rm -f /tmp/doip-discover.pid; else pkill -f DiscoverServer 2>/dev/null || true; fi; for i in $(seq 1 50); do if command -v ss >/dev/null 2>&1; then ss -tlpn 2>/dev/null | grep -q ':13400' || exit 0; else netstat -an 2>/dev/null | grep -q '\\.13400.*LISTEN' || exit 0; fi; sleep 0.1; done; echo 'Warning: Port 13400 still in use' >&2; exit 0" - ) - set_tests_properties(Integration_StopDiscoverServer PROPERTIES - FIXTURES_CLEANUP demo_server - TIMEOUT 10 - LABELS integration - RUN_SERIAL TRUE + DEPENDS copy_test_script_discover_cs + LABELS "integration" ) -endif() # WITH_INTEGRATION_TEST \ No newline at end of file +endif() \ No newline at end of file diff --git a/test/integration/discover/run-test.sh b/test/integration/discover/run-test.sh new file mode 100644 index 0000000..f39b218 --- /dev/null +++ b/test/integration/discover/run-test.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +./DiscoverServer & +SERVER_PID=$! +sleep 1 + +# Run tests +./DiscoverClient +CLIENT_EXIT=$? + +kill $SERVER_PID + +# Return client's exit code +exit $CLIENT_EXIT \ No newline at end of file diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index 4eaf07a..efe55dc 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -62,6 +62,6 @@ if (WITH_INTEGRATION_TEST) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) set_tests_properties(uds-download-test PROPERTIES - DEPENDS "setup_venv;copy_uds_download_script" + DEPENDS "copy_uds_download_script" ) endif() \ No newline at end of file diff --git a/test/integration/uds-download/run-test.sh b/test/integration/uds-download/run-test.sh index 5d9ced3..ac39881 100644 --- a/test/integration/uds-download/run-test.sh +++ b/test/integration/uds-download/run-test.sh @@ -1,6 +1,4 @@ #!/bin/bash -# Activate virtual environment -source venv/bin/activate # Start server in background, log to server.log ./UdsServer > server.log 2>&1 & @@ -15,6 +13,7 @@ CLIENT_EXIT=$? # Kill server kill $SERVER_PID +pkill UdsServer # Print logs for debugging echo "=== Server Log ===" From 9badff770f0b47a555f910d8b71de97aed6a20dd Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:52:31 +0100 Subject: [PATCH 36/44] chore: Add some debug output to track down CI problem --- test/integration/discover/run-test.sh | 2 ++ test/integration/uds-download/run-test.sh | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/integration/discover/run-test.sh b/test/integration/discover/run-test.sh index f39b218..8b67f20 100644 --- a/test/integration/discover/run-test.sh +++ b/test/integration/discover/run-test.sh @@ -9,6 +9,8 @@ sleep 1 CLIENT_EXIT=$? kill $SERVER_PID +pkill DiscoverServer + # Return client's exit code exit $CLIENT_EXIT \ No newline at end of file diff --git a/test/integration/uds-download/run-test.sh b/test/integration/uds-download/run-test.sh index ac39881..b5c95c9 100644 --- a/test/integration/uds-download/run-test.sh +++ b/test/integration/uds-download/run-test.sh @@ -1,6 +1,9 @@ #!/bin/bash # Start server in background, log to server.log +echo "Check for running servers..." >> server.log 2>&1 +ps -aux | grep Server >> server.log 2>&1 +echo "Starting UdsServer..." >> server.log 2>&1 ./UdsServer > server.log 2>&1 & SERVER_PID=$! @@ -8,7 +11,8 @@ SERVER_PID=$! sleep 1 # Run client, log to client.log -python3 ./test-local-client.py > client.log 2>&1 +ls -la >> client.log 2>&1 +python3 test-local-client.py > client.log 2>&1 CLIENT_EXIT=$? # Kill server From 252ca1cbc169df853b8bda91ced930e0d3a46246 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:52:45 +0100 Subject: [PATCH 37/44] chore: Rename test to match conventions --- test/integration/uds-download/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index efe55dc..22de506 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -57,11 +57,11 @@ message(STATUS "Adding integration tests for examples") if (WITH_INTEGRATION_TEST) add_test( - NAME uds-download-test + NAME Integration-UdsDownload COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) - set_tests_properties(uds-download-test PROPERTIES + set_tests_properties(Integration-UdsDownload PROPERTIES DEPENDS "copy_uds_download_script" ) endif() \ No newline at end of file From 35206ae29f6634635705f09b0f9439d27c032b4f Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 18:58:17 +0100 Subject: [PATCH 38/44] chore: Copy run-test only when integration tests are active, add debug --- test/integration/discover/CMakeLists.txt | 1 + test/integration/discover/run-test.sh | 10 ++++++++-- test/integration/uds-download/CMakeLists.txt | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/test/integration/discover/CMakeLists.txt b/test/integration/discover/CMakeLists.txt index 6b46ec4..30301c7 100644 --- a/test/integration/discover/CMakeLists.txt +++ b/test/integration/discover/CMakeLists.txt @@ -40,6 +40,7 @@ foreach(demo_source ${DISCOVER_SOURCES}) endforeach() if (WITH_INTEGRATION_TEST) + message(STATUS "Building Discover C++ Client integration test in ${CMAKE_CURRENT_BINARY_DIR}") configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) add_custom_target( diff --git a/test/integration/discover/run-test.sh b/test/integration/discover/run-test.sh index 8b67f20..95568e3 100644 --- a/test/integration/discover/run-test.sh +++ b/test/integration/discover/run-test.sh @@ -1,16 +1,22 @@ #!/bin/bash set -e -./DiscoverServer & +./DiscoverServer > server.log 2>&1 & SERVER_PID=$! sleep 1 # Run tests -./DiscoverClient +./DiscoverClient > client.log 2>&1 CLIENT_EXIT=$? kill $SERVER_PID pkill DiscoverServer +# Print logs for debugging +echo "=== Server Log ===" +tail server.log +echo "=== Client Log ===" +cat client.log + # Return client's exit code exit $CLIENT_EXIT \ No newline at end of file diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index 22de506..7ef8b94 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -38,7 +38,6 @@ endforeach() set(SOURCE_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/test-uds-download.py) set(DESTINATION_SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/test-uds-download.py) configure_file(${SOURCE_SCRIPT} ${DESTINATION_SCRIPT} COPYONLY) -configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) # Add a custom command to copy the file only if it changes add_custom_command( @@ -56,6 +55,9 @@ add_custom_target(copy_uds_download_script ALL DEPENDS ${DESTINATION_SCRIPT}) message(STATUS "Adding integration tests for examples") if (WITH_INTEGRATION_TEST) + message(STATUS "Building UdsDownload integration test in ${CMAKE_CURRENT_BINARY_DIR}") + configure_file(run-test.sh ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh COPYONLY) + add_test( NAME Integration-UdsDownload COMMAND bash ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh From a76d97aa50d1ba95ba2914f5ccb483043e3d04b7 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 19:27:20 +0100 Subject: [PATCH 39/44] chore: Add port availability check and improve logging in UdsServer script --- examples/discover/run-test.sh | 2 + examples/minimal/run-test.sh | 2 + test/integration/discover/DiscoverServer.cpp | 3 +- test/integration/discover/run-test.sh | 1 - test/integration/uds-download/UdsServer.cpp | 6 +-- test/integration/uds-download/run-test.sh | 52 ++++++++++++++++++-- 6 files changed, 55 insertions(+), 11 deletions(-) diff --git a/examples/discover/run-test.sh b/examples/discover/run-test.sh index 088881d..f6ece52 100644 --- a/examples/discover/run-test.sh +++ b/examples/discover/run-test.sh @@ -13,6 +13,8 @@ CLIENT_EXIT=$? # Kill server kill $SERVER_PID +sleep 1 + # Print logs for debugging echo "=== Server Log ===" tail server.log diff --git a/examples/minimal/run-test.sh b/examples/minimal/run-test.sh index e5559fd..f5c3093 100644 --- a/examples/minimal/run-test.sh +++ b/examples/minimal/run-test.sh @@ -10,5 +10,7 @@ CLIENT_EXIT=$? kill $SERVER_PID +sleep 1 + # Return client's exit code exit $CLIENT_EXIT \ No newline at end of file diff --git a/test/integration/discover/DiscoverServer.cpp b/test/integration/discover/DiscoverServer.cpp index 37cf69c..7288b9b 100644 --- a/test/integration/discover/DiscoverServer.cpp +++ b/test/integration/discover/DiscoverServer.cpp @@ -99,7 +99,6 @@ int main(int argc, char *argv[]) { if (cfg.daemonize) { (void)std::remove(PID_FILE); } - // Cleanly shutdown loggers to avoid sanitizer leak reports - doip::Logger::shutdown(); + return 0; } diff --git a/test/integration/discover/run-test.sh b/test/integration/discover/run-test.sh index 95568e3..7486992 100644 --- a/test/integration/discover/run-test.sh +++ b/test/integration/discover/run-test.sh @@ -9,7 +9,6 @@ sleep 1 CLIENT_EXIT=$? kill $SERVER_PID -pkill DiscoverServer # Print logs for debugging echo "=== Server Log ===" diff --git a/test/integration/uds-download/UdsServer.cpp b/test/integration/uds-download/UdsServer.cpp index 7199444..679afe4 100644 --- a/test/integration/uds-download/UdsServer.cpp +++ b/test/integration/uds-download/UdsServer.cpp @@ -15,7 +15,7 @@ using namespace doip; -const char* PID_FILE = "/tmp/doip-discover.pid"; +const char* PID_FILE = "/tmp/doip-download.pid"; std::unique_ptr server; static std::atomic stopRequested{false}; @@ -33,7 +33,7 @@ int main(int argc, char *argv[]) { cfg.loopback = true; // For testing, use loopback announcements cfg.properties.logicalAddress = DoIPAddress(0x00E0); // Match client logical address used in tests cfg.daemonize = argc > 1 && std::string(argv[1]) == "--daemon"; // For testing, run as daemon - auto console = spdlog::stdout_color_mt("doip-server"); + auto console = spdlog::stdout_color_mt("uds-server"); Logger::setUseSyslog(cfg.daemonize); @@ -58,7 +58,7 @@ int main(int argc, char *argv[]) { // Configure logging Logger::setLevel(spdlog::level::debug); - console->info("Starting DoIP Discovery Server"); + console->info("Starting DoIP UDS Download Server"); server = std::make_unique(cfg); diff --git a/test/integration/uds-download/run-test.sh b/test/integration/uds-download/run-test.sh index b5c95c9..09e5ee0 100644 --- a/test/integration/uds-download/run-test.sh +++ b/test/integration/uds-download/run-test.sh @@ -1,23 +1,65 @@ #!/bin/bash +# Wait for port 13400 to be free (max 10 seconds) +echo "Checking if port 13400 is available..." +TIMEOUT=100 # 10 seconds (100 * 0.1s) +ELAPSED=0 + +while [ $ELAPSED -lt $TIMEOUT ]; do + PORT_IN_USE=0 + + if command -v ss >/dev/null 2>&1; then + # Linux: use ss + if ss -tln 2>/dev/null | grep -q ':13400'; then + PORT_IN_USE=1 + fi + elif command -v netstat >/dev/null 2>&1; then + # macOS/BSD: use netstat + if netstat -an 2>/dev/null | grep -q '\.13400.*LISTEN'; then + PORT_IN_USE=1 + fi + elif command -v lsof >/dev/null 2>&1; then + # Fallback: use lsof + if lsof -i :13400 >/dev/null 2>&1; then + PORT_IN_USE=1 + fi + fi + + if [ $PORT_IN_USE -eq 0 ]; then + echo "Port 13400 is available" + break + fi + + if [ $ELAPSED -eq 0 ]; then + echo "Port 13400 is still in use, waiting..." + fi + + sleep 0.1 + ELAPSED=$((ELAPSED + 1)) +done + +if [ $PORT_IN_USE -eq 1 ]; then + echo "ERROR: Port 13400 still in use after 10 seconds timeout" >&2 + exit 1 +fi + # Start server in background, log to server.log -echo "Check for running servers..." >> server.log 2>&1 +echo "Check for running servers..." > server.log 2>&1 ps -aux | grep Server >> server.log 2>&1 echo "Starting UdsServer..." >> server.log 2>&1 -./UdsServer > server.log 2>&1 & +./UdsServer >> server.log 2>&1 & SERVER_PID=$! # Give the server a moment to start sleep 1 # Run client, log to client.log -ls -la >> client.log 2>&1 -python3 test-local-client.py > client.log 2>&1 +ls -la > client.log 2>&1 +python3 test-local-client.py >> client.log 2>&1 CLIENT_EXIT=$? # Kill server kill $SERVER_PID -pkill UdsServer # Print logs for debugging echo "=== Server Log ===" From 3d386ab5d976bf721774b0afbacd97d3f257b0f9 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 20:20:46 +0100 Subject: [PATCH 40/44] fix: Update client script name in run-test.sh to match the correct test file --- test/integration/uds-download/run-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/uds-download/run-test.sh b/test/integration/uds-download/run-test.sh index 09e5ee0..bfe0049 100644 --- a/test/integration/uds-download/run-test.sh +++ b/test/integration/uds-download/run-test.sh @@ -55,7 +55,7 @@ sleep 1 # Run client, log to client.log ls -la > client.log 2>&1 -python3 test-local-client.py >> client.log 2>&1 +python3 test-uds-download.py >> client.log 2>&1 CLIENT_EXIT=$? # Kill server From 67c0d382c02e3b1c91c6302fb6589e4e853ea0f9 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 21:42:05 +0100 Subject: [PATCH 41/44] chore: Remove port check, add resource lock --- examples/discover/CMakeLists.txt | 2 + examples/discover/run-test.sh | 2 +- examples/minimal/CMakeLists.txt | 1 + test/integration/discover/CMakeLists.txt | 2 + test/integration/discover/DiscoverClient.cpp | 1 - test/integration/discover/run-test.sh | 2 + test/integration/uds-download/CMakeLists.txt | 2 + test/integration/uds-download/run-test.sh | 43 -------------------- 8 files changed, 10 insertions(+), 45 deletions(-) diff --git a/examples/discover/CMakeLists.txt b/examples/discover/CMakeLists.txt index 55a8a6d..9fefcbb 100644 --- a/examples/discover/CMakeLists.txt +++ b/examples/discover/CMakeLists.txt @@ -39,5 +39,7 @@ if (WITH_INTEGRATION_TEST) ) set_tests_properties(Integration_UdpDiscover PROPERTIES DEPENDS "copy_discover_script" + LABELS "integration" + RESOURCE_LOCK port_13400 ) endif() \ No newline at end of file diff --git a/examples/discover/run-test.sh b/examples/discover/run-test.sh index f6ece52..07e8770 100644 --- a/examples/discover/run-test.sh +++ b/examples/discover/run-test.sh @@ -13,7 +13,7 @@ CLIENT_EXIT=$? # Kill server kill $SERVER_PID -sleep 1 +sleep 2 # Print logs for debugging echo "=== Server Log ===" diff --git a/examples/minimal/CMakeLists.txt b/examples/minimal/CMakeLists.txt index f1bf358..f09b02e 100644 --- a/examples/minimal/CMakeLists.txt +++ b/examples/minimal/CMakeLists.txt @@ -42,5 +42,6 @@ if (WITH_INTEGRATION_TEST) TIMEOUT 20 DEPENDS copy_test_script_minimal_cs LABELS "integration" + RESOURCE_LOCK port_13400 ) endif() \ No newline at end of file diff --git a/test/integration/discover/CMakeLists.txt b/test/integration/discover/CMakeLists.txt index 30301c7..eafaa6c 100644 --- a/test/integration/discover/CMakeLists.txt +++ b/test/integration/discover/CMakeLists.txt @@ -46,6 +46,7 @@ if (WITH_INTEGRATION_TEST) add_custom_target( copy_test_script_discover ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/run-test.sh + ) add_test( @@ -57,5 +58,6 @@ if (WITH_INTEGRATION_TEST) TIMEOUT 20 DEPENDS copy_test_script_discover_cs LABELS "integration" + RESOURCE_LOCK port_13400 ) endif() \ No newline at end of file diff --git a/test/integration/discover/DiscoverClient.cpp b/test/integration/discover/DiscoverClient.cpp index 40c15e2..79b668f 100644 --- a/test/integration/discover/DiscoverClient.cpp +++ b/test/integration/discover/DiscoverClient.cpp @@ -8,7 +8,6 @@ using std::string; DoIPClient client; int main() { - string serverAddress = "127.0.0.1"; // Default to loopback for testing to same hosts auto console = spdlog::stdout_color_mt("discover-client"); console->info("Starting DoIP Client"); diff --git a/test/integration/discover/run-test.sh b/test/integration/discover/run-test.sh index 7486992..3b0d1ea 100644 --- a/test/integration/discover/run-test.sh +++ b/test/integration/discover/run-test.sh @@ -10,6 +10,8 @@ CLIENT_EXIT=$? kill $SERVER_PID +sleep 1 + # Print logs for debugging echo "=== Server Log ===" tail server.log diff --git a/test/integration/uds-download/CMakeLists.txt b/test/integration/uds-download/CMakeLists.txt index 7ef8b94..45c6122 100644 --- a/test/integration/uds-download/CMakeLists.txt +++ b/test/integration/uds-download/CMakeLists.txt @@ -65,5 +65,7 @@ if (WITH_INTEGRATION_TEST) ) set_tests_properties(Integration-UdsDownload PROPERTIES DEPENDS "copy_uds_download_script" + LABELS "integration" + RESOURCE_LOCK port_13400 ) endif() \ No newline at end of file diff --git a/test/integration/uds-download/run-test.sh b/test/integration/uds-download/run-test.sh index bfe0049..5a74763 100644 --- a/test/integration/uds-download/run-test.sh +++ b/test/integration/uds-download/run-test.sh @@ -1,48 +1,5 @@ #!/bin/bash -# Wait for port 13400 to be free (max 10 seconds) -echo "Checking if port 13400 is available..." -TIMEOUT=100 # 10 seconds (100 * 0.1s) -ELAPSED=0 - -while [ $ELAPSED -lt $TIMEOUT ]; do - PORT_IN_USE=0 - - if command -v ss >/dev/null 2>&1; then - # Linux: use ss - if ss -tln 2>/dev/null | grep -q ':13400'; then - PORT_IN_USE=1 - fi - elif command -v netstat >/dev/null 2>&1; then - # macOS/BSD: use netstat - if netstat -an 2>/dev/null | grep -q '\.13400.*LISTEN'; then - PORT_IN_USE=1 - fi - elif command -v lsof >/dev/null 2>&1; then - # Fallback: use lsof - if lsof -i :13400 >/dev/null 2>&1; then - PORT_IN_USE=1 - fi - fi - - if [ $PORT_IN_USE -eq 0 ]; then - echo "Port 13400 is available" - break - fi - - if [ $ELAPSED -eq 0 ]; then - echo "Port 13400 is still in use, waiting..." - fi - - sleep 0.1 - ELAPSED=$((ELAPSED + 1)) -done - -if [ $PORT_IN_USE -eq 1 ]; then - echo "ERROR: Port 13400 still in use after 10 seconds timeout" >&2 - exit 1 -fi - # Start server in background, log to server.log echo "Check for running servers..." > server.log 2>&1 ps -aux | grep Server >> server.log 2>&1 From 4852b4f31203348b4216866c25806b9e8f3f95b0 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 21:43:21 +0100 Subject: [PATCH 42/44] chore: Run tests without integration but parallel chore: Run integration tests only for coverage, but run tests strictly serial --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e626bfa..2f12f57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,7 @@ jobs: -DCMAKE_CXX_COMPILER=${{ matrix.config.cxx }} \ -DCMAKE_BUILD_TYPE=${{ matrix.config.build_type }} \ -DWITH_UNIT_TEST=ON \ + -DWITH_INTEGRATION=OFF \ -DWARNINGS_AS_ERRORS=ON - name: Configure CMake (Windows) @@ -172,6 +173,7 @@ jobs: -DCMAKE_CXX_COMPILER=g++-11 \ -DCMAKE_BUILD_TYPE=Debug \ -DWITH_UNIT_TEST=ON \ + -DWITH_INTEGRATION=OFF \ -DENABLE_SANITIZERS=ON \ -DWARNINGS_AS_ERRORS=ON @@ -260,6 +262,7 @@ jobs: -DCMAKE_CXX_COMPILER=g++-11 \ -DCMAKE_BUILD_TYPE=Debug \ -DWITH_UNIT_TEST=ON \ + -DWITH_INTEGRATION=ON \ -DCMAKE_CXX_FLAGS="--coverage" \ -DCMAKE_C_FLAGS="--coverage" \ -DWARNINGS_AS_ERRORS=OFF From 37bb5dc1440d9be691a5c5ca43e8b3f80e3b6892 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 21:46:00 +0100 Subject: [PATCH 43/44] chore: Run tests without integration but parallel chore: Run integration tests only for coverage, but run tests strictly serial --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f12f57..23282c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: -DCMAKE_CXX_COMPILER=${{ matrix.config.cxx }} \ -DCMAKE_BUILD_TYPE=${{ matrix.config.build_type }} \ -DWITH_UNIT_TEST=ON \ - -DWITH_INTEGRATION=OFF \ + -DWITH_INTEGRATION_TEST=OFF \ -DWARNINGS_AS_ERRORS=ON - name: Configure CMake (Windows) @@ -173,7 +173,7 @@ jobs: -DCMAKE_CXX_COMPILER=g++-11 \ -DCMAKE_BUILD_TYPE=Debug \ -DWITH_UNIT_TEST=ON \ - -DWITH_INTEGRATION=OFF \ + -DWITH_INTEGRATION_TEST=OFF \ -DENABLE_SANITIZERS=ON \ -DWARNINGS_AS_ERRORS=ON @@ -262,7 +262,7 @@ jobs: -DCMAKE_CXX_COMPILER=g++-11 \ -DCMAKE_BUILD_TYPE=Debug \ -DWITH_UNIT_TEST=ON \ - -DWITH_INTEGRATION=ON \ + -DWITH_INTEGRATION_TEST=ON \ -DCMAKE_CXX_FLAGS="--coverage" \ -DCMAKE_C_FLAGS="--coverage" \ -DWARNINGS_AS_ERRORS=OFF From efe402142a4f1d2f187ca638a50d28f1fa371b94 Mon Sep 17 00:00:00 2001 From: Oliver Wieland Date: Thu, 8 Jan 2026 21:47:23 +0100 Subject: [PATCH 44/44] chore: Use 6 cores --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23282c4..9912407 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,7 +98,7 @@ jobs: cmake -B build -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=${{ matrix.config.build_type }} -DWITH_UNIT_TEST=ON -DWARNINGS_AS_ERRORS=OFF - name: Build - run: cmake --build build --config ${{ matrix.config.build_type }} --parallel 4 + run: cmake --build build --config ${{ matrix.config.build_type }} --parallel 6 - name: Run tests working-directory: build